diff --git a/.gitignore b/.gitignore index c966a11..668a60f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,8 +19,8 @@ dist/ # Useless stuffs .DS_Store -# Added by goreleaser init: -.intentionally-empty-file.o .cargo/ vendor/ +wasm/ +node_modules/ diff --git a/Cargo.lock b/Cargo.lock index 96b39ac..3fc5630 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,9 +316,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" dependencies = [ "compression-codecs", "compression-core", @@ -457,9 +457,9 @@ dependencies = [ [[package]] name = "async-utility" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151" +checksum = "188f83b9a198af8c336e505611edb00d6d2ac5c694241c5a4f9a12316938cfe9" dependencies = [ "futures-util", "gloo-timers", @@ -469,9 +469,9 @@ dependencies = [ [[package]] name = "async-wsocket" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c69f24cf7a798027c6b0801de5bfd05d01fe343e87a8329f7c8b46a6d3bc4a79" +checksum = "efa9ef232c1731ddbe4a306b5b5dcf9e50a924d6bd086d4784939d14f8e78daa" dependencies = [ "futures", "futures-util", @@ -543,6 +543,7 @@ dependencies = [ "anyhow", "common", "gpui", + "instant", "log", "semver", "serde", @@ -1056,8 +1057,10 @@ dependencies = [ "common", "device", "flume 0.11.1", + "futures", "fuzzy-matcher", "gpui", + "instant", "itertools 0.13.0", "log", "nostr-sdk", @@ -1076,6 +1079,7 @@ dependencies = [ "chat", "common", "flume 0.11.1", + "futures", "gpui", "itertools 0.13.0", "linkify", @@ -1086,7 +1090,6 @@ dependencies = [ "serde", "settings", "smallvec", - "smol", "state", "theme", "ui", @@ -1130,9 +1133,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.8.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" dependencies = [ "glob", "libc", @@ -1228,7 +1231,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "gpui_util", "indexmap", @@ -1259,6 +1262,7 @@ dependencies = [ "dirs 5.0.1", "futures", "gpui", + "instant", "itertools 0.13.0", "log", "nostr", @@ -1384,21 +1388,21 @@ dependencies = [ "console_error_panic_hook", "console_log", "device", - "errno", "getrandom 0.2.17", "getrandom 0.3.4", "getrandom 0.4.3", "gpui", "gpui_platform", "gpui_web", + "instant", "log", "person", "settings", "state", "theme", - "tokio", "tracing-wasm", "ui", + "universal-time 0.3.1 (git+https://github.com/shadowylab/universal-time)", "wasm-bindgen", "workspace", ] @@ -1650,9 +1654,9 @@ dependencies = [ [[package]] name = "ctor" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e30e509674ef0ec91e21a7735766db37d163d46151b6a361d8b83dd79116bd" +checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f" dependencies = [ "link-section", "linktime-proc-macro", @@ -1711,7 +1715,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "proc-macro2", "quote", @@ -1726,6 +1730,7 @@ dependencies = [ "common", "flume 0.11.1", "gpui", + "instant", "log", "nostr-sdk", "person", @@ -1822,13 +1827,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -1903,7 +1908,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "vswhom", "winreg", ] @@ -2023,11 +2028,10 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -2607,7 +2611,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "accesskit", "anyhow", @@ -2689,7 +2693,7 @@ dependencies = [ [[package]] name = "gpui_linux" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "accesskit", "accesskit_unix", @@ -2741,7 +2745,7 @@ dependencies = [ [[package]] name = "gpui_macos" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "accesskit", "accesskit_macos", @@ -2790,7 +2794,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2801,7 +2805,7 @@ dependencies = [ [[package]] name = "gpui_platform" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "console_error_panic_hook", "gpui", @@ -2814,7 +2818,7 @@ dependencies = [ [[package]] name = "gpui_shared_string" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "schemars", "serde", @@ -2824,7 +2828,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "anyhow", "gpui", @@ -2835,7 +2839,7 @@ dependencies = [ [[package]] name = "gpui_util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "anyhow", "log", @@ -2845,7 +2849,7 @@ dependencies = [ [[package]] name = "gpui_web" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "anyhow", "console_error_panic_hook", @@ -2857,7 +2861,6 @@ dependencies = [ "log", "parking_lot", "raw-window-handle", - "smallvec", "uuid", "wasm-bindgen", "wasm-bindgen-futures", @@ -2869,7 +2872,7 @@ dependencies = [ [[package]] name = "gpui_wgpu" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "anyhow", "bytemuck", @@ -2887,6 +2890,7 @@ dependencies = [ "raw-window-handle", "smallvec", "swash", + "unicode-bidi", "unicode-segmentation", "wasm-bindgen", "wasm-bindgen-futures", @@ -2898,7 +2902,7 @@ dependencies = [ [[package]] name = "gpui_windows" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "accesskit", "accesskit_windows", @@ -3154,9 +3158,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -3188,7 +3192,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "anyhow", "async-compression", @@ -3208,7 +3212,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3493,6 +3497,18 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -3780,9 +3796,9 @@ checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" [[package]] name = "link-section" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc98458dfe90986c5e2f6ddcf68360c7e5c4252600153e06aa4ee8176c0f8d1" +checksum = "5ee1a0d6e252afe82e7bc2db42fba60e02ddf3b1accaf8cb21d96e34ba61f3d4" [[package]] name = "linkify" @@ -3795,9 +3811,9 @@ dependencies = [ [[package]] name = "linktime-proc-macro" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c7b0a3383c2a1002d11349c92c85a666a5fb679e96c79d782cf0dbe557fd6ee" +checksum = "348d0075b1fc163b26d72a7f75fc5141daf2fd1bdf128d873cbaf6785d495bdf" [[package]] name = "linux-raw-sys" @@ -4003,7 +4019,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "anyhow", "bindgen", @@ -4196,8 +4212,8 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" -version = "0.45.0-alpha.4" -source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841" +version = "0.45.0-alpha.7" +source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5" dependencies = [ "aes", "base64", @@ -4215,14 +4231,14 @@ dependencies = [ "serde", "serde_json", "unicode-normalization", - "universal-time", + "universal-time 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "url", ] [[package]] name = "nostr-blossom" -version = "0.45.0-alpha.4" -source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841" +version = "0.45.0-alpha.7" +source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5" dependencies = [ "base64", "nostr", @@ -4233,8 +4249,8 @@ dependencies = [ [[package]] name = "nostr-connect" -version = "0.45.0-alpha.4" -source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841" +version = "0.45.0-alpha.7" +source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5" dependencies = [ "async-utility", "futures-core", @@ -4247,8 +4263,8 @@ dependencies = [ [[package]] name = "nostr-database" -version = "0.45.0-alpha.4" -source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841" +version = "0.45.0-alpha.7" +source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5" dependencies = [ "btreecap", "flatbuffers", @@ -4258,8 +4274,8 @@ dependencies = [ [[package]] name = "nostr-gossip" -version = "0.45.0-alpha.4" -source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841" +version = "0.45.0-alpha.7" +source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5" dependencies = [ "nostr", "opaquerr", @@ -4267,8 +4283,8 @@ dependencies = [ [[package]] name = "nostr-gossip-memory" -version = "0.45.0-alpha.4" -source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841" +version = "0.45.0-alpha.7" +source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5" dependencies = [ "indexmap", "lru", @@ -4279,8 +4295,8 @@ dependencies = [ [[package]] name = "nostr-lmdb" -version = "0.45.0-alpha.4" -source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841" +version = "0.45.0-alpha.7" +source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5" dependencies = [ "async-utility", "flume 0.12.0", @@ -4293,8 +4309,8 @@ dependencies = [ [[package]] name = "nostr-memory" -version = "0.45.0-alpha.4" -source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841" +version = "0.45.0-alpha.7" +source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5" dependencies = [ "btreecap", "nostr", @@ -4304,8 +4320,8 @@ dependencies = [ [[package]] name = "nostr-sdk" -version = "0.45.0-alpha.4" -source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841" +version = "0.45.0-alpha.7" +source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5" dependencies = [ "async-utility", "async-wsocket", @@ -4320,7 +4336,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", - "universal-time", + "universal-time 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -4906,7 +4922,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "collections", "serde", @@ -4921,6 +4937,7 @@ dependencies = [ "common", "flume 0.11.1", "gpui", + "instant", "log", "nostr-sdk", "smallvec", @@ -5717,7 +5734,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "derive_refineable", ] @@ -5799,7 +5816,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "anyhow", "bytes", @@ -5969,9 +5986,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "log", @@ -6114,7 +6131,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "async-task", "backtrace", @@ -6128,9 +6145,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "dyn-clone", "indexmap", @@ -6142,14 +6159,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -6207,12 +6224,13 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "secp256k1" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ + "bitcoin_hashes", + "rand 0.8.7", "secp256k1-sys", - "serde", ] [[package]] @@ -6305,13 +6323,13 @@ dependencies = [ [[package]] name = "serde_derive_internals" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -6666,6 +6684,7 @@ dependencies = [ "futures", "gpui", "gpui_tokio", + "instant", "log", "mime_guess", "nostr", @@ -6738,7 +6757,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "heapless 0.9.3", "log", @@ -6749,34 +6768,35 @@ dependencies = [ [[package]] name = "sval" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb9efbae90f97301f4d25f3be63dfd99d6b7af9d088228a52ec960d649b2e7d" +checksum = "ba3a370f3cd0422964fd9a19b6516f048527738e6ec50b1b0ff79b460b468390" [[package]] name = "sval_buffer" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff5c0280ea0af40b3a1fd0b532680b5067482ffb81412ee66ec04b1d9952b49a" +checksum = "111e6e2dab25782acb41b281263f5f60d6f56a2ba0b3b076187ad23a1552544d" dependencies = [ "sval", "sval_ref", + "zerocopy", ] [[package]] name = "sval_dynamic" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b9067f2e68f58e110cf8019a268057e3791cf74b0fdb1b3c7c6e49104f44e6" +checksum = "590ac4ebd299740eac453162302a494e6d5b3be243feab8bf07d0d4331b6b2b3" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ebdbf0e4b175884aa587fcf551c16dabe245ea04235abdd8cbbd160b27ed5a" +checksum = "4b964d6d917267355d7f343c515b908586e5b4aeeada328738f703452f9412d6" dependencies = [ "itoa", "ryu", @@ -6785,9 +6805,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e448d9fa216a6c16670b28d624fbbcf5c04e41eb187bd7c52e01222ffa72a12" +checksum = "dea35e4d6b997acc7dc4786ab782f6a6c5000efe4ae93a2db89cf350775fe5fe" dependencies = [ "itoa", "ryu", @@ -6796,9 +6816,9 @@ dependencies = [ [[package]] name = "sval_nested" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021de5b5c26efd544c694cef9b8a9abe8633481bf3be1ea145a18a740818b291" +checksum = "a73195ebd4b3e5866db2e1b75f3f383657ad5abc600c646d69b4f064fee89154" dependencies = [ "sval", "sval_buffer", @@ -6807,18 +6827,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54ef5ffec8bc52ded04ee424ab8d959e25e64fd40a48a16d21f8991ce824c1bb" +checksum = "c0ef28df9d586bfd15115286651337cb5645f8c203160d22d2d1a20cf13c428c" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b983833a8a2390f89ebcf9b9acd06883017014b4ffd72ee28e0c9de6852039f5" +checksum = "d72fbe6537e88878f10237bde71ae4a1b65b2bf54bdf274abc566fc696334c92" dependencies = [ "serde_core", "sval", @@ -7186,13 +7206,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -7273,9 +7293,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -7332,9 +7352,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow 1.0.4", ] @@ -7553,6 +7573,7 @@ dependencies = [ "anyhow", "common", "gpui", + "instant", "itertools 0.13.0", "log", "lsp-types", @@ -7661,9 +7682,14 @@ dependencies = [ [[package]] name = "universal-time" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f942cacf37a673350e8f252dff4691fb8e525a5fff6e1f2c4b6c2f3b8349248d" +checksum = "47a939edecc3c5a7b83c02e5f6b3c31d2bc69eabcc9a87ab12c6d37ee6dbc856" + +[[package]] +name = "universal-time" +version = "0.3.1" +source = "git+https://github.com/shadowylab/universal-time#394cd158556e1162806f6066fe89c413c06f9421" [[package]] name = "untrusted" @@ -7732,7 +7758,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "perf", "quote", @@ -7942,8 +7968,7 @@ dependencies = [ [[package]] name = "wasm_thread" version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7516db7f32decdadb1c3b8deb1b7d78b9df7606c5cc2f6241737c2ab3a0258e" +source = "git+https://github.com/zed-industries/wasm_thread?rev=0cf96c7708dfb97ccf3da50347e25edcf75d6937#0cf96c7708dfb97ccf3da50347e25edcf75d6937" dependencies = [ "futures", "js-sys", @@ -8071,15 +8096,15 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" +checksum = "ef62a3d5f7b2411119a11b6f62570dbff91d7105e011a20fb83fbf8f5761c40f" dependencies = [ - "core-foundation 0.10.1", "jni 0.22.4", "log", "ndk-context", "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "url", "web-sys", @@ -8990,6 +9015,7 @@ dependencies = [ "common", "device", "gpui", + "instant", "log", "nostr-connect", "nostr-sdk", @@ -9465,7 +9491,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "anyhow", "chrono", @@ -9482,7 +9508,7 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" dependencies = [ "tracing", "tracing-subscriber", @@ -9493,7 +9519,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331" +source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114" [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index 67a9c89..980975d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,13 +24,12 @@ nostr-memory = { git = "https://github.com/rust-nostr/nostr" } nostr-blossom = { git = "https://github.com/rust-nostr/nostr" } nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" } nostr-connect = { git = "https://github.com/rust-nostr/nostr" } - nostr-sdk = { git = "https://github.com/rust-nostr/nostr" } nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44" ] } # Others anyhow = "1.0.44" -chrono = "0.4.38" +chrono = { version = "0.4.38", features = ["wasmbind"] } futures = "0.3" itertools = "0.13.0" log = "0.4" @@ -44,6 +43,8 @@ smallvec = "1.14.0" smol = "2" webbrowser = "1.0.4" tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] } +errno = { version = "0.3.14", default-features = false } +instant = "0.1" [patch.crates-io] # Use stacker's psm version which may have better WASM support diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index aff6855..6c3767a 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -8,6 +8,7 @@ publish.workspace = true common = { path = "../common" } gpui.workspace = true +instant.workspace = true anyhow.workspace = true log.workspace = true smallvec.workspace = true diff --git a/crates/auto_update/src/lib.rs b/crates/auto_update/src/lib.rs index b57f292..8a46a73 100644 --- a/crates/auto_update/src/lib.rs +++ b/crates/auto_update/src/lib.rs @@ -3,7 +3,6 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::sync::Arc; -use std::time::Duration; use anyhow::{Context as AnyhowContext, Error, anyhow}; use gpui::http_client::{AsyncBody, HttpClient}; @@ -11,6 +10,7 @@ use gpui::{ App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task, Window, }; +use instant::Duration; use semver::Version; use serde::Deserialize; use smallvec::{SmallVec, smallvec}; diff --git a/crates/chat/Cargo.toml b/crates/chat/Cargo.toml index 3117b91..ad7fd71 100644 --- a/crates/chat/Cargo.toml +++ b/crates/chat/Cargo.toml @@ -13,6 +13,7 @@ settings = { path = "../settings" } gpui.workspace = true nostr-sdk.workspace = true +instant.workspace = true anyhow.workspace = true itertools.workspace = true @@ -20,6 +21,7 @@ smallvec.workspace = true log.workspace = true flume.workspace = true +futures.workspace = true fuzzy-matcher = "0.3.7" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index acd5c7f..a6c9ec2 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -1,13 +1,9 @@ use std::cmp::Reverse; use std::collections::{BTreeSet, HashMap, HashSet}; -use std::hash::{DefaultHasher, Hash, Hasher}; -use std::sync::Arc; -#[cfg(target_arch = "wasm32")] -use std::sync::RwLock; use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::Duration; +use std::sync::{Arc, LazyLock, RwLock}; -use anyhow::{Context as AnyhowContext, Error, anyhow}; +use anyhow::{Error, anyhow}; use common::EventExt; use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::skim::SkimMatcherV2; @@ -15,10 +11,9 @@ use gpui::{ App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task, WeakEntity, Window, }; +use instant::Duration; use nostr_sdk::prelude::*; use smallvec::{SmallVec, smallvec}; -#[cfg(not(target_arch = "wasm32"))] -use smol::lock::RwLock; use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP, UniversalSigner}; mod message; @@ -27,6 +22,9 @@ mod room; pub use message::*; pub use room::*; +/// A static keypair used only for signing locally-cached rumor events. +static LOCAL_KEYS: LazyLock = LazyLock::new(Keys::generate); + pub fn init(window: &mut Window, cx: &mut App) { ChatRegistry::set_global(cx.new(|cx| ChatRegistry::new(window, cx)), cx); } @@ -83,15 +81,15 @@ pub struct ChatRegistry { rooms: Vec>, /// Events that failed to unwrap for any reason - trashes: Entity>, + trash: Entity>, /// Tracking events seen on which relays in the current session - seens: Arc>>>, + seen: Arc>>>, /// Mapping of unwrapped event ids to their gift wrap event ids event_map: Arc>>, - /// Tracking the status of unwrapping gift wrap events. + /// True while the initial event backlog is still loading tracking: Arc, /// Channel for sending signals to the UI. @@ -103,10 +101,37 @@ pub struct ChatRegistry { /// Async tasks tasks: SmallVec<[Task>; 2]>, + /// Notification listener task (cancelled on signer change) + notification_listener: Option>>, + + /// Signal consumer task (cancelled on signer change) + signal_consumer: Option>>, + + /// Fuzzy matcher for room search (cached; intentionally excluded from Debug) + #[allow(dead_code)] + matcher: CachedMatcher, + /// Subscriptions _subscriptions: SmallVec<[Subscription; 2]>, } +/// Wrapper to provide Debug for SkimMatcherV2 +struct CachedMatcher(SkimMatcherV2); + +impl std::fmt::Debug for CachedMatcher { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("CachedMatcher { .. }") + } +} + +impl std::ops::Deref for CachedMatcher { + type Target = SkimMatcherV2; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl EventEmitter for ChatRegistry {} impl ChatRegistry { @@ -140,35 +165,39 @@ impl ChatRegistry { // Run at the end of the current cycle cx.defer_in(window, |this, _window, cx| { - this.tracking(cx); this.get_rooms(cx); }); Self { rooms: vec![], - trashes: cx.new(|_| BTreeSet::default()), - seens: Arc::new(RwLock::new(HashMap::default())), + trash: cx.new(|_| BTreeSet::default()), + seen: Arc::new(RwLock::new(HashMap::default())), event_map: Arc::new(RwLock::new(HashMap::default())), - tracking: Arc::new(AtomicBool::new(false)), + tracking: Arc::new(AtomicBool::new(true)), + matcher: CachedMatcher(SkimMatcherV2::default()), signal_rx: rx, signal_tx: tx, tasks: smallvec![], + notification_listener: None, + signal_consumer: None, _subscriptions: subscriptions, } } /// Handle nostr notifications fn handle_notifications(&mut self, cx: &mut Context) { + // Cancel previous notification tasks before spawning new ones + self.notification_listener = None; + self.signal_consumer = None; + let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); - let tracking = self.tracking.clone(); - let seens = self.seens.clone(); + let seen = self.seen.clone(); let event_map = self.event_map.clone(); - let trashes = self.trashes.downgrade(); + let trash = self.trash.downgrade(); - let initialized_at = Timestamp::now(); let sub_id1 = SubscriptionId::new(DEVICE_GIFTWRAP); let sub_id2 = SubscriptionId::new(USER_GIFTWRAP); @@ -176,19 +205,22 @@ impl ChatRegistry { let tx = self.signal_tx.clone(); let rx = self.signal_rx.clone(); - self.tasks.push(cx.background_spawn(async move { + self.notification_listener = Some(cx.background_spawn(async move { let mut notifications = client.notifications(); let mut processed_events = HashSet::new(); + const MAX_PROCESSED: usize = 10_000; while let Some(notification) = notifications.next().await { let ClientNotification::Message { message, relay_url } = notification else { - // Skip non-message notifications continue; }; match *message { RelayMessage::Event { event, .. } => { - // De-duplicate events by their ID + // Prune the dedup set before it grows unbounded + if processed_events.len() >= MAX_PROCESSED { + processed_events.clear(); + } if !processed_events.insert(event.id) { continue; } @@ -196,7 +228,6 @@ impl ChatRegistry { // Handle msg relays event to determine when the app is ready to subscribe if event.kind == Kind::InboxRelays { let current_user = signer.get_public_key_async().await?; - // Emit the inbox ready signal if event.pubkey == current_user { tx.send_async(Signal::InboxReady).await?; } @@ -209,33 +240,30 @@ impl ChatRegistry { // Keep track of which relays have seen this event { - let mut seens = seens.write().await; - seens.entry(event.id).or_default().insert(relay_url); + let mut seen = seen.write().unwrap(); + seen.entry(event.id).or_default().insert(relay_url); } // Extract the rumor from the gift wrap event match extract_rumor(&client, &signer, event.as_ref()).await { Ok(rumor) => { - // Map the rumor id to the gift wrap event id for later lookup + let Some(rumor_id) = rumor.id else { + log::error!("Rumor missing id after ensure_id"); + continue; + }; { - let mut event_map = event_map.write().await; - event_map.insert(rumor.id.unwrap(), event.id); + let mut event_map = event_map.write().unwrap(); + event_map.insert(rumor_id, event.id); } - // Check if the rumor has a recipient if rumor.tags.is_empty() { let signal = Signal::error(&event, "Recipient is missing"); tx.send_async(signal).await?; } - // Check if the rumor was created after the chat was initialized (for detecting new messages) - if rumor.created_at >= initialized_at { - let signal = Signal::message(event.id, rumor); - tx.send_async(signal).await?; - } else { - // Mark the chat still processing new messages - tracking.store(true, Ordering::Release); - } + // Emit message for both new and backlog events + let signal = Signal::message(event.id, rumor); + tx.send_async(signal).await?; } Err(e) => { let reason = format!("Failed to extract rumor: {e}"); @@ -256,7 +284,7 @@ impl ChatRegistry { Ok(()) })); - self.tasks.push(cx.spawn(async move |this, cx| { + self.signal_consumer = Some(cx.spawn(async move |this, cx| { while let Ok(message) = rx.recv_async().await { match message { Signal::Message(message) => { @@ -270,13 +298,17 @@ impl ChatRegistry { })?; } Signal::Eose => { + this.update(cx, |this, _cx| { + this.tracking.store(false, Ordering::Release); + })?; + this.update(cx, |this, cx| { this.get_rooms(cx); })?; } - Signal::Error(trash) => { - trashes.update(cx, |this, cx| { - this.insert(trash); + Signal::Error(failed) => { + trash.update(cx, |this, cx| { + this.insert(failed); cx.notify(); })?; } @@ -287,25 +319,6 @@ impl ChatRegistry { })); } - /// Tracking the status of unwrapping gift wrap events. - fn tracking(&mut self, cx: &mut Context) { - let status = self.tracking.clone(); - let tx = self.signal_tx.clone(); - - self.tasks.push(cx.spawn(async move |_, cx| { - let loop_duration = Duration::from_secs(15); - loop { - if status.load(Ordering::Acquire) { - _ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed); - _ = tx.send_async(Signal::Eose).await; - } else { - _ = tx.send_async(Signal::Eose).await; - } - cx.background_executor().timer(loop_duration).await; - } - })); - } - /// Get all necessary metadata from relays for current user pub fn get_metadata(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); @@ -315,55 +328,43 @@ impl ChatRegistry { return; }; - self.tasks.push(cx.background_spawn(async move { + self.tasks.push(cx.spawn(async move |this, cx| { + // Subscribe to metadata from relays let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - // Construct filter for msg relays let msg_relays = Filter::new() .kind(Kind::InboxRelays) .author(public_key) .limit(1); - // Construct filter for contact list let contact_list = Filter::new() .kind(Kind::ContactList) .author(public_key) .limit(1); - // Subscribe - client + _ = client .subscribe(vec![msg_relays, contact_list]) .close_on(opts) - .await?; + .await; - Ok(()) - })); - - let client = nostr.read(cx).client(); - - // Spawn a task to verify user inbox relays after 5 seconds - self.tasks.push(cx.spawn(async move |this, cx| { + // Give relays time to respond cx.background_executor().timer(Duration::from_secs(5)).await; - if !cx - .background_spawn(async move { - // Construct inbox relays filter - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); + // Verify inbox relays were received + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); - // Check the latest inbox relays event in database - client - .database() - .query(filter) - .await - .unwrap_or_default() - .first_owned() - .is_some() - }) + let found = client + .database() + .query(filter) .await - { + .unwrap_or_default() + .first_owned() + .is_some(); + + if !found { this.update(cx, |_this, cx| { cx.emit(ChatEvent::InboxRelayNotFound); })?; @@ -379,58 +380,52 @@ impl ChatRegistry { let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); - let task: Task> = cx.background_spawn(async move { - let public_key = signer.get_public_key_async().await?; - - // Construct inbox relays filter - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); - - // Get the latest inbox relays event in database - let event = client - .database() - .query(filter) - .await? - .first_owned() - .ok_or(anyhow::anyhow!("No inbox relays found"))?; - - // Extract relay list from event - let relays: Vec = nip17::extract_relay_list(&event).collect(); - - // Ensure relay connections - for url in relays.iter() { - client.add_relay(url).and_connect().await?; - } - - // Construct gift wrap event filter - let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); - let id = SubscriptionId::new(format!("{}-msg", public_key.to_hex())); - - // Construct target for subscription - let target: HashMap = relays - .into_iter() - .map(|relay| (relay, filter.clone())) - .collect(); - - client.subscribe(target).with_id(id).await?; - - Ok(()) - }); - self.tasks.push(cx.spawn(async move |this, cx| { + let task: Task> = cx.background_spawn(async move { + let public_key = signer.get_public_key_async().await?; + + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); + + let event = client + .database() + .query(filter) + .await? + .first_owned() + .ok_or(anyhow::anyhow!("No inbox relays found"))?; + + let relays: Vec = nip17::extract_relay_list(&event).collect(); + for url in relays.iter() { + client.add_relay(url).and_connect().await?; + } + + let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); + let id = SubscriptionId::new(USER_GIFTWRAP); + + let target: HashMap = relays + .into_iter() + .map(|relay| (relay, filter.clone())) + .collect(); + + client.subscribe(target).with_id(id).await?; + + Ok(()) + }); + if let Err(e) = task.await { this.update(cx, |_this, cx| { cx.emit(ChatEvent::Error(e.to_string())); })?; } + Ok(()) })); } - /// Refresh the chat registry, fetching messages and contact list from relays. - pub fn refresh(&mut self, cx: &mut Context) { + /// Reload the chat registry, fetching messages and contact list from relays. + pub fn reload(&mut self, cx: &mut Context) { self.reset(cx); self.get_metadata(cx); self.get_rooms(cx); @@ -468,35 +463,38 @@ impl ChatRegistry { /// Count the number of messages seen by a given relay. pub fn count_messages(&self, relay_url: &RelayUrl) -> usize { - self.seens - .read_blocking() + self.seen + .read() + .unwrap() .values() - .filter(|seen| seen.contains(relay_url)) + .filter(|s| s.contains(relay_url)) .count() } /// Count the number of trash messages. pub fn count_trash_messages(&self, cx: &App) -> usize { - self.trashes.read(cx).len() + self.trash.read(cx).len() } /// Get the trash messages entity. - pub fn trashes(&self) -> Entity> { - self.trashes.clone() + pub fn trash(&self) -> Entity> { + self.trash.clone() } /// Get the relays that have seen a given rumor id. pub fn rumor_seen_on(&self, id: &EventId) -> Option> { self.event_map - .read_blocking() + .read() + .unwrap() .get(id) .map(|id| self.seen_on(id)) } /// Get the relays that have seen a given gift wrap id. pub fn seen_on(&self, id: &EventId) -> HashSet { - self.seens - .read_blocking() + self.seen + .read() + .unwrap() .get(id) .cloned() .unwrap_or_default() @@ -505,31 +503,24 @@ impl ChatRegistry { /// Add a new room to the start of list. pub fn add_room(&mut self, room: I, cx: &mut Context) where - I: Into + 'static, + I: Into, { let nostr = NostrRegistry::global(cx); - let Some(public_key) = nostr.read(cx).current_user() else { return; }; - cx.spawn(async move |this, cx| { - let room: Room = room.into().organize(&public_key); + let room: Room = room.into().organize(&public_key); + self.rooms.insert(0, cx.new(|_| room)); - this.update(cx, |this, cx| { - this.rooms.insert(0, cx.new(|_| room)); - cx.emit(ChatEvent::Ping); - cx.notify(); - }) - .ok() - }) - .detach(); + cx.emit(ChatEvent::Ping); + cx.notify(); } /// Emit an open room event. /// /// If the room is new, add it to the registry. - pub fn emit_room(&mut self, room: &Entity, cx: &mut Context) { + pub fn emit_room(&mut self, room: &Entity, window: &mut Window, cx: &mut Context) { // Get the room's ID. let id = room.read(cx).id; @@ -538,14 +529,18 @@ impl ChatRegistry { self.rooms.insert(0, room.to_owned()); } - // Emit the open room event. - cx.emit(ChatEvent::OpenRoom(id)); + // Emit the open room event deferred to avoid re-entrant reads + cx.defer_in(window, move |_this, _window, cx| { + cx.emit(ChatEvent::OpenRoom(id)); + }); } /// Close a room. - pub fn close_room(&mut self, id: u64, cx: &mut Context) { + pub fn close_room(&mut self, id: u64, window: &mut Window, cx: &mut Context) { if self.rooms.iter().any(|r| r.read(cx).id == id) { - cx.emit(ChatEvent::CloseRoom(id)); + cx.defer_in(window, move |_this, _window, cx| { + cx.emit(ChatEvent::CloseRoom(id)); + }); } } @@ -557,8 +552,6 @@ impl ChatRegistry { /// Finding rooms based on a query. pub fn find(&self, query: &str, cx: &App) -> Vec> { - let matcher = SkimMatcherV2::default(); - if let Ok(public_key) = PublicKey::parse(query) { self.rooms .iter() @@ -569,7 +562,7 @@ impl ChatRegistry { self.rooms .iter() .filter(|room| { - matcher + self.matcher .fuzzy_match(room.read(cx).display_name(cx).as_ref(), query) .is_some() }) @@ -581,7 +574,7 @@ impl ChatRegistry { /// Reset the registry. pub fn reset(&mut self, cx: &mut Context) { self.rooms.clear(); - self.trashes.update(cx, |this, cx| { + self.trash.update(cx, |this, cx| { this.clear(); cx.notify(); }); @@ -635,7 +628,9 @@ impl ChatRegistry { })?; } Err(e) => { - log::error!("Failed to load rooms: {}", e); + this.update(cx, |_, cx| { + cx.emit(ChatEvent::Error(e.to_string())); + })?; } }; @@ -653,37 +648,21 @@ impl ChatRegistry { let client = nostr.read(cx).client(); cx.background_spawn(async move { - // Get contacts let contacts = client .database() .contacts_public_keys(public_key) .await .unwrap_or_default(); - // Construct authored filter - let authored_filter = Filter::new() + // Query all cached rumor events (works with both old and new cache formats) + let filter = Filter::new() .kind(Kind::ApplicationSpecificData) - .custom_tag(SingleLetterTag::lowercase(Alphabet::A), public_key); + .custom_tag(SingleLetterTag::lowercase(Alphabet::K), "14"); + let events = client.database().query(filter).await?; - // Get all authored events - let authored = client.database().query(authored_filter).await?; - - // Construct addressed filter - let addressed_filter = Filter::new() - .kind(Kind::ApplicationSpecificData) - .custom_tag(SingleLetterTag::lowercase(Alphabet::P), public_key); - - // Get all addressed events - let addressed = client.database().query(addressed_filter).await?; - - // Merge authored and addressed events - let events = authored.merge(addressed); - - // Collect results let mut rooms: HashSet = HashSet::new(); let mut grouped: HashMap> = HashMap::new(); - // Process each event and group by room hash for raw in events.into_iter() { if let Ok(rumor) = UnsignedEvent::from_json(&raw.content) && rumor.tags.public_keys().peekable().peek().is_some() @@ -775,9 +754,14 @@ async fn extract_rumor( } // Try to unwrap with the available signer - let unwrapped = try_unwrap(signer, gift_wrap).await?; + let unwrapped = try_unwrap_with(signer, gift_wrap).await?; let mut rumor = unwrapped.rumor; + // Verify rumor author matches the seal sender (as per mobile implementation) + if rumor.pubkey != unwrapped.sender { + return Err(anyhow!("Rumor author does not match seal sender")); + } + // Generate event id for the rumor if it doesn't have one rumor.ensure_id(); @@ -789,25 +773,6 @@ async fn extract_rumor( Ok(rumor) } -/// Helper method to try unwrapping with different signers -async fn try_unwrap(signer: &UniversalSigner, gift_wrap: &Event) -> Result { - /* - * // Try with the device signer first - if let Some(signer) = signer.get_encryption_signer().await { - log::info!("trying with encryption key"); - if let Ok(unwrapped) = try_unwrap_with(gift_wrap, &signer).await { - return Ok(unwrapped); - } - } - - // Fallback to the user's signer - let user_signer = signer.get().await; - */ - let unwrapped = try_unwrap_with(signer, gift_wrap).await?; - - Ok(unwrapped) -} - /// Attempts to unwrap a gift wrap event with a given signer. async fn try_unwrap_with( signer: &UniversalSigner, @@ -820,7 +785,7 @@ async fn try_unwrap_with( // Verify the sealed event let seal: Event = Event::from_json(seal)?; - seal.verify_with_ctx(&SECP256K1)?; + seal.verify()?; // Get the rumor event let rumor = signer @@ -837,39 +802,20 @@ async fn try_unwrap_with( /// Stores an unwrapped event in local database with reference to original async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> { - let rumor_id = rumor.id.context("Rumor is missing an event id")?; - let author = rumor.pubkey; - let conversation = conversation_id(rumor); + let room_id = rumor.uniq_id().to_string(); - let mut tags = rumor.tags.clone().to_vec(); + let tags = vec![ + Tag::identifier(id), + Tag::public_key(rumor.pubkey), + Tag::custom("r", [room_id]), + Tag::custom("k", ["14"]), + ]; - // Add a unique identifier - tags.push(Tag::identifier(id)); - - // Add a reference to the rumor's author - tags.push(Tag::custom("a", [author])); - - // Add a conversation id - tags.push(Tag::custom("c", [conversation.to_string()])); - - // Add a reference to the rumor's id - tags.push(Tag::event(rumor_id)); - - // Add references to the rumor's participants - for receiver in rumor.tags.public_keys() { - tags.push(Tag::custom("P", [receiver])); - } - - // Convert rumor to json - let content = rumor.as_json(); - - // Construct the event - let event = EventBuilder::new(Kind::ApplicationSpecificData, content) + let event = EventBuilder::new(Kind::ApplicationSpecificData, rumor.as_json()) .tags(tags) - .finalize_async(&Keys::generate()) + .finalize_async(&*LOCAL_KEYS) .await?; - // Save the event to the database client.database().save_event(&event).await?; Ok(()) @@ -877,10 +823,7 @@ async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Resul /// Retrieves a previously unwrapped event from local database async fn get_rumor(client: &Client, gift_wrap: EventId) -> Result { - let filter = Filter::new() - .kind(Kind::ApplicationSpecificData) - .identifier(gift_wrap) - .limit(1); + let filter = Filter::new().identifier(gift_wrap).limit(1); if let Some(event) = client.database().query(filter).await?.first_owned() { UnsignedEvent::from_json(event.content).map_err(|e| anyhow!(e)) @@ -888,15 +831,3 @@ async fn get_rumor(client: &Client, gift_wrap: EventId) -> Result u64 { - let mut hasher = DefaultHasher::new(); - let mut pubkeys: Vec = rumor.tags.public_keys().collect(); - pubkeys.push(rumor.pubkey); - pubkeys.sort(); - pubkeys.dedup(); - pubkeys.hash(&mut hasher); - - hasher.finish() -} diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index 9971a85..d207c05 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; use std::hash::{Hash, Hasher}; -use std::time::Duration; +use instant::Duration; use anyhow::{Error, anyhow}; use common::EventExt; @@ -363,22 +363,31 @@ impl Room { .exit_policy(ReqExitPolicy::ExitOnEOSE) .timeout(Some(Duration::from_secs(TIMEOUT))); - for public_key in members.into_iter() { - let inbox = Filter::new() - .author(public_key) - .kind(Kind::InboxRelays) - .limit(1); + let tasks: Vec<_> = members + .into_iter() + .map(|public_key| { + let client = client.clone(); + async move { + let inbox = Filter::new() + .author(public_key) + .kind(Kind::InboxRelays) + .limit(1); - let announcement = Filter::new() - .author(public_key) - .kind(Kind::Custom(10044)) - .limit(1); + let announcement = Filter::new() + .author(public_key) + .kind(Kind::Custom(10044)) + .limit(1); - // Subscribe to the target - client - .subscribe(vec![inbox, announcement]) - .close_on(opts) - .await?; + client + .subscribe(vec![inbox, announcement]) + .close_on(opts) + .await + } + }) + .collect(); + + for result in futures::future::join_all(tasks).await { + result?; } Ok(()) @@ -389,12 +398,12 @@ impl Room { pub fn get_messages(&self, cx: &App) -> Task, Error>> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let conversation_id = self.id.to_string(); + let room_id = self.id.to_string(); cx.background_spawn(async move { let filter = Filter::new() .kind(Kind::ApplicationSpecificData) - .custom_tag(SingleLetterTag::lowercase(Alphabet::C), conversation_id); + .custom_tag(SingleLetterTag::lowercase(Alphabet::R), room_id); let messages = client .database() @@ -402,10 +411,6 @@ impl Room { .await? .into_iter() .filter_map(|event| UnsignedEvent::from_json(&event.content).ok()) - .filter(|event| { - // Only process private direct messages and file messages - event.kind == Kind::PrivateDirectMessage || event.kind == Kind::Custom(15) - }) .sorted_by_key(|message| message.created_at) .collect(); @@ -429,14 +434,6 @@ impl Room { // Get current user's public key let sender = nostr.read(cx).current_user()?; - // Get all members, excluding the sender - let members: Vec = self - .members - .iter() - .filter(|public_key| public_key != &&sender) - .map(|member| persons.read(cx).get(member, cx)) - .collect(); - // Construct event's tags let mut tags = vec![]; @@ -450,8 +447,9 @@ impl Room { tags.push(Tag::event(id)) } - // Add all receiver tags - for member in members.into_iter() { + // Add all receiver tags (no intermediate allocation) + for public_key in self.members.iter().filter(|pk| *pk != &sender) { + let member = persons.read(cx).get(public_key, cx); tags.push( Nip01Tag::PublicKey { public_key: member.public_key(), @@ -473,6 +471,30 @@ impl Room { Some(event) } + /// Select the appropriate signer based on signer kind and available keys. + fn select_signer( + signer_kind: &SignerKind, + has_announcement: bool, + encryption_signer: &Option, + user_signer: &UniversalSigner, + ) -> UniversalSigner { + match signer_kind { + SignerKind::Auto => { + if has_announcement { + encryption_signer + .clone() + .unwrap_or_else(|| user_signer.clone()) + } else { + user_signer.clone() + } + } + SignerKind::Encryption => encryption_signer + .clone() + .expect("encryption signer must be set"), + SignerKind::User => user_signer.clone(), + } + } + /// Send rumor event to all members's messaging relays pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option>> { let config = self.config.clone(); @@ -525,23 +547,12 @@ impl Room { } // Determine the signer to use - let signer = match signer_kind { - SignerKind::Auto => { - if announcement.is_some() - && let Some(encryption_signer) = encryption_signer.clone() - { - // Safe to unwrap due to earlier checks - encryption_signer - } else { - user_signer.clone() - } - } - SignerKind::Encryption => { - // Safe to unwrap due to earlier checks - encryption_signer.as_ref().unwrap().clone() - } - SignerKind::User => user_signer.clone(), - }; + let signer = Self::select_signer( + signer_kind, + announcement.is_some(), + &encryption_signer, + &user_signer, + ); // Send the gift wrap event and collect the report match send_gift_wrap(&client, &signer, &member, &rumor, signer_kind).await { @@ -561,23 +572,12 @@ impl Room { let public_key = sender.public_key(); // Determine the signer to use - let signer = match signer_kind { - SignerKind::Auto => { - if sender.announcement().is_some() - && let Some(encryption_signer) = encryption_signer.clone() - { - // Safe to unwrap due to earlier checks - encryption_signer - } else { - user_signer.clone() - } - } - SignerKind::Encryption => { - // Safe to unwrap due to earlier checks - encryption_signer.as_ref().unwrap().clone() - } - SignerKind::User => user_signer.clone(), - }; + let signer = Self::select_signer( + signer_kind, + sender.announcement().is_some(), + &encryption_signer, + &user_signer, + ); match send_gift_wrap(&client, &signer, &sender, &rumor, signer_kind).await { Ok(report) => reports.push(report), diff --git a/crates/chat_ui/Cargo.toml b/crates/chat_ui/Cargo.toml index 0cc8ed3..fd2c1f1 100644 --- a/crates/chat_ui/Cargo.toml +++ b/crates/chat_ui/Cargo.toml @@ -19,11 +19,10 @@ anyhow.workspace = true itertools.workspace = true smallvec.workspace = true flume.workspace = true +futures.workspace = true log.workspace = true serde.workspace = true linkify = "0.10.0" pulldown-cmark = "0.13.1" -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -smol.workspace = true diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 9fc1164..27d5380 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -1,12 +1,11 @@ -use std::collections::{BTreeMap, BTreeSet, HashSet}; -use std::sync::Arc; -#[cfg(target_arch = "wasm32")] -use std::sync::RwLock; +use std::collections::{BTreeMap, HashSet}; +use std::sync::{Arc, RwLock}; pub use actions::*; use anyhow::{Context as AnyhowContext, Error}; use chat::{ChatRegistry, Message, Room, RoomEvent, SendReport, SendStatus}; use common::{TimestampExt, coop_cache}; +use futures::lock::Mutex; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, @@ -20,8 +19,6 @@ use nostr_sdk::prelude::*; use person::{Person, PersonRegistry}; use settings::{AppSettings, SignerKind}; use smallvec::{SmallVec, smallvec}; -#[cfg(not(target_arch = "wasm32"))] -use smol::lock::RwLock; use state::{NostrRegistry, upload}; use theme::ActiveTheme; use ui::avatar::Avatar; @@ -56,14 +53,14 @@ pub struct ChatPanel { /// Message list state list_state: ListState, - /// All messages - messages: BTreeSet, + /// All messages (sorted by created_at) + messages: Vec, /// Mapping message ids to their rendered texts rendered_texts_by_id: BTreeMap, /// Mapping message (rumor event) ids to their reports - reports_by_id: Entity>>, + reports_by_id: Arc>>>, /// Chat input state input: Entity, @@ -75,7 +72,7 @@ pub struct ChatPanel { subject_bar: Entity, /// Sent message ids - sent_ids: Arc>>, + sent_ids: Arc>>, /// Replies to replies_to: Entity>, @@ -98,10 +95,10 @@ impl ChatPanel { // Define attachments and replies_to entities let attachments = cx.new(|_| vec![]); let replies_to = cx.new(|_| HashSet::new()); - let reports_by_id = cx.new(|_| BTreeMap::new()); + let reports_by_id = Arc::new(RwLock::new(BTreeMap::new())); // Define list of messages - let messages = BTreeSet::default(); + let messages = Vec::new(); let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.)); // Get room id and name @@ -172,7 +169,7 @@ impl ChatPanel { attachments, rendered_texts_by_id: BTreeMap::new(), reports_by_id, - sent_ids: Arc::new(RwLock::new(Vec::new())), + sent_ids: Arc::new(Mutex::new(Vec::new())), uploading: false, subscriptions, tasks: vec![], @@ -192,7 +189,7 @@ impl ChatPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let sent_ids = self.sent_ids.clone(); - let reports = self.reports_by_id.downgrade(); + let reports = self.reports_by_id.clone(); let (tx, rx) = flume::bounded::>(256); @@ -207,7 +204,7 @@ impl ChatPanel { message, } = *message { - let sent_ids = sent_ids.read().await; + let sent_ids = sent_ids.lock().await; if sent_ids.contains(&event_id) { let status = if status { @@ -223,10 +220,11 @@ impl ChatPanel { Ok(()) })); - self.tasks.push(cx.spawn(async move |_this, cx| { + self.tasks.push(cx.spawn(async move |this, cx| { while let Ok(status) = rx.recv_async().await { - reports.update(cx, |this, cx| { - for reports in this.values_mut() { + { + let mut map = reports.write().unwrap(); + for reports in map.values_mut() { for report in reports.iter_mut() { let Some(output) = report.output.as_mut() else { continue; @@ -243,10 +241,10 @@ impl ChatPanel { } } } - cx.notify(); } } - })?; + } + this.update(cx, |_, cx| cx.notify()).ok(); } Ok(()) })); @@ -264,7 +262,11 @@ impl ChatPanel { this.insert_message(message, false, cx); } RoomEvent::Reload => { - this.get_messages(window, cx); + // Defer to avoid re-entrant read on Room while + // emit_refresh holds a write lock (via refresh_rooms). + cx.defer_in(window, |this, window, cx| { + this.get_messages(window, cx); + }); } }; }, @@ -345,69 +347,47 @@ impl ChatPanel { return; } - // Get room entity let room = self.room.clone(); - - // Get content and replies let replies: Vec = self.replies_to.read(cx).iter().copied().collect(); let content = value.to_string(); - - self.tasks.push(cx.spawn_in(window, async move |this, cx| { - let room = room.upgrade().context("Room is not available")?; - - this.update_in(cx, |this, window, cx| { - match room.read(cx).rumor(content, replies, cx) { - Some(rumor) => { - this.insert_message(&rumor, true, cx); - this.send_and_wait(rumor, window, cx); - this.clear(window, cx); - } - None => { - window.push_notification("Failed to create message", cx); - } - } - })?; - - Ok(()) - })); - } - - /// Send message in the background and wait for the response - fn send_and_wait(&mut self, rumor: UnsignedEvent, window: &mut Window, cx: &mut Context) { let sent_ids = self.sent_ids.clone(); - // This can't fail, because we already ensured that the ID is set - let id = rumor.id.unwrap(); + // Upgrade room and create rumor + send task in a single read lock + let Some(room_entity) = room.upgrade() else { + return; + }; + let (rumor, send_task) = match room_entity.read_with(cx, |room, cx| { + let rumor = room.rumor(content.clone(), replies, cx)?; + let send_task = room.send(rumor.clone(), cx)?; + Some((rumor, send_task)) + }) { + Some(pair) => pair, + None => { + window.push_notification("Failed to create message", cx); + return; + } + }; - // Add empty reports + let id = rumor.id.expect("rumor must have an id"); + + // Insert optimistic message and clear input + self.insert_message(&rumor, true, cx); self.insert_reports(id, vec![], cx); + self.clear(window, cx); - // Upgrade room reference - let Some(room) = self.room.upgrade() else { - return; - }; - - // Get the send message task - let Some(task) = room.read(cx).send(rumor, cx) else { - window.push_notification("Failed to send message", cx); - return; - }; - + // Spawn a single task to await the send and update reports self.tasks.push(cx.spawn_in(window, async move |this, cx| { - // Send and get reports - let outputs = task.await; + let outputs = send_task.await; - // Add sent IDs to the list - let mut sent_ids = sent_ids.write().await; + let mut sent_ids = sent_ids.lock().await; sent_ids.extend(outputs.iter().filter_map(|output| output.gift_wrap_id)); - // Update the state this.update(cx, |this, cx| { this.insert_reports(id, outputs, cx); })?; Ok(()) - })) + })); } /// Clear the input field, attachments, and replies @@ -429,10 +409,13 @@ impl ChatPanel { /// Insert reports fn insert_reports(&mut self, id: EventId, reports: Vec, cx: &mut Context) { - self.reports_by_id.update(cx, |this, cx| { - this.entry(id).or_default().extend(reports); - cx.notify(); - }); + self.reports_by_id + .write() + .unwrap() + .entry(id) + .or_default() + .extend(reports); + cx.notify(); } /// Insert a message into the chat panel @@ -441,9 +424,10 @@ impl ChatPanel { E: Into, { let old_len = self.messages.len(); + let msg: Message = m.into(); - // Extend the messages list with the new events - if self.messages.insert(m.into()) { + if let Err(pos) = self.messages.binary_search(&msg) { + self.messages.insert(pos, msg); self.list_state.splice(old_len..old_len, 1); if scroll { @@ -466,13 +450,12 @@ impl ChatPanel { } /// Check if a message has any reports - fn has_reports(&self, id: &EventId, cx: &App) -> bool { - self.reports_by_id.read(cx).get(id).is_some() + fn has_reports(&self, id: &EventId, _cx: &App) -> bool { + self.reports_by_id.read().unwrap().get(id).is_some() } - /// Get all sent reports for a message by its ID - fn sent_reports(&self, id: &EventId, cx: &App) -> Option> { - self.reports_by_id.read(cx).get(id).cloned() + fn sent_reports(&self, id: &EventId, _cx: &App) -> Option> { + self.reports_by_id.read().unwrap().get(id).cloned() } /// Get a message by its ID @@ -796,10 +779,8 @@ impl ChatPanel { return true; } - let mut iter = self.messages.iter(); - - if let Some(previous) = iter.nth(ix - 1) - && let Some(current) = iter.next() + if let Some(previous) = self.messages.get(ix - 1) + && let Some(current) = self.messages.get(ix) { if current.author != previous.author { return true; @@ -822,7 +803,7 @@ impl ChatPanel { window: &mut Window, cx: &mut Context, ) -> AnyElement { - if let Some(message) = self.messages.iter().nth(ix) { + if let Some(message) = self.messages.get(ix) { let persons = PersonRegistry::global(cx); let show_author = self.is_group_start(ix); let text = self @@ -1496,12 +1477,7 @@ impl Render for ChatPanel { .gap_2() .border_b_1() .border_color(cx.theme().border) - .child( - Input::new(&self.subject_input) - .text_sm() - .small() - .bordered(false), - ) + .child(Input::new(&self.subject_input).text_sm().small()) .child( Button::new("change") .icon(IconName::CheckCircle) diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index bdad290..4c8c198 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -7,6 +7,7 @@ publish.workspace = true [dependencies] gpui.workspace = true nostr.workspace = true +instant.workspace = true nostr-sdk.workspace = true itertools.workspace = true diff --git a/crates/common/src/debounced_delay.rs b/crates/common/src/debounced_delay.rs index 995e983..17e1c22 100644 --- a/crates/common/src/debounced_delay.rs +++ b/crates/common/src/debounced_delay.rs @@ -1,5 +1,5 @@ use std::marker::PhantomData; -use std::time::Duration; +use instant::Duration; use futures::channel::oneshot; use futures::FutureExt; diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml index 20131b2..fa2b0c3 100644 --- a/crates/device/Cargo.toml +++ b/crates/device/Cargo.toml @@ -14,6 +14,7 @@ settings = { path = "../settings" } gpui.workspace = true nostr-sdk.workspace = true +instant.workspace = true anyhow.workspace = true smallvec.workspace = true log.workspace = true diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 5c06f1c..32c2ea1 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::Duration; +use instant::Duration; use anyhow::{Context as AnyhowContext, Error, anyhow}; use gpui::{ @@ -92,7 +92,6 @@ impl DeviceRegistry { fn new(window: &mut Window, cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); let settings = AppSettings::global(cx); - let nip4e_enabled = settings.read(cx).is_nip4e_enabled(cx); let signer = cx.new(|_| None); let mut subscriptions = smallvec![]; @@ -109,7 +108,7 @@ impl DeviceRegistry { subscriptions.push( // Observe the user signer cx.subscribe(&nostr, move |this, _nostr, event, cx| { - if event.signer_changed() && nip4e_enabled { + if event.signer_changed() && settings.read(cx).is_nip4e_enabled(cx) { this.get_announcement(cx); } }), @@ -226,6 +225,11 @@ impl DeviceRegistry { let keys = get_keys(&client, &signer).await?; let content = keys.secret_key().to_bech32()?; + if cfg!(target_arch = "wasm32") { + return Err(anyhow!("Not supported")); + } + + #[cfg(not(target_arch = "wasm32"))] smol::fs::write(path, &content).await?; Ok(()) @@ -258,28 +262,20 @@ impl DeviceRegistry { })); let announcement_existed = self.announcement_existed.clone(); - let executor = cx.background_executor().clone(); self.tasks.push(cx.spawn(async move |this, cx| { - if !cx - .background_spawn(async move { - // Wait for 5 seconds - executor.timer(Duration::from_secs(5)).await; + // Wait for 5 seconds + cx.background_executor().timer(Duration::from_secs(5)).await; - // Then check if the msg relays have been found - if !announcement_existed.load(Ordering::Acquire) { - return true; - } - - false - }) - .await - { - this.update(cx, |_this, cx| { - cx.emit(DeviceEvent::NotSet); - })?; + // Then check if the msg relays have been found + if announcement_existed.load(Ordering::Acquire) { + return Ok(()); } + this.update(cx, |_this, cx| { + cx.emit(DeviceEvent::NotSet); + })?; + Ok(()) })); } @@ -404,11 +400,10 @@ impl DeviceRegistry { let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); - let Ok(app_keys) = get_or_init_app_keys(cx) else { - return; - }; + let app_keys_task = get_or_init_app_keys(cx); let task: Task, Error>> = cx.background_spawn(async move { + let app_keys = app_keys_task.await?; let app_pubkey = app_keys.public_key(); let public_key = signer.get_public_key_async().await?; @@ -489,11 +484,10 @@ impl DeviceRegistry { /// Parse the approval event to get encryption key then set it fn extract_encryption(&mut self, event: Event, cx: &mut Context) { - let Ok(app_keys) = get_or_init_app_keys(cx) else { - return; - }; + let app_keys_task = get_or_init_app_keys(cx); let task: Task> = cx.background_spawn(async move { + let app_keys = app_keys_task.await?; let master = event .tags .iter() @@ -573,7 +567,7 @@ impl DeviceRegistry { Ok(()) }); - cx.spawn_in(window, async move |_this, cx| { + self.tasks.push(cx.spawn_in(window, async move |_this, cx| { match task.await { Ok(_) => { cx.update(|window, cx| { @@ -591,8 +585,9 @@ impl DeviceRegistry { .ok(); } }; - }) - .detach(); + + Ok(()) + })); } /// Handle encryption request @@ -715,33 +710,34 @@ impl DeviceRegistry { struct DeviceNotification; -/// Get or create new app keys -fn get_or_init_app_keys(cx: &App) -> Result { +/// Get or create new app keys (async, returns a task) +fn get_or_init_app_keys(cx: &App) -> Task> { let read = cx.read_credentials(CLIENT_NAME); - let stored_keys: Option = cx.foreground_executor().block_on(async move { - if let Ok(Some((_, secret))) = read.await { - SecretKey::from_slice(&secret).map(Keys::new).ok() - } else { - None - } - }); - if let Some(keys) = stored_keys { - Ok(keys) - } else { + cx.spawn(async move |cx| { + if let Ok(Some((_, secret))) = read.await + && let Ok(keys) = SecretKey::from_slice(&secret).map(Keys::new) + { + return Ok(keys); + } + + // No stored keys found or invalid — generate new ones let keys = Keys::generate(); let user = keys.public_key().to_hex(); let secret = keys.secret_key().to_secret_bytes(); - let write = cx.write_credentials(CLIENT_NAME, &user, &secret); - cx.foreground_executor().block_on(async move { - if let Err(e) = write.await { - log::error!("Keyring not available or panic: {e}") - } + cx.update(|cx| { + let write = cx.write_credentials(CLIENT_NAME, &user, &secret); + cx.background_spawn(async move { + if let Err(e) = write.await { + log::error!("Keyring not available or panic: {e}") + } + }) + .detach(); }); Ok(keys) - } + }) } /// Encrypt and store device keys in the local database. diff --git a/crates/person/Cargo.toml b/crates/person/Cargo.toml index 9ba05ed..ea15a5b 100644 --- a/crates/person/Cargo.toml +++ b/crates/person/Cargo.toml @@ -10,6 +10,7 @@ state = { path = "../state" } gpui.workspace = true nostr-sdk.workspace = true +instant.workspace = true anyhow.workspace = true smallvec.workspace = true flume.workspace = true diff --git a/crates/person/src/lib.rs b/crates/person/src/lib.rs index b8423f1..a0a46e2 100644 --- a/crates/person/src/lib.rs +++ b/crates/person/src/lib.rs @@ -1,7 +1,6 @@ -use std::cell::RefCell; use std::collections::{HashMap, HashSet}; -use std::rc::Rc; -use std::time::Duration; +use std::sync::RwLock; +use instant::Duration; use anyhow::{Error, anyhow}; use common::EventExt; @@ -24,9 +23,9 @@ impl Global for GlobalPersonRegistry {} #[derive(Debug, Clone)] enum Dispatch { - Person(Box), - Announcement(Box), - Relays(Box), + Person(Person), + Announcement(Event), + Relays(Event), } /// Person Registry @@ -36,7 +35,7 @@ pub struct PersonRegistry { persons: HashMap>, /// Set of public keys that have been seen - seens: Rc>>, + seen: RwLock>, /// Sender for requesting metadata sender: flume::Sender, @@ -63,53 +62,38 @@ impl PersonRegistry { // Channel for communication between nostr and gpui let (tx, rx) = flume::bounded::(100); - let (mta_tx, mta_rx) = flume::unbounded::(); + let (metadata_tx, metadata_rx) = flume::unbounded::(); let mut tasks = smallvec![]; - tasks.push( - // Handle nostr notifications - cx.background_spawn({ - let client = client.clone(); + let client2 = client.clone(); + tasks.push(cx.background_spawn(async move { + Self::handle_notifications(&client2, &tx).await; + })); - async move { - Self::handle_notifications(&client, &tx).await; - } - }), - ); + let client3 = client.clone(); + tasks.push(cx.background_spawn(async move { + Self::handle_requests(&client3, &metadata_rx).await; + })); - tasks.push( - // Handle metadata requests - cx.background_spawn({ - let client = client.clone(); - - async move { - Self::handle_requests(&client, &mta_rx).await; - } - }), - ); - - tasks.push( - // Update GPUI state - cx.spawn(async move |this, cx| { - while let Ok(event) = rx.recv_async().await { - this.update(cx, |this, cx| { - match event { - Dispatch::Person(person) => { - this.insert(*person, cx); - } - Dispatch::Announcement(event) => { - this.set_announcement(&event, cx); - } - Dispatch::Relays(event) => { - this.set_messaging_relays(&event, cx); - } - }; - }) - .ok(); - } - }), - ); + tasks.push(cx.spawn(async move |this, cx| { + while let Ok(event) = rx.recv_async().await { + this.update(cx, |this, cx| { + match event { + Dispatch::Person(person) => { + this.insert(person, cx); + } + Dispatch::Announcement(event) => { + this.set_announcement(&event, cx); + } + Dispatch::Relays(event) => { + this.set_messaging_relays(&event, cx); + } + }; + }) + .ok(); + } + })); // Load all user profiles from the database cx.defer_in(window, |this, _window, cx| { @@ -118,8 +102,8 @@ impl PersonRegistry { Self { persons: HashMap::new(), - seens: Rc::new(RefCell::new(HashSet::new())), - sender: mta_tx, + seen: RwLock::new(HashSet::new()), + sender: metadata_tx, tasks, } } @@ -145,24 +129,25 @@ impl PersonRegistry { Kind::Metadata => { let metadata = Metadata::from_json(&event.content).unwrap_or_default(); let person = Person::new(event.pubkey, metadata); - let val = Box::new(person); - // Send - tx.send_async(Dispatch::Person(val)).await.ok(); + if tx.send_async(Dispatch::Person(person)).await.is_err() { + log::warn!("PersonRegistry channel closed, dropping metadata event"); + } } Kind::ContactList => { let public_keys = event.extract_public_keys(); - // Get metadata for all public keys - get_metadata(client, public_keys).await.ok(); + if let Err(e) = get_metadata(client, public_keys).await { + log::warn!("Failed to get metadata for contact list: {e}"); + } } Kind::InboxRelays => { - let val = Box::new(event.into_owned()); - // Send - tx.send_async(Dispatch::Relays(val)).await.ok(); + tx.send_async(Dispatch::Relays(event.into_owned())) + .await + .ok(); } Kind::Custom(10044) => { - let val = Box::new(event.into_owned()); - // Send - tx.send_async(Dispatch::Announcement(val)).await.ok(); + tx.send_async(Dispatch::Announcement(event.into_owned())) + .await + .ok(); } _ => {} } @@ -182,13 +167,17 @@ impl PersonRegistry { Ok(Some(public_key)) => { batch.insert(public_key); // Process the batch if it's full - if batch.len() >= 20 { - get_metadata(client, std::mem::take(&mut batch)).await.ok(); + if batch.len() >= 20 + && let Err(e) = get_metadata(client, std::mem::take(&mut batch)).await + { + log::warn!("Failed to get metadata batch: {e}"); } } _ => { - if !batch.is_empty() { - get_metadata(client, std::mem::take(&mut batch)).await.ok(); + if !batch.is_empty() + && let Err(e) = get_metadata(client, std::mem::take(&mut batch)).await + { + log::warn!("Failed to get metadata batch: {e}"); } } } @@ -217,7 +206,7 @@ impl PersonRegistry { self.tasks.push(cx.spawn(async move |this, cx| { if let Ok(persons) = task.await { this.update(cx, |this, cx| { - this.bulk_inserts(persons, cx); + this.bulk_insert(persons, cx); }) .ok(); } @@ -256,7 +245,7 @@ impl PersonRegistry { } /// Insert batch of persons - fn bulk_inserts(&mut self, persons: Vec, cx: &mut Context) { + fn bulk_insert(&mut self, persons: Vec, cx: &mut Context) { for person in persons.into_iter() { let public_key = person.public_key(); self.persons @@ -290,15 +279,14 @@ impl PersonRegistry { } let public_key = *public_key; - let mut seen = self.seens.borrow_mut(); - if seen.insert(public_key) { + if self.seen.write().unwrap().insert(public_key) { let sender = self.sender.clone(); // Spawn background task to request metadata cx.background_spawn(async move { if let Err(e) = sender.send_async(public_key).await { - log::warn!("Failed to send public key for metadata request: {}", e); + log::warn!("Failed to send public key for metadata request: {e}"); } }) .detach(); diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 89e6080..150ae19 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -1,4 +1,3 @@ -use std::fmt::Display; use std::rc::Rc; use anyhow::{Error, anyhow}; @@ -42,28 +41,10 @@ setting_accessors! { pub hide_avatar: bool, pub screening: bool, pub nip4e: bool, - pub auth_mode: AuthMode, pub trusted_relays: Vec, pub file_server: Url, } -/// Authentication mode -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub enum AuthMode { - #[default] - Auto, - Manual, -} - -impl Display for AuthMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AuthMode::Auto => write!(f, "Auto"), - AuthMode::Manual => write!(f, "Ask every time"), - } - } -} - /// Signer kind #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum SignerKind { @@ -141,9 +122,6 @@ pub struct Settings { /// Enable decoupling encryption key pub nip4e: bool, - /// Authentication mode - pub auth_mode: AuthMode, - /// Trusted relays; Coop will automatically authenticate with these relays pub trusted_relays: Vec, @@ -159,7 +137,6 @@ impl Default for Settings { hide_avatar: false, screening: true, nip4e: false, - auth_mode: AuthMode::default(), trusted_relays: vec![], file_server: Url::parse("https://blossom.band/").unwrap(), } diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index 15e6006..273dbe2 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -14,6 +14,7 @@ nostr-blossom.workspace = true nostr-connect.workspace = true gpui.workspace = true +instant.workspace = true flume.workspace = true futures.workspace = true log.workspace = true diff --git a/crates/state/src/constants.rs b/crates/state/src/constants.rs index c0473cf..a84bea9 100644 --- a/crates/state/src/constants.rs +++ b/crates/state/src/constants.rs @@ -39,14 +39,15 @@ pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nip46.com"; pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"]; /// Default search relays -pub const INDEXER_RELAYS: [&str; 1] = ["wss://indexer.coracle.social"]; +pub const INDEXER_RELAYS: [&str; 2] = ["wss://indexer.coracle.social", "wss://user.kindpag.es"]; /// Default search relays pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"]; /// Default bootstrap relays -pub const BOOTSTRAP_RELAYS: [&str; 3] = [ +pub const BOOTSTRAP_RELAYS: [&str; 4] = [ "wss://relay.ditto.pub", "wss://relay.primal.net", - "wss://user.kindpag.es", + "wss://relay.nostr.net", + "wss://profiles.nostr1.com", ]; diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 811d3f1..9186a89 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; -use std::time::Duration; use anyhow::{Error, anyhow}; use common::config_dir; use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, Window}; +use instant::Duration; use nostr_connect::prelude::*; use nostr_gossip_memory::prelude::*; #[cfg(not(target_arch = "wasm32"))] @@ -130,7 +130,12 @@ impl NostrRegistry { // Connect to bootstrap relays after the window is ready cx.defer_in(window, |this, _window, cx| { this.connect_bootstrap_relays(cx); - this.get_user_credential(cx); + + if cfg!(target_arch = "wasm32") { + cx.emit(StateEvent::NoSigner); + } else { + this.get_user_credential(cx); + } }); Self { @@ -164,7 +169,7 @@ impl NostrRegistry { ::Error: std::error::Error + Send + Sync + 'static, ::Error: std::error::Error + Send + Sync + 'static, { - cx.spawn(async move |this, cx| { + let task = cx.spawn(async move |this, cx| { match new_signer.get_public_key_async().await { Ok(public_key) => { this.update(cx, |this, cx| { @@ -181,9 +186,9 @@ impl NostrRegistry { } }; - Ok::<(), anyhow::Error>(()) - }) - .detach(); + Ok(()) + }); + self.tasks.push(task); } /// Connect to the bootstrapping relays @@ -268,6 +273,10 @@ impl NostrRegistry { /// Get the master key that used for Nostr Connect pub fn get_master_key(&self, cx: &App) -> Task { + if cfg!(target_arch = "wasm32") { + return cx.background_spawn(async move { Keys::generate() }); + } + let task = cx.read_credentials(MASTER_KEYRING); cx.spawn(async move |cx| { diff --git a/crates/state/src/signer.rs b/crates/state/src/signer.rs index c3e78fd..cf9a2bf 100644 --- a/crates/state/src/signer.rs +++ b/crates/state/src/signer.rs @@ -1,5 +1,7 @@ use std::error::Error; use std::fmt; +use std::future::Future; +use std::pin::Pin; use std::sync::{Arc, RwLock}; use nostr_connect::client::AuthUrlHandler; @@ -60,21 +62,23 @@ impl UniversalSigner { } trait InnerSigner: fmt::Debug + Send + Sync + 'static { - fn get_public_key_async(&self) -> BoxedFuture<'_, Result>; + fn get_public_key_async( + &self, + ) -> Pin> + Send + '_>>; fn sign_event_async( &self, unsigned: UnsignedEvent, - ) -> BoxedFuture<'_, Result>; + ) -> Pin> + Send + '_>>; fn nip44_encrypt_async<'a>( &'a self, public_key: &'a PublicKey, content: &'a str, - ) -> BoxedFuture<'a, Result>; + ) -> Pin> + Send + 'a>>; fn nip44_decrypt_async<'a>( &'a self, public_key: &'a PublicKey, payload: &'a str, - ) -> BoxedFuture<'a, Result>; + ) -> Pin> + Send + 'a>>; } #[derive(Debug)] @@ -87,7 +91,9 @@ where ::Error: Error + Send + Sync + 'static, ::Error: Error + Send + Sync + 'static, { - fn get_public_key_async(&self) -> BoxedFuture<'_, Result> { + fn get_public_key_async( + &self, + ) -> Pin> + Send + '_>> { Box::pin(async move { AsyncGetPublicKey::get_public_key_async(&self.0) .await @@ -98,7 +104,7 @@ where fn sign_event_async( &self, unsigned: UnsignedEvent, - ) -> BoxedFuture<'_, Result> { + ) -> Pin> + Send + '_>> { Box::pin(async move { AsyncSignEvent::sign_event_async(&self.0, unsigned) .await @@ -110,7 +116,7 @@ where &'a self, public_key: &'a PublicKey, content: &'a str, - ) -> BoxedFuture<'a, Result> { + ) -> Pin> + Send + 'a>> { Box::pin(async move { AsyncNip44::nip44_encrypt_async(&self.0, public_key, content) .await @@ -122,7 +128,7 @@ where &'a self, public_key: &'a PublicKey, payload: &'a str, - ) -> BoxedFuture<'a, Result> { + ) -> Pin> + Send + 'a>> { Box::pin(async move { AsyncNip44::nip44_decrypt_async(&self.0, public_key, payload) .await @@ -142,7 +148,9 @@ impl UniversalSigner { impl AsyncGetPublicKey for UniversalSigner { type Error = UniversalSignerError; - fn get_public_key_async(&self) -> BoxedFuture<'_, Result> { + fn get_public_key_async( + &self, + ) -> Pin> + Send + '_>> { let inner = self.inner.read().expect("RwLock poisoned").clone(); Box::pin(async move { inner.get_public_key_async().await }) } @@ -154,7 +162,7 @@ impl AsyncSignEvent for UniversalSigner { fn sign_event_async( &self, unsigned: UnsignedEvent, - ) -> BoxedFuture<'_, Result> { + ) -> Pin> + Send + '_>> { let inner = self.inner.read().expect("RwLock poisoned").clone(); Box::pin(async move { inner.sign_event_async(unsigned).await }) } @@ -167,7 +175,7 @@ impl AsyncNip44 for UniversalSigner { &'a self, public_key: &'a PublicKey, content: &'a str, - ) -> BoxedFuture<'a, Result> { + ) -> Pin> + Send + 'a>> { let inner = self.inner.read().expect("RwLock poisoned").clone(); Box::pin(async move { inner.nip44_encrypt_async(public_key, content).await }) } @@ -176,7 +184,7 @@ impl AsyncNip44 for UniversalSigner { &'a self, public_key: &'a PublicKey, payload: &'a str, - ) -> BoxedFuture<'a, Result> { + ) -> Pin> + Send + 'a>> { let inner = self.inner.read().expect("RwLock poisoned").clone(); Box::pin(async move { inner.nip44_decrypt_async(public_key, payload).await }) } @@ -186,8 +194,10 @@ impl AsyncNip44 for UniversalSigner { pub struct CoopAuthUrlHandler; impl AuthUrlHandler for CoopAuthUrlHandler { - #[allow(mismatched_lifetime_syntaxes)] - fn on_auth_url(&self, auth_url: Url) -> BoxedFuture> { + fn on_auth_url( + &self, + auth_url: Url, + ) -> Pin> + Send + '_>> { Box::pin(async move { webbrowser::open(auth_url.as_str()).unwrap(); Ok(()) diff --git a/crates/theme/src/colors.rs b/crates/theme/src/colors.rs index 46915dd..fc6bd65 100644 --- a/crates/theme/src/colors.rs +++ b/crates/theme/src/colors.rs @@ -112,7 +112,7 @@ impl ThemeColors { elevated_surface_background: neutral().light().step_3(), panel_background: neutral().light().step_1(), overlay: neutral().light_alpha().step_3(), - title_bar: neutral().light().step_3(), + title_bar: neutral().light().step_2(), title_bar_inactive: neutral().light().step_1(), window_border: hsl(240.0, 5.9, 78.0), @@ -198,7 +198,7 @@ impl ThemeColors { elevated_surface_background: neutral().dark().step_3(), panel_background: neutral().dark().step_1(), overlay: neutral().dark_alpha().step_3(), - title_bar: neutral().dark().step_3(), + title_bar: neutral().dark().step_2(), title_bar_inactive: neutral().dark().step_1(), window_border: hsl(240.0, 3.7, 28.0), diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index e11adcd..830901a 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -34,7 +34,7 @@ pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0); pub const TITLEBAR_HEIGHT: Pixels = px(36.0); /// Defines workspace tabbar height -pub const TABBAR_HEIGHT: Pixels = px(28.0); +pub const TABBAR_HEIGHT: Pixels = px(44.0); /// Defines default sidebar width pub const SIDEBAR_WIDTH: Pixels = px(240.); diff --git a/crates/theme/src/platform_kind.rs b/crates/theme/src/platform_kind.rs index 95db95b..afc545a 100644 --- a/crates/theme/src/platform_kind.rs +++ b/crates/theme/src/platform_kind.rs @@ -3,6 +3,7 @@ pub enum PlatformKind { Mac, Linux, Windows, + Web, } impl PlatformKind { @@ -11,22 +12,21 @@ impl PlatformKind { Self::Linux } else if cfg!(target_os = "windows") { Self::Windows - } else { + } else if cfg!(target_os = "macos") { Self::Mac + } else { + Self::Web } } - #[allow(dead_code)] pub fn is_linux(&self) -> bool { matches!(self, Self::Linux) } - #[allow(dead_code)] pub fn is_windows(&self) -> bool { matches!(self, Self::Windows) } - #[allow(dead_code)] pub fn is_mac(&self) -> bool { matches!(self, Self::Mac) } diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 0ee2e24..ce16ba9 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -9,6 +9,7 @@ common = { path = "../common" } theme = { path = "../theme" } gpui.workspace = true +instant.workspace = true serde.workspace = true smallvec.workspace = true anyhow.workspace = true @@ -21,7 +22,7 @@ regex = "1" lsp-types = "0.97.0" ropey = { version = "=2.0.0-beta.1", features = ["metric_lines_lf", "metric_utf16"] } sum_tree = { git = "https://github.com/zed-industries/zed" } -tree-sitter = "0.26" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] smol.workspace = true +tree-sitter = "0.26" diff --git a/crates/ui/src/checkbox.rs b/crates/ui/src/checkbox.rs index 0434032..1ce8e19 100644 --- a/crates/ui/src/checkbox.rs +++ b/crates/ui/src/checkbox.rs @@ -1,5 +1,5 @@ use std::rc::Rc; -use std::time::Duration; +use instant::Duration; use gpui::prelude::FluentBuilder as _; use gpui::{ diff --git a/crates/ui/src/dock/dock.rs b/crates/ui/src/dock/dock.rs index df302bd..4fb4321 100644 --- a/crates/ui/src/dock/dock.rs +++ b/crates/ui/src/dock/dock.rs @@ -214,6 +214,8 @@ impl Dock { pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context) { self.open = open; let item = self.panel.clone(); + // Use defer_in (not window.defer) so the callback is cancelled + // if this Dock entity is dropped before the deferred frame runs. cx.defer_in(window, move |_, window, cx| { item.set_collapsed(!open, window, cx); }); diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs index ad8f365..b08b9b0 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock/mod.rs @@ -2,11 +2,10 @@ use std::sync::Arc; use gpui::prelude::FluentBuilder; use gpui::{ - AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations, Edges, Entity, - EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _, - Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div, px, + AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges, Entity, EntityId, + EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, + Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div, px, }; -use theme::CLIENT_SIDE_DECORATION_ROUNDING; use crate::ElementExt; @@ -110,15 +109,8 @@ impl DockItem { window: &mut Window, cx: &mut App, ) -> Self { - let mut items = items; - let stack_panel = cx.new(|cx| { let mut stack_panel = StackPanel::new(axis, window, cx); - for (i, item) in items.iter_mut().enumerate() { - let view = item.view(); - let size = sizes.get(i).copied().flatten(); - stack_panel.add_panel(view.clone(), size, dock_area.clone(), window, cx) - } for (i, item) in items.iter().enumerate() { let view = item.view(); @@ -745,34 +737,22 @@ impl EventEmitter for DockArea {} impl Render for DockArea { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let view = cx.entity().clone(); - let decorations = window.window_decorations(); div() .id("dock-area") .relative() .size_full() - .overflow_hidden() .on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds)) .map(|this| { if let Some(zoom_view) = self.zoom_view.clone() { - this.map(|this| match decorations { - Decorations::Server => this, - Decorations::Client { tiling } => this - .when(!(tiling.top || tiling.right), |div| { - div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |div| { - div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) - }), - }) - .child(zoom_view) + this.child(zoom_view) } else { // render dock this.child( div() .flex() .flex_row() - .h_full() + .size_full() // Left dock .when_some(self.left_dock.clone(), |this, dock| { this.child(div().flex().flex_none().child(dock)) @@ -783,14 +763,8 @@ impl Render for DockArea { .flex() .flex_1() .flex_col() - .overflow_hidden() // Top center - .child( - div() - .flex_1() - .overflow_hidden() - .child(self.render_items(window, cx)), - ) + .child(div().flex_1().child(self.render_items(window, cx))) // Bottom Dock .when_some(self.bottom_dock.clone(), |this, dock| { this.child(dock) diff --git a/crates/ui/src/dock/stack_panel.rs b/crates/ui/src/dock/stack_panel.rs index c8e68b6..fff92ec 100644 --- a/crates/ui/src/dock/stack_panel.rs +++ b/crates/ui/src/dock/stack_panel.rs @@ -1,13 +1,12 @@ use std::sync::Arc; -use gpui::prelude::FluentBuilder; use gpui::{ App, AppContext, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, }; use smallvec::SmallVec; -use theme::{ActiveTheme, AxisExt as _, CLIENT_SIDE_DECORATION_ROUNDING, Placement}; +use theme::{AxisExt as _, Placement}; use super::{DockArea, PanelEvent}; use crate::dock::panel::{Panel, PanelView}; @@ -369,26 +368,20 @@ impl Focusable for StackPanel { } impl EventEmitter for StackPanel {} + impl EventEmitter for StackPanel {} impl Render for StackPanel { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - h_flex() - .size_full() - .overflow_hidden() - .bg(cx.theme().panel_background) - .when(cx.theme().platform.is_linux(), |this| { - this.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .child( - ResizablePanelGroup::new("stack-panel-group") - .with_state(&self.state) - .axis(self.axis) - .children(self.panels.clone().into_iter().map(|panel| { - resizable_panel() - .child(panel.view()) - .visible(panel.visible(cx)) - })), - ) + h_flex().size_full().overflow_hidden().child( + ResizablePanelGroup::new("stack-panel-group") + .with_state(&self.state) + .axis(self.axis) + .children(self.panels.clone().into_iter().map(|panel| { + resizable_panel() + .child(panel.view()) + .visible(panel.visible(cx)) + })), + ) } } diff --git a/crates/ui/src/dock/tab_panel.rs b/crates/ui/src/dock/tab_panel.rs index 36f4b03..4b96597 100644 --- a/crates/ui/src/dock/tab_panel.rs +++ b/crates/ui/src/dock/tab_panel.rs @@ -7,7 +7,7 @@ use gpui::{ ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window, div, px, rems, }; -use theme::{ActiveTheme, AxisExt, CLIENT_SIDE_DECORATION_ROUNDING, Placement, TABBAR_HEIGHT}; +use theme::{ActiveTheme, AxisExt, Placement, TABBAR_HEIGHT}; use crate::button::{Button, ButtonVariants as _}; use crate::dock::dock::DockPlacement; @@ -567,6 +567,7 @@ impl TabPanel { let left_dock_button = self.render_dock_toggle_button(DockPlacement::Left, window, cx); let bottom_dock_button = self.render_dock_toggle_button(DockPlacement::Bottom, window, cx); let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx); + let has_extend_dock_button = left_dock_button.is_some() || bottom_dock_button.is_some(); let tabs_count = self.panels.len(); let is_bottom_dock = bottom_dock_button.is_some(); @@ -586,6 +587,7 @@ impl TabPanel { .py_2() .pl_3() .pr_2() + .rounded_t(cx.theme().radius_lg) .bg(cx.theme().panel_background) .when(left_dock_button.is_some(), |this| this.pl_2()) .when(right_dock_button.is_some(), |this| this.pr_2()) @@ -641,17 +643,14 @@ impl TabPanel { TabBar::new("tab-bar") .track_scroll(&self.tab_bar_scroll_handle) .h(TABBAR_HEIGHT) + .bg(cx.theme().panel_background) + .rounded_t(cx.theme().radius_lg) .when(has_extend_dock_button, |this| { this.prefix( h_flex() .items_center() .top_0() .right(-px(1.)) - .border_r_1() - .border_b_1() - .h_full() - .border_color(cx.theme().border) - .bg(cx.theme().tab_background) .pl_0p5() .pr_1() .children(left_dock_button) @@ -689,6 +688,7 @@ impl TabPanel { let panel = panel.clone(); move |view, _ev, window, cx| { view.remove_panel(&panel, window, cx); + view.set_active_ix(ix, window, cx); } })), ) @@ -780,12 +780,8 @@ impl TabPanel { .top_0() .right_0() .h_full() - .border_l_1() - .border_b_1() .px_0p5() .gap_1() - .border_color(cx.theme().border) - .bg(cx.theme().tab_background) .child(self.render_toolbar(state, window, cx)) .when_some(right_dock_button, |this, btn| this.child(btn)), ) @@ -815,10 +811,8 @@ impl TabPanel { .child( div() .size_full() + .rounded_b(cx.theme().radius_lg) .bg(cx.theme().panel_background) - .when(cx.theme().platform.is_linux(), |this| { - this.rounded_b(CLIENT_SIDE_DECORATION_ROUNDING) - }) .overflow_hidden() .child( active_panel @@ -1140,17 +1134,24 @@ impl Render for TabPanel { state.closable = false; } - v_flex() + div() .when(!self.collapsed, |this| { this.on_action(cx.listener(Self::on_action_toggle_zoom)) .on_action(cx.listener(Self::on_action_close_panel)) }) .id("tab-panel") - .tab_group() .track_focus(&focus_handle) .size_full() + .p_1() .overflow_hidden() - .child(self.render_title_bar(&state, window, cx)) - .child(self.render_active_panel(&state, window, cx)) + .child( + v_flex() + .rounded(cx.theme().radius_lg) + .when(cx.theme().shadow, |this| this.shadow_xs()) + .size_full() + .tab_group() + .child(self.render_title_bar(&state, window, cx)) + .child(self.render_active_panel(&state, window, cx)), + ) } } diff --git a/crates/ui/src/history.rs b/crates/ui/src/history.rs index d933c4c..56785f0 100644 --- a/crates/ui/src/history.rs +++ b/crates/ui/src/history.rs @@ -1,5 +1,5 @@ use std::fmt::Debug; -use std::time::{Duration, Instant}; +use instant::{Duration, Instant}; /// A HistoryItem represents a single change in the history. /// It must implement Clone and PartialEq to be used in the History. diff --git a/crates/ui/src/indicator.rs b/crates/ui/src/indicator.rs index b07bb65..f606559 100644 --- a/crates/ui/src/indicator.rs +++ b/crates/ui/src/indicator.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use instant::Duration; use gpui::prelude::FluentBuilder as _; use gpui::{ diff --git a/crates/ui/src/input/blink_cursor.rs b/crates/ui/src/input/blink_cursor.rs index 98b112d..a0c2c97 100644 --- a/crates/ui/src/input/blink_cursor.rs +++ b/crates/ui/src/input/blink_cursor.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use instant::Duration; use gpui::{Context, Pixels, Task, px}; diff --git a/crates/ui/src/input/display_map/display_map.rs b/crates/ui/src/input/display_map/display_map.rs index bb19824..affdfa3 100644 --- a/crates/ui/src/input/display_map/display_map.rs +++ b/crates/ui/src/input/display_map/display_map.rs @@ -1,202 +1,61 @@ -/// DisplayMap: Public facade for Editor/Input display mapping. -/// -/// This combines WrapMap and FoldMap to provide a unified API: -/// - BufferPoint ↔ DisplayPoint conversion -/// - Fold management (candidates, toggle, query) -/// - Automatic projection updates on text/layout changes use std::ops::Range; use gpui::{App, Font, Pixels}; use ropey::Rope; -use super::fold_map::FoldMap; -use super::folding::FoldRange; use super::text_wrapper::{LineItem, WrapDisplayPoint}; use super::wrap_map::WrapMap; -use super::{BufferPoint, DisplayPoint}; use crate::input::Point as TreeSitterPoint; -use crate::input::display_map::WrapPoint; -use crate::input::rope_ext::RopeExt as _; -/// DisplayMap is the main interface for Editor/Input coordinate mapping. -/// -/// It manages the two-layer projection: -/// 1. Buffer → Wrap (soft-wrapping) -/// 2. Wrap → Display (folding) -/// -/// Editor/Input only needs to work with BufferPoint and DisplayPoint. +/// DisplayMap is the main interface for Input coordinate mapping. pub struct DisplayMap { wrap_map: WrapMap, - fold_map: FoldMap, } impl DisplayMap { pub fn new(font: Font, font_size: Pixels, wrap_width: Option) -> Self { Self { wrap_map: WrapMap::new(font, font_size, wrap_width), - fold_map: FoldMap::new(), } } - // ==================== Core Coordinate Mapping ==================== - - /// Convert buffer position to display position - pub fn buffer_pos_to_display_pos(&self, pos: BufferPoint) -> DisplayPoint { - // Buffer → Wrap - let wrap_pos = self.wrap_map.buffer_pos_to_wrap_pos(pos); - - // Wrap → Display - if let Some(display_row) = self.fold_map.wrap_row_to_display_row(wrap_pos.row) { - DisplayPoint::new(display_row, wrap_pos.col) - } else { - // Cursor is in a folded region, find nearest visible row - let display_row = self.fold_map.nearest_visible_display_row(wrap_pos.row); - DisplayPoint::new(display_row, 0) // Column 0 at fold boundary - } - } - - /// Convert display position to buffer position - pub fn display_pos_to_buffer_pos(&self, pos: DisplayPoint) -> BufferPoint { - // Display → Wrap - let wrap_row = self.fold_map.display_row_to_wrap_row(pos.row).unwrap_or(0); - - // Wrap → Buffer - let wrap_pos = WrapPoint::new(wrap_row, pos.col); - self.wrap_map.wrap_pos_to_buffer_pos(wrap_pos) - } - - /// Get total number of visible display rows + /// Get total number of display rows (same as wrap rows without folding) #[inline] pub fn display_row_count(&self) -> usize { - self.fold_map.display_row_count() + self.wrap_map.wrap_row_count() } /// Get the buffer line for a given display row pub fn display_row_to_buffer_line(&self, display_row: usize) -> usize { - // Display → Wrap - let wrap_row = self - .fold_map - .display_row_to_wrap_row(display_row) - .unwrap_or(0); - - // Wrap → Buffer line - self.wrap_map.wrap_row_to_buffer_line(wrap_row) + self.wrap_map.wrap_row_to_buffer_line(display_row) } /// Get the display row range for a buffer line: [start, end) - /// Returns None if the buffer line is completely hidden pub fn buffer_line_to_display_row_range(&self, line: usize) -> Option> { - // Buffer line → Wrap row range - let wrap_row_range = self.wrap_map.buffer_line_to_wrap_row_range(line); - - // Find first and last visible display rows in this range - let mut first_display_row = None; - let mut last_display_row = None; - - for wrap_row in wrap_row_range { - if let Some(display_row) = self.fold_map.wrap_row_to_display_row(wrap_row) { - if first_display_row.is_none() { - first_display_row = Some(display_row); - } - last_display_row = Some(display_row); - } - } - - if let (Some(start), Some(end)) = (first_display_row, last_display_row) { - Some(start..end + 1) - } else { - None // Completely folded - } + let range = self.wrap_map.buffer_line_to_wrap_row_range(line); + if range.is_empty() { None } else { Some(range) } } - /// Check if a buffer line is completely hidden + /// Check if a buffer line is completely hidden (never true without folding) #[inline] - pub fn is_buffer_line_hidden(&self, line: usize) -> bool { - self.buffer_line_to_display_row_range(line).is_none() + pub fn is_buffer_line_hidden(&self, _line: usize) -> bool { + false } - /// Set fold candidates (from tree-sitter/LSP) - pub fn set_fold_candidates(&mut self, candidates: Vec) { - self.fold_map.set_candidates(candidates); - self.rebuild_fold_projection(); - } - - /// Set a fold at the given start_line (must be in candidates) - pub fn set_folded(&mut self, start_line: usize, folded: bool) { - self.fold_map.set_folded(start_line, folded); - self.rebuild_fold_projection(); - } - - /// Toggle fold at the given start_line - pub fn toggle_fold(&mut self, start_line: usize) { - self.fold_map.toggle_fold(start_line); - self.rebuild_fold_projection(); - } - - /// Check if a line is currently folded + /// All wrap rows are visible since there's no folding. #[inline] - pub fn is_folded_at(&self, start_line: usize) -> bool { - self.fold_map.is_folded_at(start_line) + pub fn folded_ranges(&self) -> &[()] { + &[] } - /// Check if a line is a fold candidate - #[inline] - pub fn is_fold_candidate(&self, start_line: usize) -> bool { - self.fold_map.is_fold_candidate(start_line) - } - - /// Get all currently folded ranges - #[inline] - pub fn folded_ranges(&self) -> &[FoldRange] { - self.fold_map.folded_ranges() - } - - /// Clear all folds - pub fn clear_folds(&mut self) { - self.fold_map.clear_folds(); - self.rebuild_fold_projection(); - } - - // ==================== Text and Layout Updates ==================== - - /// Adjust folds and candidates for a text edit before updating the wrap map. - /// - /// Must be called with the OLD text (before replacement) and the edit range/new_text - /// so we can compute which old lines were affected. - pub fn adjust_folds_for_edit(&mut self, old_text: &Rope, range: &Range, new_text: &str) { - if self.fold_map.folded_ranges().is_empty() && self.fold_map.fold_candidates().is_empty() { - return; - } - - let edit_start_line = old_text.offset_to_point(range.start).row; - let edit_end_line = old_text.offset_to_point(range.end.min(old_text.len())).row; - - let old_lines_in_range = edit_end_line.saturating_sub(edit_start_line); - let new_lines_in_range = new_text.chars().filter(|c| *c == '\n').count(); - let line_delta = new_lines_in_range as isize - old_lines_in_range as isize; - - self.fold_map - .adjust_folds_for_edit(edit_start_line, edit_end_line, line_delta); - } - - /// Incrementally update fold candidates after a text edit. - /// - /// Extracts new fold candidates only within the edited byte range - /// and merges them with existing (already adjusted) candidates. - pub fn update_fold_candidates_for_edit( + /// Adjust folds for edit (no-op without folding) + pub fn adjust_folds_for_edit( &mut self, - tree: &super::folding::Tree, - edit_byte_range: Range, - new_text: &Rope, + _old_text: &Rope, + _range: &Range, + _new_text: &str, ) { - let new_start_line = new_text.offset_to_point(edit_byte_range.start).row; - let new_end_line = new_text - .offset_to_point(edit_byte_range.end.min(new_text.len())) - .row; - - let new_candidates = super::folding::extract_fold_ranges_in_range(tree, edit_byte_range); - self.fold_map - .merge_candidates_for_edit(new_start_line, new_end_line, new_candidates); + // No-op: no folding } /// Update text (incremental or full) @@ -209,52 +68,28 @@ impl DisplayMap { ) { self.wrap_map .on_text_changed(changed_text, range, new_text, cx); - self.rebuild_fold_projection(); } /// Update layout parameters (wrap width or font) pub fn on_layout_changed(&mut self, wrap_width: Option, cx: &mut App) { self.wrap_map.on_layout_changed(wrap_width, cx); - self.rebuild_fold_projection(); } /// Set font parameters pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) { self.wrap_map.set_font(font, font_size, cx); - self.rebuild_fold_projection(); } /// Ensure text is prepared (initializes wrapper if needed) pub fn ensure_text_prepared(&mut self, text: &Rope, cx: &mut App) { - let did_initialize = self.wrap_map.ensure_text_prepared(text, cx); - if did_initialize { - self.rebuild_fold_projection(); - } + self.wrap_map.ensure_text_prepared(text, cx); } /// Initialize with text pub fn set_text(&mut self, text: &Rope, cx: &mut App) { self.wrap_map.set_text(text, cx); - self.rebuild_fold_projection(); } - // ==================== Internal Helpers ==================== - - /// Rebuild fold projection after wrap_map or fold state changes - /// Only rebuilds if there are actually folded ranges - fn rebuild_fold_projection(&mut self) { - if !self.fold_map.folded_ranges().is_empty() { - self.fold_map.rebuild(&self.wrap_map); - } else { - // No active folds: identity mapping (wrap_row == display_row). - // Just update cached count so query methods work without Vec allocation. - self.fold_map - .mark_dirty_with_wrap_count(self.wrap_map.wrap_row_count()); - } - } - - // ==================== Wrap Display Point Operations ==================== - /// Convert byte offset to wrap display point (with soft wrap info). #[inline] pub(crate) fn offset_to_wrap_display_point(&self, offset: usize) -> WrapDisplayPoint { @@ -273,23 +108,30 @@ impl DisplayMap { self.wrap_map.wrapper().display_point_to_point(point) } - /// Convert a wrap row to a display row (skipping folded rows). - /// Returns None if the wrap row is folded. + /// Since there's no folding, wrap row == display row. #[inline] pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option { - self.fold_map.wrap_row_to_display_row(wrap_row) + if wrap_row < self.wrap_row_count() { + Some(wrap_row) + } else { + None + } } - /// Find the nearest visible display row for a given wrap row. + /// Since there's no folding, nearest visible row is the row itself. #[inline] pub fn nearest_visible_display_row(&self, wrap_row: usize) -> usize { - self.fold_map.nearest_visible_display_row(wrap_row) + wrap_row.min(self.wrap_row_count().saturating_sub(1)) } - /// Convert a display row to a wrap row. + /// Since there's no folding, display row == wrap row. #[inline] pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option { - self.fold_map.display_row_to_wrap_row(display_row) + if display_row < self.wrap_row_count() { + Some(display_row) + } else { + None + } } /// Get the longest row index (by byte length). @@ -298,8 +140,6 @@ impl DisplayMap { self.wrap_map.wrapper().longest_row.row } - // ==================== Access Methods ==================== - /// Get access to line items (for rendering) #[inline] pub(crate) fn lines(&self) -> &[LineItem] { @@ -312,14 +152,13 @@ impl DisplayMap { self.wrap_map.text() } - /// Calculate how many wrap rows of a buffer line are visible (not folded) + /// Calculate how many wrap rows of a buffer line are visible #[inline] pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize { - self.wrap_map - .visible_wrap_row_count_for_line(line, &self.fold_map) + self.wrap_map.visible_wrap_row_count_for_buffer_line(line) } - /// Get the wrap row count (before folding) + /// Get the wrap row count #[inline] pub fn wrap_row_count(&self) -> usize { self.wrap_map.wrap_row_count() diff --git a/crates/ui/src/input/display_map/fold_map.rs b/crates/ui/src/input/display_map/fold_map.rs deleted file mode 100644 index bcec679..0000000 --- a/crates/ui/src/input/display_map/fold_map.rs +++ /dev/null @@ -1,343 +0,0 @@ -/// FoldMap: Folding projection layer (Wrap rows → Display rows). -/// -/// This module manages code folding by: -/// - Filtering out wrap rows that belong to folded regions -/// - Maintaining bidirectional mapping: wrap_row ↔ display_row -/// - Handling fold state changes and rebuilding the projection -use super::folding::FoldRange; -use super::wrap_map::WrapMap; - -/// FoldMap projects wrap rows to display rows by hiding folded regions. -pub struct FoldMap { - /// Mapping: display_row → wrap_row - /// index = display_row, value = actual wrap_row - visible_wrap_rows: Vec, - - /// Reverse mapping: wrap_row → display_row - /// index = wrap_row, value = Some(display_row) if visible, None if folded - wrap_row_to_display_row: Vec>, - - /// Candidate fold ranges (from tree-sitter/LSP) - /// Sorted by start_line, unique start_line - candidates: Vec, - - /// Currently folded ranges - /// Subset of candidates, sorted by start_line - folded: Vec, - - /// Flag indicating if the fold projection needs rebuilding - /// Used for lazy evaluation to avoid expensive rebuilds on every text change - needs_rebuild: bool, - - /// Cached wrap_row_count from last rebuild - /// Used to detect if WrapMap changed and rebuild is needed - cached_wrap_row_count: usize, -} - -impl FoldMap { - pub fn new() -> Self { - Self { - visible_wrap_rows: Vec::new(), - wrap_row_to_display_row: Vec::new(), - candidates: Vec::new(), - folded: Vec::new(), - needs_rebuild: true, - cached_wrap_row_count: 0, - } - } - - /// Update cached wrap_row_count without full rebuild. - /// Used when no folds are active (identity mapping assumed). - pub(super) fn mark_dirty_with_wrap_count(&mut self, wrap_row_count: usize) { - self.needs_rebuild = true; - self.cached_wrap_row_count = wrap_row_count; - } - - /// Get total number of visible display rows - pub fn display_row_count(&self) -> usize { - if self.folded.is_empty() { - return self.cached_wrap_row_count; - } - self.visible_wrap_rows.len() - } - - /// Convert wrap_row to display_row - /// Returns None if the wrap_row is hidden by folding - pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option { - if self.folded.is_empty() { - return if wrap_row < self.cached_wrap_row_count { - Some(wrap_row) - } else { - None - }; - } - self.wrap_row_to_display_row - .get(wrap_row) - .copied() - .flatten() - } - - /// Convert display_row to wrap_row - pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option { - if self.folded.is_empty() { - return if display_row < self.cached_wrap_row_count { - Some(display_row) - } else { - None - }; - } - self.visible_wrap_rows.get(display_row).copied() - } - - /// Find the nearest visible display_row for a given wrap_row - pub fn nearest_visible_display_row(&self, wrap_row: usize) -> usize { - if self.folded.is_empty() { - return wrap_row.min(self.cached_wrap_row_count.saturating_sub(1)); - } - - if let Some(dr) = self.wrap_row_to_display_row(wrap_row) { - return dr; - } - - match self.visible_wrap_rows.binary_search(&wrap_row) { - Ok(idx) => idx, - Err(insert_pos) => insert_pos.saturating_sub(1), - } - } - - /// Set fold candidates (from tree-sitter/LSP), full replacement. - pub fn set_candidates(&mut self, mut candidates: Vec) { - // Sort and deduplicate by start_line - candidates.sort_by_key(|r| r.start_line); - candidates.dedup_by_key(|r| r.start_line); - self.candidates = candidates; - - // Remove any folded ranges that are no longer in candidates - self.folded.retain(|fold| { - self.candidates - .iter() - .any(|c| c.start_line == fold.start_line) - }); - } - - /// Merge new candidates extracted from an edited region into existing candidates. - /// - /// Replaces candidates within [edit_start_line, edit_end_line] with `new_candidates`, - /// keeping candidates outside the edit range intact. - pub fn merge_candidates_for_edit( - &mut self, - edit_start_line: usize, - edit_end_line: usize, - new_candidates: Vec, - ) { - // Remove old candidates within the edit range (already done by adjust_folds_for_edit) - // But do it again in case adjust wasn't called or range differs - self.candidates - .retain(|c| c.start_line < edit_start_line || c.start_line > edit_end_line); - - // Add new candidates - self.candidates.extend(new_candidates); - self.candidates.sort_by_key(|r| r.start_line); - self.candidates.dedup_by_key(|r| r.start_line); - } - - /// Set a fold at the given start_line (must be in candidates) - pub fn set_folded(&mut self, start_line: usize, folded: bool) { - if folded { - // Find the candidate range for this start_line - if let Some(candidate) = self.candidates.iter().find(|c| c.start_line == start_line) { - // Add to folded if not already present - if !self.folded.iter().any(|f| f.start_line == start_line) { - self.folded.push(*candidate); - self.folded.sort_by_key(|r| r.start_line); - self.needs_rebuild = true; - } - } - } else { - // Remove from folded - self.folded.retain(|f| f.start_line != start_line); - self.needs_rebuild = true; - } - } - - /// Toggle fold at the given start_line - pub fn toggle_fold(&mut self, start_line: usize) { - let is_folded = self.is_folded_at(start_line); - self.set_folded(start_line, !is_folded); - } - - /// Check if a line is currently folded - pub fn is_folded_at(&self, start_line: usize) -> bool { - self.folded.iter().any(|f| f.start_line == start_line) - } - - /// Check if a line is a fold candidate - pub fn is_fold_candidate(&self, start_line: usize) -> bool { - self.candidates.iter().any(|c| c.start_line == start_line) - } - - /// Get all fold candidates - #[inline] - pub fn fold_candidates(&self) -> &[FoldRange] { - &self.candidates - } - - /// Get all currently folded ranges - #[inline] - pub fn folded_ranges(&self) -> &[FoldRange] { - &self.folded - } - - /// Clear all folds - #[inline] - pub fn clear_folds(&mut self) { - self.folded.clear(); - } - - /// Adjust folds and candidates after a text edit. - /// - /// - Folds/candidates overlapping the edited line range are removed - /// - Folds/candidates after the edit are shifted by line_delta - /// - /// This avoids expensive full tree traversal on every keystroke. - pub fn adjust_folds_for_edit( - &mut self, - edit_start_line: usize, - edit_end_line: usize, - line_delta: isize, - ) { - // Adjust folded ranges - if !self.folded.is_empty() { - self.folded.retain(|fold| { - !(fold.start_line <= edit_end_line && fold.end_line >= edit_start_line) - }); - - if line_delta != 0 { - for fold in &mut self.folded { - if fold.start_line > edit_end_line { - fold.start_line = (fold.start_line as isize + line_delta).max(0) as usize; - fold.end_line = (fold.end_line as isize + line_delta).max(0) as usize; - } - } - } - } - - // Adjust candidates the same way - if !self.candidates.is_empty() { - self.candidates - .retain(|c| !(c.start_line <= edit_end_line && c.end_line >= edit_start_line)); - - if line_delta != 0 { - for c in &mut self.candidates { - if c.start_line > edit_end_line { - c.start_line = (c.start_line as isize + line_delta).max(0) as usize; - c.end_line = (c.end_line as isize + line_delta).max(0) as usize; - } - } - } - } - - self.needs_rebuild = true; - } - - /// Rebuild the fold mapping after wrap_map or fold state changes - /// - /// This is the core algorithm that projects wrap rows to display rows. - pub fn rebuild(&mut self, wrap_map: &WrapMap) { - let wrap_row_count = wrap_map.wrap_row_count(); - - // Performance optimization: skip rebuild if nothing changed - if !self.needs_rebuild && wrap_row_count == self.cached_wrap_row_count { - return; - } - - self.cached_wrap_row_count = wrap_row_count; - - self.visible_wrap_rows.clear(); - self.wrap_row_to_display_row = vec![None; wrap_row_count]; - - if self.folded.is_empty() { - // Fast path: no folds, all wrap rows are visible - self.visible_wrap_rows = (0..wrap_row_count).collect(); - for (display_row, &wrap_row) in self.visible_wrap_rows.iter().enumerate() { - self.wrap_row_to_display_row[wrap_row] = Some(display_row); - } - self.needs_rebuild = false; - return; - } - - // Build set of hidden wrap_row ranges from folded buffer lines - let mut hidden_ranges = Vec::new(); - for fold in &self.folded { - // Hide wrap rows from (start_line + 1) to (end_line - 1) (inclusive) - // Both the first line and last line of the fold remain visible - let hide_start_line = fold.start_line + 1; - let hide_end_line = fold.end_line.saturating_sub(1); - - if hide_start_line > hide_end_line { - continue; // No middle lines to hide (0 or 1 lines between start and end) - } - - // Get wrap_row ranges for the hidden buffer lines - let start_wrap_row = wrap_map.buffer_line_to_first_wrap_row(hide_start_line); - let end_wrap_row = if hide_end_line + 1 < wrap_map.buffer_line_count() { - wrap_map.buffer_line_to_first_wrap_row(hide_end_line + 1) - } else { - wrap_row_count - }; - - if start_wrap_row < end_wrap_row { - hidden_ranges.push(start_wrap_row..end_wrap_row); - } - } - - // Merge overlapping hidden ranges - hidden_ranges.sort_by_key(|r| r.start); - let mut merged_hidden = Vec::new(); - for range in hidden_ranges { - if let Some(last) = merged_hidden.last_mut() { - if range.start <= *last { - // Overlapping or adjacent, merge - *last = (*last).max(range.end); - } else { - merged_hidden.push(range.start); - merged_hidden.push(range.end); - } - } else { - merged_hidden.push(range.start); - merged_hidden.push(range.end); - } - } - - // Scan all wrap rows and filter out hidden ones - let mut display_row = 0; - let mut hidden_iter = merged_hidden.chunks_exact(2); - let mut current_hidden = hidden_iter.next(); - - for wrap_row in 0..wrap_row_count { - // Check if wrap_row is in current hidden range - let is_hidden = if let Some(&[start, end]) = current_hidden { - if wrap_row >= end { - current_hidden = hidden_iter.next(); - if let Some(&[new_start, new_end]) = current_hidden { - wrap_row >= new_start && wrap_row < new_end - } else { - false - } - } else { - wrap_row >= start && wrap_row < end - } - } else { - false - }; - - if !is_hidden { - self.visible_wrap_rows.push(wrap_row); - self.wrap_row_to_display_row[wrap_row] = Some(display_row); - display_row += 1; - } - } - - self.needs_rebuild = false; - } -} diff --git a/crates/ui/src/input/display_map/folding.rs b/crates/ui/src/input/display_map/folding.rs deleted file mode 100644 index 3c711fc..0000000 --- a/crates/ui/src/input/display_map/folding.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::ops::Range; - -#[cfg(not(target_family = "wasm"))] -use tree_sitter::Node; -#[cfg(not(target_family = "wasm"))] -pub use tree_sitter::Tree; - -#[cfg(target_family = "wasm")] -/// Stub type for tree-sitter Tree on WASM (tree-sitter not available). -pub struct Tree; - -#[cfg(not(target_family = "wasm"))] -/// Minimum line span for a node to be considered foldable. -const MIN_FOLD_LINES: usize = 2; - -/// A fold range representing a foldable code region. -/// -/// The fold range spans from start_line to end_line (inclusive). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct FoldRange { - /// Start line (inclusive) - pub start_line: usize, - /// End line (inclusive) - pub end_line: usize, -} - -impl FoldRange { - pub fn new(start_line: usize, end_line: usize) -> Self { - assert!( - start_line <= end_line, - "fold start_line must be <= end_line" - ); - Self { - start_line, - end_line, - } - } -} - -#[cfg(not(target_family = "wasm"))] -/// Check if a named node qualifies as a fold candidate. -/// -/// Uses a structural heuristic: any **named** node spanning ≥ MIN_FOLD_LINES -/// is foldable. tree-sitter already parses code into semantic units (functions, -/// classes, blocks, etc.), so named nodes naturally correspond to meaningful -/// foldable regions across all languages without a per-language node-type list. -fn is_foldable_node(node: &Node) -> bool { - let start = node.start_position().row; - let end = node.end_position().row; - end.saturating_sub(start) >= MIN_FOLD_LINES -} - -#[cfg(not(target_family = "wasm"))] -/// Extract fold ranges only within a byte range (for incremental updates after edits). -/// -/// Skips subtrees entirely outside the range, making it O(nodes in range) -/// instead of O(all nodes in tree). -pub fn extract_fold_ranges_in_range(tree: &Tree, byte_range: Range) -> Vec { - let mut ranges = Vec::new(); - let root = tree.root_node(); - let mut cursor = root.walk(); - // Skip the root, it's not foldable. Use named_children to skip literal tokens. - for child in root.named_children(&mut cursor) { - collect_foldable_nodes_in_range(child, &byte_range, &mut ranges); - } - - ranges.sort_by_key(|r| r.start_line); - ranges.dedup_by_key(|r| r.start_line); - ranges -} - -#[cfg(not(target_family = "wasm"))] -/// Recursively collect foldable nodes, skipping subtrees outside byte_range. -fn collect_foldable_nodes_in_range( - node: Node, - byte_range: &Range, - ranges: &mut Vec, -) { - if node.end_byte() <= byte_range.start || node.start_byte() >= byte_range.end { - return; - } - - if !is_foldable_node(&node) { - return; - } - - ranges.push(FoldRange { - start_line: node.start_position().row, - end_line: node.end_position().row, - }); - - let mut cursor = node.walk(); - for child in node.named_children(&mut cursor) { - collect_foldable_nodes_in_range(child, byte_range, ranges); - } -} diff --git a/crates/ui/src/input/display_map/mod.rs b/crates/ui/src/input/display_map/mod.rs index 11738a5..486b3e5 100644 --- a/crates/ui/src/input/display_map/mod.rs +++ b/crates/ui/src/input/display_map/mod.rs @@ -1,61 +1,7 @@ #[allow(clippy::module_inception)] mod display_map; -mod fold_map; -#[cfg(not(target_family = "wasm"))] -mod folding; -#[cfg(target_family = "wasm")] -pub mod folding; mod text_wrapper; mod wrap_map; -// Re-export public API -// Re-export FoldRange and extract_fold_ranges -pub use folding::FoldRange; - pub use self::display_map::DisplayMap; pub(crate) use self::text_wrapper::LineLayout; - -/// Position in the buffer (logical text). -/// -/// - `line`: 0-based logical line number (split by `\n`) -/// - `col`: 0-based column offset (byte offset) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct BufferPoint { - pub line: usize, - pub col: usize, -} - -impl BufferPoint { - pub fn new(line: usize, col: usize) -> Self { - Self { line, col } - } -} - -/// Position after soft-wrapping but before folding (internal). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub(super) struct WrapPoint { - pub row: usize, - pub col: usize, -} - -impl WrapPoint { - pub fn new(row: usize, col: usize) -> Self { - Self { row, col } - } -} - -/// Final display position (after soft-wrapping and folding). -/// -/// - `row`: 0-based display row (final visible row) -/// - `col`: 0-based display column -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct DisplayPoint { - pub row: usize, - pub col: usize, -} - -impl DisplayPoint { - pub fn new(row: usize, col: usize) -> Self { - Self { row, col } - } -} diff --git a/crates/ui/src/input/display_map/wrap_map.rs b/crates/ui/src/input/display_map/wrap_map.rs index a93cf72..ac422fc 100644 --- a/crates/ui/src/input/display_map/wrap_map.rs +++ b/crates/ui/src/input/display_map/wrap_map.rs @@ -9,10 +9,7 @@ use std::ops::Range; use gpui::{App, Font, Pixels}; use ropey::Rope; -use super::fold_map::FoldMap; -use super::text_wrapper::{LineItem, TextWrapper, WrapDisplayPoint}; -use super::{BufferPoint, WrapPoint}; -use crate::input::rope_ext::RopeExt; +use super::text_wrapper::{LineItem, TextWrapper}; /// WrapMap manages soft-wrapping and provides buffer ↔ wrap coordinate mapping. pub struct WrapMap { @@ -55,49 +52,6 @@ impl WrapMap { self.wrapper.lines.len() } - /// Convert buffer position to wrap position - pub(super) fn buffer_pos_to_wrap_pos(&self, pos: BufferPoint) -> WrapPoint { - let BufferPoint { line, col } = pos; - - // Clamp to valid range - let line = line.min(self.buffer_line_count().saturating_sub(1)); - let line_item = self.wrapper.lines.get(line); - - let col = if let Some(line_item) = line_item { - col.min(line_item.len()) - } else { - 0 - }; - - // Calculate offset in rope - let line_start_offset = self.wrapper.text().line_start_offset(line); - let offset = line_start_offset + col; - - // Use TextWrapper's existing conversion - let display_point = self.wrapper.offset_to_display_point(offset); - - WrapPoint::new(display_point.row, display_point.column) - } - - /// Convert wrap position to buffer position - pub(super) fn wrap_pos_to_buffer_pos(&self, pos: WrapPoint) -> BufferPoint { - let WrapPoint { row, col } = pos; - - // Clamp wrap_row to valid range - let row = row.min(self.wrap_row_count().saturating_sub(1)); - - // Use TextWrapper's existing conversion - let display_point = WrapDisplayPoint::new(row, 0, col); - let offset = self.wrapper.display_point_to_offset(display_point); - - // Convert offset to buffer position - let point = self.wrapper.text().offset_to_point(offset); - let line_start = self.wrapper.text().line_start_offset(point.row); - let col = offset.saturating_sub(line_start); - - BufferPoint::new(point.row, col) - } - /// Get the buffer line for a given wrap row pub fn wrap_row_to_buffer_line(&self, wrap_row: usize) -> usize { if wrap_row >= self.wrap_row_count() { @@ -176,8 +130,6 @@ impl WrapMap { let wrap_row_count = self.wrapper.len(); // Skip if nothing changed: both buffer line count and total wrap row count must match. - // Checking wrap_row_count is essential because soft-wrap can change the number of - // wrap rows per line without changing the buffer line count. if line_count == self.cached_line_count && wrap_row_count == self.cached_wrap_row_count && !self.buffer_line_starts.is_empty() @@ -212,11 +164,9 @@ impl WrapMap { self.wrapper.text() } - /// Calculate how many wrap rows of a buffer line are visible (not folded) - pub fn visible_wrap_row_count_for_line(&self, line: usize, fold_map: &FoldMap) -> usize { - let wrap_range = self.buffer_line_to_wrap_row_range(line); - wrap_range - .filter(|&wr| fold_map.wrap_row_to_display_row(wr).is_some()) - .count() + /// Calculate how many wrap rows of a buffer line are visible. + /// Without folding, all wrap rows are visible. + pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize { + self.buffer_line_to_wrap_row_range(line).len() } } diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index 8367e27..a19c27f 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -3,9 +3,8 @@ use std::rc::Rc; use gpui::{ AnyElement, App, Bounds, Corners, Edges, Element, ElementId, ElementInputHandler, Entity, - GlobalElementId, Half, HighlightStyle, Hitbox, HitboxBehavior, Hsla, InteractiveElement, - IntoElement, LayoutId, MouseButton, MouseMoveEvent, MouseUpEvent, Path, Pixels, Point, - Position, ShapedLine, SharedString, Size, Style, Styled as _, TextAlign, TextRun, TextStyle, + GlobalElementId, Half, Hsla, IntoElement, LayoutId, MouseButton, MouseMoveEvent, MouseUpEvent, + Path, Pixels, Point, Position, SharedString, Size, Style, TextAlign, TextRun, TextStyle, UnderlineStyle, Window, fill, point, px, relative, size, }; use ropey::Rope; @@ -14,19 +13,14 @@ use theme::ActiveTheme; use super::mode::InputMode; use super::{InputState, LastLayout, WhitespaceIndicators}; -use crate::button::{Button, ButtonVariants as _}; +use crate::Root; use crate::input::RopeExt as _; use crate::input::blink_cursor::CURSOR_WIDTH; use crate::input::display_map::LineLayout; use crate::scroll::Scrollbar; -use crate::{IconName, Root, Selectable, Sizable as _}; const BOTTOM_MARGIN_ROWS: usize = 3; pub(super) const RIGHT_MARGIN: Pixels = px(10.); -pub(super) const LINE_NUMBER_RIGHT_MARGIN: Pixels = px(10.); -const FOLD_ICON_WIDTH: Pixels = px(14.); -const FOLD_ICON_HITBOX_WIDTH: Pixels = px(18.); -const MAX_HIGHLIGHT_LINE_LENGTH: usize = 10_000; #[derive(Clone, Copy, Debug, PartialEq)] struct EditorScrollbarLayout { @@ -72,7 +66,7 @@ impl EditorScrollbarLayout { let left = if line_number_width == px(0.) { px(0.) } else { - paddings.left + line_number_width - LINE_NUMBER_RIGHT_MARGIN + paddings.left + line_number_width }; Self { @@ -216,14 +210,6 @@ fn masked_display_offset(text: &Rope, original_offset: usize) -> usize { text.offset_to_char_index(original_offset) * MASK_CHAR.len_utf8() } -/// Layout information for fold icons. -struct FoldIconLayout { - /// Hitbox for the line number area (used for hover detection) - line_number_hitbox: Hitbox, - /// List of (display_row, is_folded, icon_element) pairs for each fold candidate - icons: Vec<(usize, bool, gpui::AnyElement)>, -} - pub(super) struct TextElement { pub(crate) state: Entity, placeholder: SharedString, @@ -285,7 +271,7 @@ impl TextElement { scroll_size: Size, _: &mut Window, cx: &mut App, - ) -> (Option>, Point, Option) { + ) -> (Option>, Point) { let state = self.state.read(cx); let line_height = last_layout.line_height; @@ -307,7 +293,6 @@ impl TextElement { cursor = masked_display_offset(&state.text, cursor); } - let mut current_row = None; let mut scroll_offset = state.scroll_handle.offset(); let mut cursor_bounds = None; @@ -330,7 +315,6 @@ impl TextElement { let visible_buffer_lines = &last_layout.visible_buffer_lines; let mut vi = 0; // index into visible_buffer_lines / lines for (ix, wrap_line) in buffer_lines.iter().enumerate() { - let row = ix; let line_origin = point(px(0.), offset_y); // break loop if all cursor positions are found @@ -353,7 +337,6 @@ impl TextElement { if let Some(pos) = line.position_for_index(offset, last_layout, state.cursor_line_end_affinity) { - current_row = Some(row); cursor_pos = Some(line_origin + pos); } } @@ -377,7 +360,6 @@ impl TextElement { // Not visible (before visible range or hidden/folded). // Just increase the offset_y and prev_lines_offset for scroll tracking. if prev_lines_offset >= cursor && cursor_pos.is_none() { - current_row = Some(row); cursor_pos = Some(line_origin); } if prev_lines_offset >= selected_range.start && cursor_start.is_none() { @@ -494,7 +476,7 @@ impl TextElement { bounds.origin += scroll_offset; - (cursor_bounds, scroll_offset, current_row) + (cursor_bounds, scroll_offset) } /// Layout the match range to a Path. @@ -642,41 +624,6 @@ impl TextElement { builder.build().ok() } - fn layout_search_matches( - &self, - _last_layout: &LastLayout, - _bounds: &Bounds, - _cx: &mut App, - ) -> Vec<(Path, bool)> { - vec![] - } - - fn layout_hover_highlight( - &self, - _last_layout: &LastLayout, - _bounds: &Bounds, - _cx: &mut App, - ) -> Option> { - None - } - - fn layout_document_colors( - &self, - document_colors: &[(Range, Hsla)], - last_layout: &LastLayout, - bounds: &Bounds, - _cx: &mut App, - ) -> Vec<(Path, Hsla)> { - let mut paths = vec![]; - for (range, color) in document_colors.iter() { - if let Some(path) = Self::layout_match_range(range.clone(), last_layout, bounds) { - paths.push((path, *color)); - } - } - - paths - } - fn layout_selections( &self, last_layout: &LastLayout, @@ -784,267 +731,20 @@ impl TextElement { (visible_range, visible_buffer_lines, visible_top) } - /// Return (line_number_width, line_number_len) - fn layout_line_numbers( - state: &InputState, - text: &Rope, - font_size: Pixels, - style: &TextStyle, - window: &mut Window, - ) -> (Pixels, usize) { - let total_lines = text.lines_len(); - let line_number_len = match total_lines { - 0..=9999 => 5, - 10000..=99999 => 6, - 100000..=999999 => 7, - _ => 8, - }; - - let mut line_number_width = if state.mode.line_number() { - let empty_line_number = window.text_system().shape_line( - "+".repeat(line_number_len).into(), - font_size, - &[TextRun { - len: line_number_len, - font: style.font(), - color: gpui::black(), - background_color: None, - underline: None, - strikethrough: None, - }], - None, - ); - - empty_line_number.width + LINE_NUMBER_RIGHT_MARGIN - } else if state.mode.is_code_editor() && state.mode.is_multi_line() { - LINE_NUMBER_RIGHT_MARGIN - } else { - px(0.) - }; - - if state.mode.is_folding() { - // Add extra space for fold icons - line_number_width += FOLD_ICON_HITBOX_WIDTH - } - - (line_number_width, line_number_len) - } - /// Layout shaped lines for whitespace indicators (space and tab). /// /// Returns `WhitespaceIndicators` with shaped lines for space and tab characters. fn layout_whitespace_indicators( - state: &InputState, + _state: &InputState, text_size: Pixels, style: &TextStyle, window: &mut Window, cx: &App, ) -> Option { - if !state.show_whitespaces { - return None; - } - - let invisible_color = cx.theme().text_muted; - let space_font_size = text_size.half(); - let tab_font_size = text_size; - - let space_text = SharedString::new_static("•"); - let space = window.text_system().shape_line( - space_text.clone(), - space_font_size, - &[TextRun { - len: space_text.len(), - font: style.font(), - color: invisible_color, - background_color: None, - underline: None, - strikethrough: None, - }], - None, - ); - - let tab_text = SharedString::new_static("→"); - let tab = window.text_system().shape_line( - tab_text.clone(), - tab_font_size, - &[TextRun { - len: tab_text.len(), - font: style.font(), - color: invisible_color, - background_color: None, - underline: None, - strikethrough: None, - }], - None, - ); - - Some(WhitespaceIndicators { space, tab }) - } - - /// Compute inline completion ghost lines for rendering. - /// - /// Returns (first_line, ghost_lines) where: - /// - first_line: Shaped text for the first line (goes after cursor on same line) - /// - ghost_lines: Shaped lines for subsequent lines (shift content down) - fn layout_inline_completion( - _state: &InputState, - _visible_range: &Range, - _font_size: Pixels, - _window: &mut Window, - _cx: &App, - ) -> (Option, Vec) { - (None, vec![]) - } - - /// Return (line_number_width, line_number_len) - /// Layout fold icon hitboxes during prepaint phase. - /// - /// This creates hitboxes for the fold icon area, positioned to the right of line numbers. - /// Icons are created and prepainted here to avoid panics. - fn layout_fold_icons( - &self, - origin_x: Pixels, - bounds: &Bounds, - last_layout: &LastLayout, - window: &mut Window, - cx: &mut App, - ) -> FoldIconLayout { - // First pass: collect fold information from state - struct FoldInfo { - buffer_line: usize, - is_folded: bool, - display_row: usize, - offset_y: Pixels, - } - - let line_number_hitbox = window.insert_hitbox( - Bounds::new( - point(origin_x, bounds.origin.y + last_layout.visible_top), - size(last_layout.line_number_width, bounds.size.height), - ), - HitboxBehavior::Normal, - ); - - let mut icon_layout = FoldIconLayout { - line_number_hitbox, - icons: vec![], - }; - - let fold_infos: Vec = { - let state = self.state.read(cx); - if !state.mode.is_folding() { - return icon_layout; - } - - let mut infos = Vec::with_capacity(last_layout.visible_buffer_lines.len()); - let mut offset_y = last_layout.visible_top; - - for (line, &buffer_line) in last_layout - .lines - .iter() - .zip(last_layout.visible_buffer_lines.iter()) - { - if state.display_map.is_fold_candidate(buffer_line) { - let is_folded = state.display_map.is_folded_at(buffer_line); - infos.push(FoldInfo { - buffer_line, - is_folded, - display_row: buffer_line, - offset_y, - }); - } - - offset_y += line.wrapped_lines.len() * last_layout.line_height; - } - - infos - }; // state is dropped here - - // Second pass: create and prepaint icons - let line_height = last_layout.line_height; - let line_number_width = - last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN - FOLD_ICON_HITBOX_WIDTH; - let icon_relative_pos = point( - (FOLD_ICON_HITBOX_WIDTH - FOLD_ICON_WIDTH).half(), - (line_height - FOLD_ICON_WIDTH).half(), - ); - - for (ix, info) in fold_infos.iter().enumerate() { - // Position fold icon to the right of line numbers. - // Use origin_x (unscrolled) so icons stay fixed in the gutter during horizontal scroll. - let fold_icon_bounds = Bounds::new( - point( - origin_x + icon_relative_pos.x + line_number_width, - bounds.origin.y + icon_relative_pos.y + info.offset_y, - ), - size(FOLD_ICON_HITBOX_WIDTH, line_height), - ); - - // Create and prepaint icon - let mut icon = Button::new(("fold", ix)) - .ghost() - .icon(if info.is_folded { - IconName::CaretRight - } else { - IconName::CaretDown - }) - .xsmall() - .rounded_xs() - .size(FOLD_ICON_WIDTH) - .selected(info.is_folded) - .on_mouse_down(MouseButton::Left, { - let state = self.state.clone(); - let buffer_line = info.buffer_line; - move |_, _: &mut Window, cx: &mut App| { - cx.stop_propagation(); - - state.update(cx, |state, cx| { - state.display_map.toggle_fold(buffer_line); - cx.notify(); - }); - } - }) - .into_any_element(); - - icon.prepaint_as_root( - fold_icon_bounds.origin, - fold_icon_bounds.size.into(), - window, - cx, - ); - - icon_layout - .icons - .push((info.display_row, info.is_folded, icon)); - } - - icon_layout - } - - /// Paint fold icons using prepaint hitboxes. - /// - /// This handles: - /// - Rendering fold icons (chevron-right for folded, chevron-down for expanded) - /// - Mouse click handling to toggle fold state - /// - Cursor style changes on hover - /// - Only show icon on hover or for current line - fn paint_fold_icons( - &mut self, - fold_icon_layout: &mut FoldIconLayout, - current_row: Option, - window: &mut Window, - cx: &mut App, - ) { - let is_hovered = fold_icon_layout.line_number_hitbox.is_hovered(window); - for (display_row, is_folded, icon) in fold_icon_layout.icons.iter_mut() { - let is_current_line = current_row == Some(*display_row); - - if !is_hovered && !is_current_line && !*is_folded { - continue; - } - - icon.paint(window, cx); - } + // Whitespace indicators are not currently enabled. + // When re-enabled, check `state.show_whitespaces` to conditionally enable. + let _ = (text_size, style, window, cx); + None } #[allow(clippy::too_many_arguments)] @@ -1098,10 +798,6 @@ impl TextElement { } let mut lines = Vec::with_capacity(last_layout.visible_buffer_lines.len()); - // run_offset tracks position in the runs vec coordinate space (only visible line bytes). - // This is separate from the visible_text offset because runs from highlight_lines - // only cover visible (non-folded) lines. - let mut run_offset = 0; for (vi, &buffer_line) in last_layout.visible_buffer_lines.iter().enumerate() { let line_text: String = display_text.slice_line(buffer_line).into(); @@ -1112,9 +808,10 @@ impl TextElement { debug_assert_eq!(line_item.len(), line_text.len()); let mut wrapped_lines = SmallVec::with_capacity(1); + let line_offset = display_text.line_start_offset(buffer_line); for range in &line_item.wrapped_lines { - let line_runs = runs_for_range(runs, run_offset, range); + let line_runs = runs_for_range(runs, line_offset, range); let line_runs = if bg_segments.is_empty() { line_runs } else { @@ -1137,107 +834,21 @@ impl TextElement { .lines(wrapped_lines) .with_whitespaces(whitespace_indicators.clone()); lines.push(line_layout); - - // +1 for the `\n` - run_offset += line_text.len() + 1; } lines } - - /// First usize is the offset of skipped. - fn highlight_lines( - &mut self, - visible_buffer_lines: &[usize], - _visible_top: Pixels, - _visible_byte_range: Range, - cx: &mut App, - ) -> Option, HighlightStyle)>> { - let state = self.state.read(cx); - let text = &state.text; - let is_multi_line = state.mode.is_multi_line(); - - let mut styles = Vec::with_capacity(visible_buffer_lines.len()); - - // Helper to flush a contiguous range of lines. These ranges are disjoint, - // so appending avoids repeatedly cloning and recombining prior styles. - let flush_range = |start_line: usize, end_line: usize, _skip: bool, styles: &mut Vec<_>| { - let byte_start = text.line_start_offset(start_line); - let byte_end = if is_multi_line { - // +1 for `\n` - text.line_start_offset(end_line + 1) - } else { - text.line_end_offset(end_line) - }; - let range_styles = vec![(byte_start..byte_end, HighlightStyle::default())]; - styles.extend(range_styles); - }; - - // Group contiguous visible lines into ranges and call styles() once per range - let mut visible_iter = visible_buffer_lines.iter().peekable(); - let mut range_start: Option = None; - - while let Some(&line) = visible_iter.next() { - // Check if this line is too long for highlighting - let line_len = text.slice_line(line).len(); - if line_len > MAX_HIGHLIGHT_LINE_LENGTH { - // Flush any accumulated range first - if let Some(start) = range_start.take() { - flush_range(start, line - 1, false, &mut styles); - } - - flush_range(line, line, true, &mut styles); - continue; - } - - range_start.get_or_insert(line); - - // Check if next line is contiguous, if so keep accumulating - if visible_iter - .peek() - .map(|&&next| next == line + 1) - .unwrap_or(false) - { - continue; - } - - // Flush the contiguous range - let start_line = range_start.take().unwrap(); - flush_range(start_line, line, false, &mut styles); - } - - Some(styles) - } } pub(super) struct PrepaintState { /// The lines of entire lines. last_layout: LastLayout, - /// The lines only contains the visible lines in the viewport, based on `visible_range`. - /// - /// The child is the soft lines. - line_numbers: Option>>, /// Size of the scrollable area by entire lines. scroll_size: Size, cursor_bounds: Option>, cursor_scroll_offset: Point, - /// row index (zero based), no wrap, same line as the cursor. - current_row: Option, selection_path: Option>, - hover_highlight_path: Option>, - search_match_paths: Vec<(Path, bool)>, - document_color_paths: Vec<(Path, Hsla)>, - hover_definition_hitbox: Option, - indent_guides_path: Option>, bounds: Bounds, - /// Fold icon layout data - fold_icon_layout: FoldIconLayout, - // Inline completion rendering data - /// Shaped ghost lines to paint after cursor row (completion lines 2+) - ghost_lines: Vec, - /// First line of inline completion (painted after cursor on same line) - ghost_first_line: Option, - ghost_lines_height: Pixels, } impl PrepaintState { @@ -1356,13 +967,6 @@ impl Element for TextElement { .text .line_end_offset(visible_range.end.saturating_sub(1)); - let highlight_styles = self.highlight_lines( - &visible_buffer_lines, - visible_top, - visible_start_offset..visible_end_offset, - cx, - ); - let state = self.state.read(cx); let multi_line = state.mode.is_multi_line(); let text = state.text.clone(); @@ -1382,9 +986,8 @@ impl Element for TextElement { (&text, fg) }; - // Calculate the width of the line numbers - let (line_number_width, line_number_len) = - Self::layout_line_numbers(state, &text, text_size, &text_style, window); + // Line numbers are not used (code editor mode removed) + let line_number_width = px(0.); let mut bounds = bounds; let wrap_width = if multi_line && state.soft_wrap { @@ -1452,28 +1055,7 @@ impl Element for TextElement { }; let runs = if !is_empty { - if let Some(highlight_styles) = highlight_styles { - let mut runs = Vec::with_capacity(highlight_styles.len()); - - runs.extend(highlight_styles.iter().map(|(range, style)| { - let mut run = text_style.clone().highlight(*style).to_run(range.len()); - - if let Some(ime_marked_range) = &state.ime_marked_range - && range.start >= ime_marked_range.start - && range.end <= ime_marked_range.end - { - run.color = marked_run.color; - run.strikethrough = marked_run.strikethrough; - run.underline = marked_run.underline; - } - - run - })); - - runs.into_iter().filter(|run| run.len > 0).collect() - } else { - vec![run] - } + vec![run] } else if let Some(ime_marked_range) = &state.ime_marked_range { // IME marked text vec![ @@ -1498,8 +1080,6 @@ impl Element for TextElement { vec![run] }; - let document_colors = []; - // Create shaped lines for whitespace indicators before layout let whitespace_indicators = Self::layout_whitespace_indicators(state, text_size, &text_style, window, cx); @@ -1510,7 +1090,7 @@ impl Element for TextElement { &last_layout, text_size, &runs, - &document_colors, + &[], whitespace_indicators, window, ); @@ -1540,26 +1120,8 @@ impl Element for TextElement { } last_layout.lines = Rc::new(lines); - let (ghost_first_line, ghost_lines) = Self::layout_inline_completion( - state, - &last_layout.visible_range, - text_size, - window, - cx, - ); - let ghost_line_count = ghost_lines.len(); - let ghost_lines_height = ghost_line_count as f32 * line_height; - let total_wrapped_lines = state.display_map.wrap_row_count(); - let empty_bottom_height = if state.mode.is_code_editor() { - bounds - .size - .height - .half() - .max(BOTTOM_MARGIN_ROWS * line_height) - } else { - px(0.) - }; + let empty_bottom_height = px(0.); let mut scroll_size = size( if longest_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width { @@ -1567,7 +1129,7 @@ impl Element for TextElement { } else { longest_line_width }, - (total_wrapped_lines as f32 * line_height + empty_bottom_height + ghost_lines_height) + (total_wrapped_lines as f32 * line_height + empty_bottom_height) .max(bounds.size.height), ); @@ -1608,75 +1170,16 @@ impl Element for TextElement { // Calculate the scroll offset to keep the cursor in view - // Save the unscrolled x before layout_cursor modifies bounds.origin with scroll_offset. - // Fold icons and their hitboxes must use this value so they stay fixed in the gutter - // regardless of horizontal scroll position. + // Save the bounds before layout_cursor modifies bounds.origin with scroll_offset. let input_bounds = bounds; - let original_x = bounds.origin.x; - let (cursor_bounds, cursor_scroll_offset, current_row) = + let (cursor_bounds, cursor_scroll_offset) = self.layout_cursor(&last_layout, &mut bounds, scroll_size, window, cx); last_layout.cursor_bounds = cursor_bounds; - let search_match_paths = self.layout_search_matches(&last_layout, &bounds, cx); let selection_path = self.layout_selections(&last_layout, &mut bounds, window, cx); - let hover_highlight_path = self.layout_hover_highlight(&last_layout, &bounds, cx); - let document_color_paths = - self.layout_document_colors(&document_colors, &last_layout, &bounds, cx); let state = self.state.read(cx); - let line_numbers = if state.mode.line_number() { - let mut line_numbers = Vec::with_capacity(last_layout.visible_buffer_lines.len()); - let other_line_runs = vec![TextRun { - len: line_number_len, - font: style.font(), - color: cx.theme().text_muted, - background_color: None, - underline: None, - strikethrough: None, - }]; - let current_line_runs = vec![TextRun { - len: line_number_len, - font: style.font(), - color: cx.theme().text, - background_color: None, - underline: None, - strikethrough: None, - }]; - - // build line numbers - for (line, &buffer_line) in last_layout - .lines - .iter() - .zip(last_layout.visible_buffer_lines.iter()) - { - let line_no: SharedString = - format!("{:>width$}", buffer_line + 1, width = line_number_len).into(); - - let runs = if current_row == Some(buffer_line) { - ¤t_line_runs - } else { - &other_line_runs - }; - - let mut sub_lines: SmallVec<[ShapedLine; 1]> = SmallVec::new(); - sub_lines.push( - window - .text_system() - .shape_line(line_no, text_size, runs, None), - ); - for _ in 0..line.wrapped_lines.len().saturating_sub(1) { - sub_lines.push(ShapedLine::default()); - } - line_numbers.push(sub_lines); - } - Some(line_numbers) - } else { - None - }; - - let indent_guides_path = - self.layout_indent_guides(state, &bounds, &last_layout, &text_style, window); state .editor_scrollbar_snapshot @@ -1688,27 +1191,13 @@ impl Element for TextElement { state, ))); - let fold_icon_layout = - self.layout_fold_icons(original_x, &bounds, &last_layout, window, cx); - PrepaintState { bounds, last_layout, scroll_size, - line_numbers, cursor_bounds, cursor_scroll_offset, - current_row, selection_path, - search_match_paths, - hover_highlight_path, - hover_definition_hitbox: None, - document_color_paths, - indent_guides_path, - fold_icon_layout, - ghost_first_line, - ghost_lines, - ghost_lines_height, } } @@ -1765,56 +1254,15 @@ impl Element for TextElement { let invisible_top_padding = prepaint.last_layout.visible_top; - // Paint active line - let mut offset_y = px(0.); - if let Some(line_numbers) = prepaint.line_numbers.as_ref() { - offset_y += invisible_top_padding; - - // Each item is the normal lines. - for (lines, _) in line_numbers - .iter() - .zip(prepaint.last_layout.visible_buffer_lines.iter()) - { - let height = line_height * lines.len() as f32; - offset_y += height; - } - } - - // Paint indent guides - if let Some(path) = prepaint.indent_guides_path.take() { - window.paint_path(path, cx.theme().border.opacity(0.85)); - } - // Paint selections - if window.is_window_active() { - let secondary_selection = cx.theme().selection; - for (path, is_active) in prepaint.search_match_paths.iter() { - window.paint_path(path.clone(), secondary_selection); - - if *is_active { - window.paint_path(path.clone(), cx.theme().selection); - } - } - - if let Some(path) = prepaint.selection_path.take() { - window.paint_path(path, cx.theme().selection); - } - - // Paint hover highlight - if let Some(path) = prepaint.hover_highlight_path.take() { - window.paint_path(path, secondary_selection); - } + if window.is_window_active() + && let Some(path) = prepaint.selection_path.take() + { + window.paint_path(path, cx.theme().selection); } - // Paint document colors - for (path, color) in prepaint.document_color_paths.iter() { - window.paint_path(path.clone(), *color); - } - - // Paint text with inline completion ghost line support + // Paint text let mut offset_y = invisible_top_padding; - let ghost_lines = &prepaint.ghost_lines; - let has_ghost_lines = !ghost_lines.is_empty(); // Keep scrollbar offset always be positive,Start from the left position let scroll_offset = if text_align == TextAlign::Right { @@ -1827,16 +1275,12 @@ impl Element for TextElement { px(0.) }; - // Track the y-position of the cursor row for positioning the first line suffix - let mut cursor_row_y = None; - - for (line, &buffer_line) in prepaint + for (line, _buffer_line) in prepaint .last_layout .lines .iter() .zip(prepaint.last_layout.visible_buffer_lines.iter()) { - let row = buffer_line; let line_y = origin.y + offset_y; let p = point( origin.x + prepaint.last_layout.line_number_width + (scroll_offset), @@ -1853,40 +1297,6 @@ impl Element for TextElement { cx, ); offset_y += line.size(line_height).height; - - if Some(row) == prepaint.current_row { - cursor_row_y = Some(line_y); - } - - // After the cursor row, paint ghost lines (which shifts subsequent content down) - if has_ghost_lines && Some(row) == prepaint.current_row { - let ghost_x = origin.x + prepaint.last_layout.line_number_width; - - for ghost_line in ghost_lines { - let ghost_p = point(ghost_x, origin.y + offset_y); - - // Paint semi-transparent background for ghost line - let ghost_bounds = Bounds::new( - ghost_p, - size( - bounds.size.width - prepaint.last_layout.line_number_width, - line_height, - ), - ); - window.paint_quad(fill(ghost_bounds, cx.theme().surface_background)); - - // Paint ghost line text - _ = ghost_line.paint( - ghost_p, - line_height, - text_align, - Some(prepaint.last_layout.content_width), - window, - cx, - ); - offset_y += line_height; - } - } } // Paint blinking cursor @@ -1897,49 +1307,6 @@ impl Element for TextElement { window.paint_quad(fill(cursor_bounds, cx.theme().cursor)); } - // Paint line numbers - let mut offset_y = px(0.); - if let Some(line_numbers) = prepaint.line_numbers.as_ref() { - offset_y += invisible_top_padding; - - window.paint_quad(fill( - Bounds { - origin: input_bounds.origin, - size: size( - prepaint.last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN, - input_bounds.size.height + prepaint.ghost_lines_height, - ), - }, - cx.theme().surface_background, - )); - - // Each item is the normal lines. - for (lines, &buffer_line) in line_numbers - .iter() - .zip(prepaint.last_layout.visible_buffer_lines.iter()) - { - let p = point(input_bounds.origin.x, origin.y + offset_y); - - for line in lines { - _ = line.paint(p, line_height, TextAlign::Left, None, window, cx); - offset_y += line_height; - } - - // Add ghost line height after cursor row for line numbers alignment - if !prepaint.ghost_lines.is_empty() && prepaint.current_row == Some(buffer_line) { - offset_y += prepaint.ghost_lines_height; - } - } - } - - // Paint fold icons (only visible on hover or for current line) - self.paint_fold_icons( - &mut prepaint.fold_icon_layout, - prepaint.current_row, - window, - cx, - ); - self.state.update(cx, |state, cx| { state.last_layout = Some(prepaint.last_layout.clone()); state.last_bounds = Some(bounds); @@ -1953,27 +1320,6 @@ impl Element for TextElement { cx.notify(); }); - if let Some(hitbox) = prepaint.hover_definition_hitbox.as_ref() { - window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox); - } - - // Paint inline completion first line suffix (after cursor on same line) - if focused - && let Some(first_line) = &prepaint.ghost_first_line - && let (Some(cursor_bounds), Some(cursor_row_y)) = - (prepaint.cursor_bounds_with_scroll(), cursor_row_y) - { - let first_line_x = cursor_bounds.origin.x + cursor_bounds.size.width; - let p = point(first_line_x, cursor_row_y); - - // Paint background to cover any existing text - let bg_bounds = Bounds::new(p, size(first_line.width + px(4.), line_height)); - window.paint_quad(fill(bg_bounds, cx.theme().surface_background)); - - // Paint first line completion text - _ = first_line.paint(p, line_height, text_align, None, window, cx); - } - self.paint_mouse_listeners(window, cx); } } diff --git a/crates/ui/src/input/indent.rs b/crates/ui/src/input/indent.rs index 54dd386..2f38e49 100644 --- a/crates/ui/src/input/indent.rs +++ b/crates/ui/src/input/indent.rs @@ -1,12 +1,8 @@ -use gpui::{ - Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels, SharedString, - TextRun, TextStyle, Window, point, px, -}; +use gpui::{Context, EntityInputHandler, SharedString, Window}; use ropey::RopeSlice; -use crate::input::element::TextElement; use crate::input::mode::InputMode; -use crate::input::{Indent, IndentInline, InputState, LastLayout, Outdent, OutdentInline, RopeExt}; +use crate::input::{Indent, IndentInline, InputState, Outdent, OutdentInline}; #[derive(Debug, Copy, Clone)] pub struct TabSize { @@ -49,165 +45,14 @@ impl TabSize { } } -impl InputMode { - #[inline] - pub(super) fn is_indentable(&self) -> bool { - match self { - InputMode::PlainText { multi_line, .. } | InputMode::CodeEditor { multi_line, .. } => { - *multi_line - } - _ => false, - } - } - - #[inline] - pub(super) fn has_indent_guides(&self) -> bool { - match self { - InputMode::CodeEditor { - indent_guides, - multi_line, - .. - } => *indent_guides && *multi_line, - _ => false, - } - } - - #[inline] - pub(super) fn tab_size(&self) -> TabSize { - match self { - InputMode::PlainText { tab, .. } => *tab, - InputMode::CodeEditor { tab, .. } => *tab, - _ => TabSize::default(), - } - } -} - -impl TextElement { - /// Measure the indent width in pixels for given column count. - fn measure_indent_width(&self, style: &TextStyle, column: usize, window: &Window) -> Pixels { - let font_size = style.font_size.to_pixels(window.rem_size()); - let layout = window.text_system().shape_line( - SharedString::from(" ".repeat(column)), - font_size, - &[TextRun { - len: column, - font: style.font(), - color: Hsla::default(), - background_color: None, - strikethrough: None, - underline: None, - }], - None, - ); - - layout.width - } - - pub(super) fn layout_indent_guides( - &self, - state: &InputState, - bounds: &Bounds, - last_layout: &LastLayout, - text_style: &TextStyle, - window: &mut Window, - ) -> Option> { - if !state.mode.has_indent_guides() { - return None; - } - - let indent_width = - self.measure_indent_width(text_style, state.mode.tab_size().tab_size, window); - - let tab_size = state.mode.tab_size(); - let line_height = last_layout.line_height; - let mut builder = PathBuilder::stroke(px(1.)); - let mut offset_y = last_layout.visible_top; - let mut last_indents = vec![]; - - for (&buffer_line, line_layout) in last_layout - .visible_buffer_lines - .iter() - .zip(last_layout.lines.iter()) - { - let line = state.text.slice_line(buffer_line); - let mut current_indents = vec![]; - if line.len() > 0 { - let indent_count = tab_size.indent_count(&line); - for offset in (0..indent_count).step_by(tab_size.tab_size) { - let x = if indent_count > 0 { - indent_width * offset as f32 / tab_size.tab_size as f32 - } else { - px(0.) - }; - - let pos = point(x + last_layout.line_number_width, offset_y); - - builder.move_to(pos); - builder.line_to(point(pos.x, pos.y + line_height)); - current_indents.push(pos.x); - } - } else if !last_indents.is_empty() { - for x in &last_indents { - let pos = point(*x, offset_y); - builder.move_to(pos); - builder.line_to(point(pos.x, pos.y + line_height)); - } - current_indents = last_indents.clone(); - } - - offset_y += line_layout.wrapped_lines.len() * line_height; - last_indents = current_indents; - } - - builder.translate(bounds.origin); - let path = builder.build().unwrap(); - Some(path) - } -} - impl InputState { - /// Set whether to show indent guides in code editor mode, default is true. - /// - /// Only for [`InputMode::CodeEditor`] mode. - pub fn indent_guides(mut self, indent_guides: bool) -> Self { - debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line()); - if let InputMode::CodeEditor { - indent_guides: l, .. - } = &mut self.mode - { - *l = indent_guides; - } - self - } - - /// Set indent guides in code editor mode. - /// - /// Only for [`InputMode::CodeEditor`] mode. - pub fn set_indent_guides( - &mut self, - indent_guides: bool, - _: &mut Window, - cx: &mut Context, - ) { - debug_assert!(self.mode.is_code_editor()); - if let InputMode::CodeEditor { - indent_guides: l, .. - } = &mut self.mode - { - *l = indent_guides; - } - cx.notify(); - } - /// Set the tab size for the input. /// - /// Only for [`InputMode::PlainText`] and [`InputMode::CodeEditor`] mode with multi_line. + /// Only for [`InputMode::PlainText`] mode with multi_line. pub fn tab_size(mut self, tab: TabSize) -> Self { - debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor()); - match &mut self.mode { - InputMode::PlainText { tab: t, .. } => *t = tab, - InputMode::CodeEditor { tab: t, .. } => *t = tab, - _ => {} + debug_assert!(self.mode.is_multi_line()); + if let InputMode::PlainText { tab: t, .. } = &mut self.mode { + *t = tab; } self } diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index 8187819..5efa0a0 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -35,8 +35,6 @@ pub struct Input { cleanable: bool, mask_toggle: bool, disabled: bool, - bordered: bool, - focus_bordered: bool, tab_index: isize, selected: bool, } @@ -73,8 +71,6 @@ impl Input { cleanable: false, mask_toggle: false, disabled: false, - bordered: true, - focus_bordered: true, tab_index: 0, selected: false, } @@ -108,18 +104,6 @@ impl Input { self } - /// Set the bordered for the input, default: true - pub fn bordered(mut self, bordered: bool) -> Self { - self.bordered = bordered; - self - } - - /// Set focus border for the input, default is true. - pub fn focus_bordered(mut self, bordered: bool) -> Self { - self.focus_bordered = bordered; - self - } - /// Set whether to show the clear button when the input field is not empty, default is false. pub fn cleanable(mut self, cleanable: bool) -> Self { self.cleanable = cleanable; @@ -234,12 +218,6 @@ impl RenderOnce for Input { let (bg, _) = input_style(state.disabled, cx); - let bg = if state.mode.is_code_editor() { - cx.theme().surface_background - } else { - bg - }; - let prefix = self.prefix; let suffix = self.suffix; let show_clear_button = self.cleanable @@ -338,11 +316,6 @@ impl RenderOnce for Input { this.bg(bg) .when(self.disabled, |this| this.opacity(0.5)) .rounded(cx.theme().radius) - .when(self.bordered, |this| { - this.border_color(cx.theme().border) - .border_1() - .when(cx.theme().shadow, |this| this.shadow_xs()) - }) }) .items_center() .gap(gap_x) diff --git a/crates/ui/src/input/mod.rs b/crates/ui/src/input/mod.rs index 63d5bce..de8e497 100644 --- a/crates/ui/src/input/mod.rs +++ b/crates/ui/src/input/mod.rs @@ -18,9 +18,7 @@ mod state; pub(crate) use clear_button::*; pub use cursor::*; -#[cfg(target_family = "wasm")] -pub use display_map::folding::Tree; -pub use display_map::{BufferPoint, DisplayMap, DisplayPoint, FoldRange}; +pub use display_map::DisplayMap; pub use indent::TabSize; pub use input::*; pub use mask_pattern::MaskPattern; diff --git a/crates/ui/src/input/mode.rs b/crates/ui/src/input/mode.rs index aea247a..30b1f43 100644 --- a/crates/ui/src/input/mode.rs +++ b/crates/ui/src/input/mode.rs @@ -1,26 +1,11 @@ -use std::cell::RefCell; -use std::rc::Rc; - -use gpui::{SharedString, Task}; -use ropey::Rope; - use super::display_map::DisplayMap; -use crate::input::TabSize; - -#[allow(dead_code)] -pub(super) struct PendingBackgroundParse { - pub parse_task: Rc>>>, - pub language: SharedString, - pub text: Rope, - pub is_folding: bool, -} #[derive(Clone)] pub(crate) enum InputMode { /// A plain text input mode. PlainText { multi_line: bool, - tab: TabSize, + tab: crate::input::indent::TabSize, rows: usize, }, /// An auto grow input mode. @@ -29,18 +14,6 @@ pub(crate) enum InputMode { min_rows: usize, max_rows: usize, }, - /// A code editor input mode. - CodeEditor { - multi_line: bool, - tab: TabSize, - rows: usize, - /// Show line number - line_number: bool, - language: SharedString, - indent_guides: bool, - folding: bool, - parse_task: Rc>>>, - }, } impl Default for InputMode { @@ -55,25 +28,11 @@ impl InputMode { pub(super) fn plain_text() -> Self { InputMode::PlainText { multi_line: false, - tab: TabSize::default(), + tab: crate::input::indent::TabSize::default(), rows: 1, } } - /// Create a code editor input mode with default settings. - pub(super) fn code_editor(language: impl Into) -> Self { - InputMode::CodeEditor { - rows: 2, - multi_line: true, - tab: TabSize::default(), - language: language.into(), - line_number: true, - indent_guides: true, - folding: true, - parse_task: Rc::new(RefCell::new(None)), - } - } - /// Create an auto grow input mode with given min and max rows. pub(super) fn auto_grow(min_rows: usize, max_rows: usize) -> Self { InputMode::AutoGrow { @@ -86,7 +45,6 @@ impl InputMode { pub(super) fn multi_line(mut self, multi_line: bool) -> Self { match &mut self { InputMode::PlainText { multi_line: ml, .. } => *ml = multi_line, - InputMode::CodeEditor { multi_line: ml, .. } => *ml = multi_line, InputMode::AutoGrow { .. } => {} } self @@ -97,28 +55,6 @@ impl InputMode { !self.is_multi_line() } - #[inline] - pub(super) fn is_code_editor(&self) -> bool { - matches!(self, InputMode::CodeEditor { .. }) - } - - /// Return true if the mode is code editor and `folding: true`, `multi_line: true`. - #[inline] - pub(crate) fn is_folding(&self) -> bool { - if cfg!(target_family = "wasm") { - return false; - } - - matches!( - self, - InputMode::CodeEditor { - folding: true, - multi_line: true, - .. - } - ) - } - #[inline] pub(super) fn is_auto_grow(&self) -> bool { matches!(self, InputMode::AutoGrow { .. }) @@ -128,7 +64,6 @@ impl InputMode { pub(super) fn is_multi_line(&self) -> bool { match self { InputMode::PlainText { multi_line, .. } => *multi_line, - InputMode::CodeEditor { multi_line, .. } => *multi_line, InputMode::AutoGrow { max_rows, .. } => *max_rows > 1, } } @@ -138,9 +73,6 @@ impl InputMode { InputMode::PlainText { rows, .. } => { *rows = new_rows; } - InputMode::CodeEditor { rows, .. } => { - *rows = new_rows; - } InputMode::AutoGrow { rows, min_rows, @@ -168,7 +100,6 @@ impl InputMode { match self { InputMode::PlainText { rows, .. } => *rows, - InputMode::CodeEditor { rows, .. } => *rows, InputMode::AutoGrow { rows, .. } => *rows, } .max(1) @@ -196,16 +127,19 @@ impl InputMode { } } - /// Return false if the mode is not [`InputMode::CodeEditor`]. #[inline] - pub(super) fn line_number(&self) -> bool { + pub(super) fn is_indentable(&self) -> bool { match self { - InputMode::CodeEditor { - line_number, - multi_line, - .. - } => *line_number && *multi_line, + InputMode::PlainText { multi_line, .. } => *multi_line, _ => false, } } + + #[inline] + pub(super) fn tab_size(&self) -> crate::input::indent::TabSize { + match self { + InputMode::PlainText { tab, .. } => *tab, + _ => crate::input::indent::TabSize::default(), + } + } } diff --git a/crates/ui/src/input/popovers/code_action_menu.rs b/crates/ui/src/input/popovers/code_action_menu.rs deleted file mode 100644 index 9e2ab5b..0000000 --- a/crates/ui/src/input/popovers/code_action_menu.rs +++ /dev/null @@ -1,337 +0,0 @@ -use std::rc::Rc; - -use gpui::prelude::FluentBuilder; -use gpui::{ - Action, AnyElement, App, AppContext, Context, DismissEvent, Empty, Entity, EventEmitter, - InteractiveElement as _, IntoElement, ParentElement, Pixels, Point, Render, RenderOnce, - SharedString, Styled, StyledText, Subscription, Window, deferred, div, px, relative, -}; -use lsp_types::CodeAction; -use theme::ActiveTheme; - -const MAX_MENU_WIDTH: Pixels = px(320.); -const MAX_MENU_HEIGHT: Pixels = px(480.); - -use crate::input::popovers::editor_popover; -use crate::input::{self, InputState}; -use crate::list::{List, ListDelegate, ListEvent, ListState}; -use crate::{IndexPath, Selectable, actions, h_flex}; - -#[derive(Debug, Clone)] -pub(crate) struct CodeActionItem { - /// The `id` of the `CodeActionProvider` that provided this item. - pub(crate) provider_id: SharedString, - pub(crate) action: CodeAction, -} - -struct MenuDelegate { - menu: Entity, - items: Vec>, - selected_ix: usize, -} - -impl MenuDelegate { - fn set_items(&mut self, items: Vec) { - self.items = items.into_iter().map(Rc::new).collect(); - self.selected_ix = 0; - } - - fn selected_item(&self) -> Option<&Rc> { - self.items.get(self.selected_ix) - } -} - -#[derive(IntoElement)] -struct MenuItem { - ix: usize, - item: Rc, - children: Vec, - selected: bool, -} - -impl MenuItem { - fn new(ix: usize, item: Rc) -> Self { - Self { - ix, - item, - children: vec![], - selected: false, - } - } -} -impl Selectable for MenuItem { - fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - fn is_selected(&self) -> bool { - self.selected - } -} - -impl ParentElement for MenuItem { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements); - } -} -impl RenderOnce for MenuItem { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let item = self.item; - - let highlights = vec![]; - - h_flex() - .id(self.ix) - .gap_2() - .p_1() - .text_xs() - .line_height(relative(1.)) - .rounded(cx.theme().radius) - .hover(|this| this.bg(cx.theme().secondary_hover)) - .when(self.selected, |this| { - this.bg(cx.theme().secondary_background) - .text_color(cx.theme().secondary_foreground) - }) - .child( - div().child(StyledText::new(item.action.title.clone()).with_highlights(highlights)), - ) - .children(self.children) - } -} - -impl EventEmitter for MenuDelegate {} - -impl ListDelegate for MenuDelegate { - type Item = MenuItem; - - fn items_count(&self, _: usize, _: &gpui::App) -> usize { - self.items.len() - } - - fn render_item( - &mut self, - ix: crate::IndexPath, - _: &mut Window, - _: &mut Context>, - ) -> Option { - let item = self.items.get(ix.row)?; - Some(MenuItem::new(ix.row, item.clone())) - } - - fn set_selected_index( - &mut self, - ix: Option, - _: &mut Window, - cx: &mut Context>, - ) { - self.selected_ix = ix.map(|i| i.row).unwrap_or(0); - cx.notify(); - } - - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { - let Some(item) = self.selected_item() else { - return; - }; - - self.menu.update(cx, |this, cx| { - this.select_item(&item, window, cx); - }); - } -} - -/// A context menu for code completions and code actions. -pub struct CodeActionMenu { - offset: usize, - state: Entity, - list: Entity>, - open: bool, - - _subscriptions: Vec, -} - -impl CodeActionMenu { - /// Creates a new `CompletionMenu` with the given offset and completion items. - /// - /// NOTE: This element should not call from InputState::new, unless that will stack overflow. - pub(crate) fn new( - state: Entity, - window: &mut Window, - cx: &mut App, - ) -> Entity { - cx.new(|cx| { - let view = cx.entity(); - let menu = MenuDelegate { - menu: view, - items: vec![], - selected_ix: 0, - }; - - let list = cx.new(|cx| ListState::new(menu, window, cx)); - - let _subscriptions = - vec![ - cx.subscribe(&list, |this: &mut Self, _, ev: &ListEvent, cx| { - match ev { - ListEvent::Confirm(_) => { - this.hide(cx); - } - _ => {} - } - cx.notify(); - }), - ]; - - Self { - offset: 0, - state, - list, - open: false, - _subscriptions, - } - }) - } - - fn select_item(&mut self, item: &CodeActionItem, window: &mut Window, cx: &mut Context) { - let state = self.state.clone(); - let item = item.clone(); - - cx.spawn_in(window, { - async move |_, cx| { - state.update_in(cx, |state, window, cx| { - state.perform_code_action(&item, window, cx); - }) - } - }) - .detach(); - - self.hide(cx); - } - - pub(crate) fn handle_action( - &mut self, - action: Box, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if !self.open { - return false; - } - - cx.propagate(); - if input::Enter::is_primary(&*action) { - self.on_action_enter(window, cx); - } else if action.partial_eq(&input::Escape) { - self.on_action_escape(window, cx); - } else if action.partial_eq(&input::MoveUp) { - self.on_action_up(window, cx); - } else if action.partial_eq(&input::MoveDown) { - self.on_action_down(window, cx); - } else { - return false; - } - - true - } - - fn on_action_enter(&mut self, window: &mut Window, cx: &mut Context) { - let Some(item) = self.list.read(cx).delegate().selected_item().cloned() else { - return; - }; - self.select_item(&item, window, cx); - } - - fn on_action_escape(&mut self, _: &mut Window, cx: &mut Context) { - self.hide(cx); - } - - fn on_action_up(&mut self, window: &mut Window, cx: &mut Context) { - self.list.update(cx, |this, cx| { - this.on_action_select_prev(&actions::SelectUp, window, cx) - }); - } - - fn on_action_down(&mut self, window: &mut Window, cx: &mut Context) { - self.list.update(cx, |this, cx| { - this.on_action_select_next(&actions::SelectDown, window, cx) - }); - } - - pub(crate) fn is_open(&self) -> bool { - self.open - } - - /// Hide the completion menu and reset the trigger start offset. - pub(crate) fn hide(&mut self, cx: &mut Context) { - self.open = false; - cx.notify(); - } - - pub(crate) fn show( - &mut self, - offset: usize, - items: impl Into>, - window: &mut Window, - cx: &mut Context, - ) { - let items = items.into(); - self.offset = offset; - self.open = true; - self.list.update(cx, |this, cx| { - this.delegate_mut().set_items(items); - this.set_selected_index(Some(IndexPath::new(0)), window, cx); - }); - - cx.notify(); - } - - fn origin(&self, cx: &App) -> Option> { - let state = self.state.read(cx); - let Some(last_layout) = state.last_layout.as_ref() else { - return None; - }; - let Some(cursor_origin) = last_layout.cursor_bounds.map(|b| b.origin) else { - return None; - }; - - let scroll_origin = self.state.read(cx).scroll_handle.offset(); - - Some( - scroll_origin + cursor_origin - state.input_bounds.origin - + Point::new(-px(4.), last_layout.line_height + px(4.)), - ) - } -} - -impl Render for CodeActionMenu { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if !self.open { - return Empty.into_any_element(); - } - - if self.list.read(cx).delegate().items.is_empty() { - self.open = false; - return Empty.into_any_element(); - } - - let Some(pos) = self.origin(cx) else { - return Empty.into_any_element(); - }; - - let max_width = MAX_MENU_WIDTH.min(window.bounds().size.width - pos.x); - - deferred( - editor_popover("code-action-menu", cx) - .absolute() - .left(pos.x) - .top(pos.y) - .max_w(max_width) - .min_w(px(120.)) - .child(List::new(&self.list).max_h(MAX_MENU_HEIGHT)) - .on_mouse_down_out(cx.listener(|this, _, _, cx| { - this.hide(cx); - })), - ) - .into_any_element() - } -} diff --git a/crates/ui/src/input/popovers/completion_menu.rs b/crates/ui/src/input/popovers/completion_menu.rs deleted file mode 100644 index c9a0933..0000000 --- a/crates/ui/src/input/popovers/completion_menu.rs +++ /dev/null @@ -1,446 +0,0 @@ -use std::rc::Rc; - -use gpui::prelude::FluentBuilder; -use gpui::{ - Action, AnyElement, App, AppContext, Context, DismissEvent, Empty, Entity, EventEmitter, - Half as _, HighlightStyle, InteractiveElement as _, IntoElement, ParentElement, Pixels, Point, - Render, RenderOnce, SharedString, Styled, StyledText, Subscription, Window, deferred, div, px, - relative, -}; -use lsp_types::{CompletionItem, CompletionTextEdit}; -use theme::ActiveTheme; - -const MAX_MENU_WIDTH: Pixels = px(320.); -const MAX_MENU_HEIGHT: Pixels = px(240.); -const POPOVER_GAP: Pixels = px(4.); - -use crate::input::popovers::{editor_popover, render_markdown}; -use crate::input::{self, InputState, RopeExt}; -use crate::list::{List, ListDelegate, ListEvent, ListState}; -use crate::{IndexPath, Selectable, actions, h_flex}; - -struct ContextMenuDelegate { - query: SharedString, - menu: Entity, - items: Vec>, - selected_ix: usize, -} - -impl ContextMenuDelegate { - fn set_items(&mut self, items: Vec) { - self.items = items.into_iter().map(Rc::new).collect(); - self.selected_ix = 0; - } - - fn selected_item(&self) -> Option<&Rc> { - self.items.get(self.selected_ix) - } -} - -#[derive(IntoElement)] -struct CompletionMenuItem { - ix: usize, - item: Rc, - children: Vec, - selected: bool, - highlight_prefix: SharedString, -} - -impl CompletionMenuItem { - fn new(ix: usize, item: Rc) -> Self { - Self { - ix, - item, - children: vec![], - selected: false, - highlight_prefix: "".into(), - } - } - - fn highlight_prefix(mut self, s: impl Into) -> Self { - self.highlight_prefix = s.into(); - self - } -} -impl Selectable for CompletionMenuItem { - fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - fn is_selected(&self) -> bool { - self.selected - } -} - -impl ParentElement for CompletionMenuItem { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements); - } -} - -impl RenderOnce for CompletionMenuItem { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let item = self.item; - - let matched_len = item - .filter_text - .as_ref() - .map(|s| s.len()) - .unwrap_or(self.highlight_prefix.len()) - .min(item.label.len()); - - let highlights = vec![( - 0..matched_len, - HighlightStyle { - color: Some(cx.theme().selection), - ..Default::default() - }, - )]; - - h_flex() - .id(self.ix) - .gap_2() - .p_1() - .text_xs() - .line_height(relative(1.)) - .rounded(cx.theme().radius.half()) - .when(item.deprecated.unwrap_or(false), |this| this.line_through()) - .hover(|this| this.bg(cx.theme().secondary_hover)) - .when(self.selected, |this| { - this.bg(cx.theme().secondary_background) - .text_color(cx.theme().secondary_foreground) - }) - .child(div().child(StyledText::new(item.label.clone()).with_highlights(highlights))) - .children(self.children) - } -} - -impl EventEmitter for ContextMenuDelegate {} - -impl ListDelegate for ContextMenuDelegate { - type Item = CompletionMenuItem; - - fn items_count(&self, _: usize, _: &gpui::App) -> usize { - self.items.len() - } - - fn render_item( - &mut self, - ix: crate::IndexPath, - _: &mut Window, - _: &mut Context>, - ) -> Option { - let item = self.items.get(ix.row)?; - Some(CompletionMenuItem::new(ix.row, item.clone()).highlight_prefix(self.query.clone())) - } - - fn set_selected_index( - &mut self, - ix: Option, - _: &mut Window, - cx: &mut Context>, - ) { - self.selected_ix = ix.map(|i| i.row).unwrap_or(0); - cx.notify(); - } - - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { - let Some(item) = self.selected_item() else { - return; - }; - - self.menu.update(cx, |this, cx| { - this.select_item(&item, window, cx); - }); - } -} - -/// A context menu for code completions and code actions. -pub struct CompletionMenu { - offset: usize, - editor: Entity, - list: Entity>, - open: bool, - - /// The offset of the first character that triggered the completion. - pub(crate) trigger_start_offset: Option, - query: SharedString, - _subscriptions: Vec, -} - -impl CompletionMenu { - /// Creates a new `CompletionMenu` with the given offset and completion items. - /// - /// NOTE: This element should not call from InputState::new, unless that will stack overflow. - pub(crate) fn new( - editor: Entity, - window: &mut Window, - cx: &mut App, - ) -> Entity { - cx.new(|cx| { - let view = cx.entity(); - let menu = ContextMenuDelegate { - query: SharedString::default(), - menu: view, - items: vec![], - selected_ix: 0, - }; - - let list = cx.new(|cx| ListState::new(menu, window, cx)); - - let _subscriptions = - vec![ - cx.subscribe(&list, |this: &mut Self, _, ev: &ListEvent, cx| { - match ev { - ListEvent::Confirm(_) => { - this.hide(cx); - } - _ => {} - } - cx.notify(); - }), - ]; - - Self { - offset: 0, - editor, - list, - open: false, - trigger_start_offset: None, - query: SharedString::default(), - _subscriptions, - } - }) - } - - fn select_item(&mut self, item: &CompletionItem, window: &mut Window, cx: &mut Context) { - let offset = self.offset; - let item = item.clone(); - let mut range = self.trigger_start_offset.unwrap_or(self.offset)..self.offset; - - let editor = self.editor.clone(); - - cx.spawn_in(window, async move |_, cx| { - editor.update_in(cx, |editor, window, cx| { - editor.completion_inserting = true; - - let mut new_text = item.label.clone(); - if let Some(text_edit) = item.text_edit.as_ref() { - match text_edit { - CompletionTextEdit::Edit(edit) => { - new_text = edit.new_text.clone(); - range.start = editor.text.position_to_offset(&edit.range.start); - range.end = editor.text.position_to_offset(&edit.range.end); - } - CompletionTextEdit::InsertAndReplace(edit) => { - new_text = edit.new_text.clone(); - range.start = editor.text.position_to_offset(&edit.replace.start); - range.end = editor.text.position_to_offset(&edit.replace.end); - } - } - } else if let Some(insert_text) = item.insert_text.clone() { - new_text = insert_text; - range = offset..offset; - } - - editor.replace_text_in_range_silent( - Some(editor.range_to_utf16(&range)), - &new_text, - window, - cx, - ); - editor.completion_inserting = false; - // FIXME: Input not get the focus - editor.focus(window, cx); - }) - }) - .detach(); - - self.hide(cx); - } - - pub(crate) fn handle_action( - &mut self, - action: Box, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if !self.open { - return false; - } - - cx.propagate(); - if input::Enter::is_primary(&*action) { - self.on_action_enter(window, cx); - } else if action.partial_eq(&input::Escape) { - self.on_action_escape(window, cx); - } else if action.partial_eq(&input::MoveUp) { - self.on_action_up(window, cx); - } else if action.partial_eq(&input::MoveDown) { - self.on_action_down(window, cx); - } else { - return false; - } - - true - } - - fn on_action_enter(&mut self, window: &mut Window, cx: &mut Context) { - let Some(item) = self.list.read(cx).delegate().selected_item().cloned() else { - return; - }; - self.select_item(&item, window, cx); - } - - fn on_action_escape(&mut self, _: &mut Window, cx: &mut Context) { - self.hide(cx); - } - - fn on_action_up(&mut self, window: &mut Window, cx: &mut Context) { - self.list.update(cx, |this, cx| { - this.on_action_select_prev(&actions::SelectUp, window, cx) - }); - } - - fn on_action_down(&mut self, window: &mut Window, cx: &mut Context) { - self.list.update(cx, |this, cx| { - this.on_action_select_next(&actions::SelectDown, window, cx) - }); - } - - pub(crate) fn is_open(&self) -> bool { - self.open - } - - /// Hide the completion menu and reset the trigger start offset. - pub(crate) fn hide(&mut self, cx: &mut Context) { - self.open = false; - self.trigger_start_offset = None; - cx.notify(); - } - - /// Sets the trigger start offset if it is not already set. - pub(crate) fn update_query(&mut self, start_offset: usize, query: impl Into) { - if self.trigger_start_offset.is_none() { - self.trigger_start_offset = Some(start_offset); - } - self.query = query.into(); - } - - pub(crate) fn show( - &mut self, - offset: usize, - items: impl Into>, - window: &mut Window, - cx: &mut Context, - ) { - let items = items.into(); - self.offset = offset; - self.open = true; - self.list.update(cx, |this, cx| { - let longest_ix = items - .iter() - .enumerate() - .max_by_key(|(_, item)| { - item.label.len() + item.detail.as_ref().map(|d| d.len()).unwrap_or(0) - }) - .map(|(ix, _)| ix) - .unwrap_or(0); - - this.delegate_mut().query = self.query.clone(); - this.delegate_mut().set_items(items); - this.set_selected_index(Some(IndexPath::new(0)), window, cx); - this.set_item_to_measure_index(IndexPath::new(longest_ix), window, cx); - }); - - cx.notify(); - } - - fn origin(&self, cx: &App) -> Option> { - let editor = self.editor.read(cx); - let Some(last_layout) = editor.last_layout.as_ref() else { - return None; - }; - let Some(cursor_origin) = last_layout.cursor_bounds.map(|b| b.origin) else { - return None; - }; - - let scroll_origin = self.editor.read(cx).scroll_handle.offset(); - - Some( - scroll_origin + cursor_origin - editor.input_bounds.origin - + Point::new(-px(4.), last_layout.line_height + px(4.)), - ) - } -} - -impl Render for CompletionMenu { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if !self.open { - return Empty.into_any_element(); - } - - if self.list.read(cx).delegate().items.is_empty() { - self.open = false; - return Empty.into_any_element(); - } - - let Some(pos) = self.origin(cx) else { - return Empty.into_any_element(); - }; - - let selected_documentation = self - .list - .read(cx) - .delegate() - .selected_item() - .and_then(|item| item.documentation.clone()); - - let max_width = MAX_MENU_WIDTH.min(window.bounds().size.width - pos.x); - let abs_pos = self.editor.read(cx).input_bounds.origin + pos; - let vertical_layout = - abs_pos.x + MAX_MENU_WIDTH + POPOVER_GAP + MAX_MENU_WIDTH + POPOVER_GAP - > window.bounds().size.width; - - deferred( - div() - .absolute() - .left(pos.x) - .top(pos.y) - .flex() - .flex_row() - .gap(POPOVER_GAP) - .items_start() - .when(vertical_layout, |this| this.flex_col()) - .child( - editor_popover("completion-menu", cx) - .max_w(max_width) - .min_w(px(120.)) - .child(List::new(&self.list).max_h(MAX_MENU_HEIGHT)), - ) - .when_some(selected_documentation, |this, documentation| { - let mut doc = match documentation { - lsp_types::Documentation::String(s) => s.clone(), - lsp_types::Documentation::MarkupContent(mc) => mc.value.clone(), - }; - if vertical_layout { - doc = doc.split("\n").next().unwrap_or_default().to_string(); - } - - this.child( - div().child( - editor_popover("completion-menu", cx) - .w(MAX_MENU_WIDTH) - .px_2() - .child(render_markdown("doc", doc, window, cx)), - ), - ) - }) - .on_mouse_down_out(cx.listener(|this, _, _, cx| { - this.hide(cx); - })), - ) - .into_any_element() - } -} diff --git a/crates/ui/src/input/popovers/context_menu.rs b/crates/ui/src/input/popovers/context_menu.rs deleted file mode 100644 index 980bf91..0000000 --- a/crates/ui/src/input/popovers/context_menu.rs +++ /dev/null @@ -1,142 +0,0 @@ -use gpui::prelude::FluentBuilder as _; -use gpui::{ - Anchor, App, AppContext as _, Context, DismissEvent, Entity, IntoElement, MouseDownEvent, - ParentElement as _, Pixels, Point, Render, Styled, Subscription, Window, anchored, deferred, - div, px, -}; - -use crate::input::popovers::ContextMenu; -use crate::input::{self, InputState}; -use crate::menu::PopupMenu; - -/// Context menu for mouse right clicks. -pub(crate) struct InputContextMenu { - editor: Entity, - menu: Entity, - mouse_position: Point, - open: bool, - - _subscriptions: Vec, -} - -impl InputState { - pub(crate) fn handle_right_click_menu( - &mut self, - event: &MouseDownEvent, - offset: usize, - window: &mut Window, - cx: &mut Context, - ) { - // Show Mouse context menu - if !self.selected_range.contains(offset) { - self.move_to(offset, None, cx); - } - - self.context_menu_content = Some(ContextMenu::RightClick(self.context_menu.clone())); - - let is_code_editor = self.mode.is_code_editor(); - if is_code_editor { - self.handle_hover_definition(offset, window, cx); - } - - let is_enable = !self.disabled; - let has_goto_definition = is_enable && self.lsp.definition_provider.is_some(); - let has_code_action = is_enable && !self.lsp.code_action_providers.is_empty(); - let is_selected = !self.selected_range.is_empty(); - let has_paste = is_enable && cx.read_from_clipboard().is_some(); - - let action_context = self.focus_handle.clone(); - self.context_menu.update(cx, |this, cx| { - this.mouse_position = event.position; - this.menu.update(cx, |menu, cx| { - let new_menu = if let Some(builder) = &self.context_menu_builder { - builder(PopupMenu::new(cx), window, cx) - } else { - PopupMenu::new(cx) - .when(is_code_editor, |m| { - m.menu_with_enable( - "Go to Definition", - Box::new(input::GoToDefinition), - has_goto_definition, - ) - .menu_with_enable( - "Show Code Actions", - Box::new(input::ToggleCodeActions), - has_code_action, - ) - .separator() - }) - .menu_with_enable("Cut", Box::new(input::Cut), is_enable && is_selected) - .menu_with_enable("Copy", Box::new(input::Copy), is_selected) - .menu_with_enable("Paste", Box::new(input::Paste), has_paste) - .separator() - .menu("Select All", Box::new(input::SelectAll)) - }; - - menu.menu_items = new_menu.menu_items; - menu.action_context = Some(action_context); - cx.notify(); - }); - cx.defer_in(window, |this, _, cx| { - this.open = true; - cx.notify(); - }); - }); - } -} - -impl InputContextMenu { - pub(crate) fn new( - editor: Entity, - window: &mut Window, - cx: &mut App, - ) -> Entity { - cx.new(|cx| { - let menu = cx.new(|cx| PopupMenu::new(cx).small()); - - let _subscriptions = vec![cx.subscribe_in(&menu, window, { - move |this: &mut Self, _, _: &DismissEvent, window, cx| { - this.close(window, cx); - } - })]; - - Self { - editor, - menu, - mouse_position: Point::default(), - open: false, - _subscriptions, - } - }) - } - - #[inline] - pub(crate) fn is_open(&self) -> bool { - self.open - } - - #[inline] - pub(crate) fn close(&mut self, window: &mut Window, cx: &mut Context) { - self.open = false; - self.editor.update(cx, |this, cx| { - this.focus(window, cx); - }); - } -} - -impl Render for InputContextMenu { - fn render(&mut self, _: &mut Window, _cx: &mut Context) -> impl IntoElement { - if !self.open { - return div().into_any_element(); - } - - deferred( - anchored() - .snap_to_window_with_margin(px(8.)) - .anchor(Anchor::TopLeft) - .position(self.mouse_position) - .child(div().cursor_default().child(self.menu.clone())), - ) - .into_any_element() - } -} diff --git a/crates/ui/src/input/popovers/diagnostic_popover.rs b/crates/ui/src/input/popovers/diagnostic_popover.rs deleted file mode 100644 index 84631e3..0000000 --- a/crates/ui/src/input/popovers/diagnostic_popover.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::rc::Rc; - -use gpui::{ - prelude::FluentBuilder as _, px, App, AppContext as _, Bounds, Context, Empty, Entity, - IntoElement, Pixels, Point, Render, Styled, Window, -}; - -use crate::{ - highlighter::DiagnosticEntry, - input::{ - popovers::{render_markdown, Popover}, - InputState, - }, -}; - -pub struct DiagnosticPopover { - state: Entity, - pub(crate) diagnostic: Rc, - bounds: Bounds, - open: bool, -} - -impl DiagnosticPopover { - pub fn new( - diagnostic: &DiagnosticEntry, - state: Entity, - cx: &mut App, - ) -> Entity { - let diagnostic = Rc::new(diagnostic.clone()); - - cx.new(|_| Self { - diagnostic, - state, - bounds: Bounds::default(), - open: true, - }) - } - - pub(crate) fn show(&mut self, cx: &mut Context) { - self.open = true; - cx.notify(); - } - - pub(crate) fn hide(&mut self, cx: &mut Context) { - self.open = false; - cx.notify(); - } - - pub(crate) fn check_to_hide(&mut self, mouse_position: Point, cx: &mut Context) { - if !self.open { - return; - } - - let padding = px(5.); - let bounds = Bounds { - origin: self.bounds.origin.map(|v| v - padding), - size: self.bounds.size.map(|v| v + padding * 2.), - }; - - if !bounds.contains(&mouse_position) { - self.hide(cx); - } - } -} - -impl Render for DiagnosticPopover { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - if !self.open { - return Empty.into_any_element(); - } - - let message = self.diagnostic.message.clone(); - - let (border, bg, fg) = ( - self.diagnostic.severity.border(cx), - self.diagnostic.severity.bg(cx), - self.diagnostic.severity.fg(cx), - ); - - Popover::new( - "diagnostic-popover", - self.state.clone(), - self.diagnostic.range.clone(), - move |window, cx| render_markdown("message", message.clone(), window, cx), - ) - .when(!self.open, |this| this.invisible()) - .px_1() - .py_0p5() - .bg(bg) - .text_color(fg) - .border_1() - .border_color(border) - .into_any_element() - } -} diff --git a/crates/ui/src/input/popovers/hover_popover.rs b/crates/ui/src/input/popovers/hover_popover.rs deleted file mode 100644 index 96d2c3b..0000000 --- a/crates/ui/src/input/popovers/hover_popover.rs +++ /dev/null @@ -1,292 +0,0 @@ -use std::{ops::Range, rc::Rc}; - -use gpui::{ - AnyElement, App, AppContext as _, AvailableSpace, Bounds, Element, ElementId, Entity, - InteractiveElement, IntoElement, MouseDownEvent, MouseMoveEvent, ParentElement as _, Pixels, - Render, StatefulInteractiveElement as _, StyleRefinement, Styled, Window, deferred, div, point, - px, -}; - -use crate::{ - StyledExt, - input::{InputState, popovers::render_markdown}, -}; - -pub struct HoverPopover { - editor: Entity, - /// The symbol range byte of the hover trigger. - pub(crate) symbol_range: Range, - pub(crate) hover: Rc, -} - -impl HoverPopover { - pub fn new( - editor: Entity, - symbol_range: Range, - hover: &lsp_types::Hover, - cx: &mut App, - ) -> Entity { - let hover = Rc::new(hover.clone()); - - cx.new(|_| Self { - editor, - symbol_range, - hover, - }) - } - - pub(crate) fn is_same(&self, offset: usize) -> bool { - self.symbol_range.contains(&offset) - } -} - -impl Render for HoverPopover { - fn render(&mut self, _: &mut Window, _: &mut gpui::Context) -> impl IntoElement { - let contents = match self.hover.contents.clone() { - lsp_types::HoverContents::Scalar(scalar) => match scalar { - lsp_types::MarkedString::String(s) => s, - lsp_types::MarkedString::LanguageString(ls) => ls.value, - }, - lsp_types::HoverContents::Array(arr) => arr - .into_iter() - .map(|item| match item { - lsp_types::MarkedString::String(s) => s, - lsp_types::MarkedString::LanguageString(ls) => ls.value, - }) - .collect::>() - .join("\n\n"), - lsp_types::HoverContents::Markup(markup) => markup.value, - }; - - Popover::new( - "hover-popover", - self.editor.clone(), - self.symbol_range.clone(), - move |window, cx| render_markdown("message", contents.clone(), window, cx), - ) - .into_any_element() - } -} - -pub(crate) struct Popover { - id: ElementId, - style: StyleRefinement, - editor: Entity, - range: Range, - width_limit: Range, - content_builder: Box AnyElement>, -} - -impl Styled for Popover { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.style - } -} - -impl Popover { - pub fn new( - id: impl Into, - editor: Entity, - range: Range, - f: F, - ) -> Self - where - F: Fn(&mut Window, &mut App) -> E + 'static, - E: IntoElement, - { - Self { - id: id.into(), - editor, - range, - style: StyleRefinement::default(), - width_limit: px(200.)..px(500.), - content_builder: Box::new(move |window, cx| (f)(window, cx).into_any_element()), - } - } - - /// Get the bounds of the range in the editor, if it is visible. - fn trigger_bounds(&self, cx: &App) -> Option> { - let editor = self.editor.read(cx); - let Some(last_layout) = editor.last_layout.as_ref() else { - return None; - }; - - let Some(last_bounds) = editor.last_bounds else { - return None; - }; - - let (_, _, start_pos) = editor.line_and_position_for_offset(self.range.start); - let (_, _, end_pos) = editor.line_and_position_for_offset(self.range.end); - - let Some(start_pos) = start_pos else { - return None; - }; - let Some(end_pos) = end_pos else { - return None; - }; - - Some(Bounds::from_corners( - last_bounds.origin + start_pos, - last_bounds.origin + end_pos + point(px(0.), last_layout.line_height), - )) - } -} - -impl IntoElement for Popover { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -pub(crate) struct PopoverLayoutState { - bounds: Bounds, - element: Option, -} - -impl Element for Popover { - type RequestLayoutState = PopoverLayoutState; - type PrepaintState = (); - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _: Option<&gpui::GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - let trigger_bounds = match self.trigger_bounds(cx) { - Some(bounds) => bounds, - None => { - return ( - div().into_any_element().request_layout(window, cx), - PopoverLayoutState { - bounds: Bounds::default(), - element: None, - }, - ); - } - }; - - let max_width = self - .width_limit - .end - .min(window.bounds().size.width - SNAP_TO_EDGE * 2) - .max(px(200.)); - let max_height = (window.bounds().size.height - SNAP_TO_EDGE * 2).min(px(320.)); - - let mut popover = deferred( - div() - .id("hover-popover-content") - .flex_none() - .occlude() - .p_1() - .text_xs() - .popover_style(cx) - .shadow_md() - .max_w(max_width) - .max_h(max_height) - .overflow_y_scroll() - .refine_style(&self.style) - .child((self.content_builder)(window, cx)), - ) - .into_any_element(); - - let popover_size = popover.layout_as_root(AvailableSpace::min_size(), window, cx); - const SNAP_TO_EDGE: Pixels = px(8.); - let top_space = trigger_bounds.top() - SNAP_TO_EDGE; - let right_space = window.bounds().size.width - trigger_bounds.left() - SNAP_TO_EDGE; - - let mut pos = point( - trigger_bounds.left(), - trigger_bounds.top() - popover_size.height, - ); - if popover_size.height > top_space { - pos.y = trigger_bounds.bottom(); - } - if popover_size.width > right_space { - pos.x = trigger_bounds.right() - popover_size.width; - } - - let mut empty = div().into_any_element(); - let layout_id = empty.request_layout(window, cx); - ( - layout_id, - PopoverLayoutState { - bounds: Bounds { - origin: pos, - size: popover_size, - }, - element: Some(popover), - }, - ) - } - - fn prepaint( - &mut self, - _: Option<&gpui::GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - _: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - let bounds = request_layout.bounds; - let Some(popover) = request_layout.element.as_mut() else { - return; - }; - - window.with_absolute_element_offset(bounds.origin, |window| { - popover.prepaint(window, cx); - }) - } - - fn paint( - &mut self, - _: Option<&gpui::GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - _: Bounds, - request_layout: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let bounds = request_layout.bounds; - let Some(popover) = request_layout.element.as_mut() else { - return; - }; - - popover.paint(window, cx); - - let editor = self.editor.clone(); - // Mouse down out to hide. - window.on_mouse_event(move |event: &MouseDownEvent, _, _, cx| { - if !bounds.contains(&event.position) { - let _ = editor.update(cx, |editor, cx| { - editor.clear_hover_state(cx); - }); - } - }); - - // Mouse out of trigger + popover bounds - let editor = self.editor.clone(); - let trigger_bounds = self.trigger_bounds(cx).unwrap_or(bounds); - let keep_open_region = trigger_bounds.union(&bounds); - window.on_mouse_event(move |event: &MouseMoveEvent, _, _, cx| { - if !keep_open_region.contains(&event.position) { - let _ = editor.update(cx, |editor, cx| { - editor.clear_hover_state(cx); - }); - } - }) - } -} diff --git a/crates/ui/src/input/popovers/mod.rs b/crates/ui/src/input/popovers/mod.rs deleted file mode 100644 index 597d812..0000000 --- a/crates/ui/src/input/popovers/mod.rs +++ /dev/null @@ -1,41 +0,0 @@ -mod code_action_menu; -mod completion_menu; -mod context_menu; -mod diagnostic_popover; -mod hover_popover; - -pub(crate) use code_action_menu::*; -pub(crate) use completion_menu::*; -pub(crate) use context_menu::*; -pub(crate) use diagnostic_popover::*; -use gpui::{ - App, Div, ElementId, Entity, InteractiveElement as _, IntoElement, SharedString, Stateful, - StyleRefinement, Styled as _, Window, div, px, rems, -}; -pub(crate) use hover_popover::*; - -use crate::StyledExt as _; - -pub(crate) enum ContextMenu { - Completion(Entity), - CodeAction(Entity), - RightClick(Entity), -} - -impl ContextMenu { - pub(crate) fn is_open(&self, cx: &App) -> bool { - match self { - ContextMenu::Completion(menu) => menu.read(cx).is_open(), - ContextMenu::CodeAction(menu) => menu.read(cx).is_open(), - ContextMenu::RightClick(menu) => menu.read(cx).is_open(), - } - } - - pub(crate) fn render(&self) -> impl IntoElement { - match self { - ContextMenu::Completion(menu) => menu.clone().into_any_element(), - ContextMenu::CodeAction(menu) => menu.clone().into_any_element(), - ContextMenu::RightClick(menu) => menu.clone().into_any_element(), - } - } -} diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 6fdd4ff..429ac86 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -99,9 +99,6 @@ actions!( MoveToPreviousWord, MoveToNextWord, Escape, - ToggleCodeActions, - Search, - GoToDefinition, ] ); @@ -250,14 +247,6 @@ pub(crate) fn init(cx: &mut App) { KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-y", Redo, Some(CONTEXT)), - #[cfg(target_os = "macos")] - KeyBinding::new("cmd-.", ToggleCodeActions, Some(CONTEXT)), - #[cfg(not(target_os = "macos"))] - KeyBinding::new("ctrl-.", ToggleCodeActions, Some(CONTEXT)), - #[cfg(target_os = "macos")] - KeyBinding::new("cmd-f", Search, Some(CONTEXT)), - #[cfg(not(target_os = "macos"))] - KeyBinding::new("ctrl-f", Search, Some(CONTEXT)), ]); } @@ -355,7 +344,6 @@ pub struct InputState { pub(super) clean_on_escape: bool, pub(super) submit_on_enter: bool, pub(super) soft_wrap: bool, - pub(super) show_whitespaces: bool, /// This flag tells the renderer to prefer the end of the current visual line. pub(crate) cursor_line_end_affinity: bool, pub(super) pattern: Option, @@ -395,7 +383,7 @@ impl InputState { pub fn new(window: &mut Window, cx: &mut Context) -> Self { let focus_handle = cx.focus_handle().tab_stop(true); let blink_cursor = cx.new(|_| BlinkCursor::new()); - let history = History::new().group_interval(std::time::Duration::from_secs(1)); + let history = History::new().group_interval(instant::Duration::from_secs(1)); let _subscriptions = vec![ // Observe the blink cursor to repaint the view when it changes. @@ -435,7 +423,6 @@ impl InputState { clean_on_escape: false, submit_on_enter: false, soft_wrap: true, - show_whitespaces: false, loading: false, pattern: None, validate: None, @@ -480,33 +467,6 @@ impl InputState { self } - /// Set Input to use [`InputMode::CodeEditor`] mode. - /// - /// Default options: - /// - /// - line_number: true - /// - tab_size: 2 - /// - hard_tabs: false - /// - height: 100% - /// - multi_line: true - /// - indent_guides: true - /// - /// If `highlighter` is None, will use the default highlighter. - /// - /// Code Editor aim for help used to simple code editing or display, not a full-featured code editor. - /// - /// ## Features - /// - /// - Syntax Highlighting - /// - Auto Indent - /// - Line Number - /// - Large Text support, up to 50K lines. - pub fn code_editor(mut self, language: impl Into) -> Self { - let language: SharedString = language.into(); - self.mode = InputMode::code_editor(language); - self - } - /// Set whether search UI allows replacement, default is true. pub fn replaceable(mut self, allow: bool) -> Self { self.replaceable = allow; @@ -519,49 +479,6 @@ impl InputState { self } - /// Set enable/disable code folding, only for [`InputMode::CodeEditor`] mode. - /// - /// Default: true - pub fn folding(mut self, folding: bool) -> Self { - debug_assert!(self.mode.is_code_editor()); - if let InputMode::CodeEditor { folding: f, .. } = &mut self.mode { - *f = folding; - } - self - } - - /// Set code folding at runtime, only for [`InputMode::CodeEditor`] mode. - /// - /// When disabling, all existing folds are cleared. - pub fn set_folding(&mut self, folding: bool, _: &mut Window, cx: &mut Context) { - debug_assert!(self.mode.is_code_editor()); - if let InputMode::CodeEditor { folding: f, .. } = &mut self.mode { - *f = folding; - } - if !folding { - self.display_map.clear_folds(); - } - cx.notify(); - } - - /// Set enable/disable line number, only for [`InputMode::CodeEditor`] mode. - pub fn line_number(mut self, line_number: bool) -> Self { - debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line()); - if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode { - *l = line_number; - } - self - } - - /// Set line number, only for [`InputMode::CodeEditor`] mode. - pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context) { - debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line()); - if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode { - *l = line_number; - } - cx.notify(); - } - /// Set the number of rows for the multi-line Textarea. /// /// This is only used when `multi_line` is set to true. @@ -569,9 +486,7 @@ impl InputState { /// default: 2 pub fn rows(mut self, rows: usize) -> Self { match &mut self.mode { - InputMode::PlainText { rows: r, .. } | InputMode::CodeEditor { rows: r, .. } => { - *r = rows - } + InputMode::PlainText { rows: r, .. } => *r = rows, InputMode::AutoGrow { max_rows: max_r, rows: r, @@ -584,31 +499,6 @@ impl InputState { self } - /// Set highlighter language for for [`InputMode::CodeEditor`] mode. - pub fn set_highlighter( - &mut self, - new_language: impl Into, - cx: &mut Context, - ) { - if let InputMode::CodeEditor { - language, - parse_task, - .. - } = &mut self.mode - { - *language = new_language.into(); - parse_task.borrow_mut().take(); - } - cx.notify(); - } - - fn reset_highlighter(&mut self, cx: &mut Context) { - if let InputMode::CodeEditor { parse_task, .. } = &mut self.mode { - parse_task.borrow_mut().take(); - } - cx.notify(); - } - /// Set placeholder pub fn set_placeholder( &mut self, @@ -726,7 +616,6 @@ impl InputState { let text: SharedString = text.into(); let range = 0..self.text.chars().map(|c| c.len_utf16()).sum(); self.replace_text_in_range_silent(Some(range), &text, window, cx); - self.reset_highlighter(cx); self.disabled = was_disabled; } @@ -779,12 +668,6 @@ impl InputState { self } - /// Set whether to show whitespace characters. - pub fn show_whitespaces(mut self, show: bool) -> Self { - self.show_whitespaces = show; - self - } - /// Update the soft wrap mode for multi-line input, default is true. pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context) { debug_assert!(self.mode.is_multi_line()); @@ -808,12 +691,6 @@ impl InputState { cx.notify(); } - /// Update whether to show whitespace characters. - pub fn set_show_whitespaces(&mut self, show: bool, _: &mut Window, cx: &mut Context) { - self.show_whitespaces = show; - cx.notify(); - } - /// Set the regular expression pattern of the input field. /// /// Only for [`InputMode::SingleLine`] mode. @@ -1038,7 +915,7 @@ impl InputState { let row = self.text.offset_to_point(self.cursor()).row; let logical_start = self.text.line_start_offset(row); - if self.soft_wrap && self.mode.is_code_editor() { + if self.soft_wrap { let wrap_point = self.display_map.offset_to_wrap_display_point(self.cursor()); if let Some(line) = self.display_map.lines().get(row) && let Some(range) = line.wrapped_lines.get(wrap_point.local_row) @@ -1066,7 +943,7 @@ impl InputState { let logical_start = self.text.line_start_offset(row); let logical_end = self.text.line_end_offset(row); - if self.soft_wrap && self.mode.is_code_editor() { + if self.soft_wrap { let wrap_point = self.display_map.offset_to_wrap_display_point(self.cursor()); if let Some(line) = self.display_map.lines().get(row) && let Some(range) = line.wrapped_lines.get(wrap_point.local_row) @@ -1264,7 +1141,7 @@ impl InputState { if insert_newline { // Get current line indent - let indent = if self.mode.is_code_editor() { + let indent = if self.mode.is_indentable() { self.indent_of_next_line() } else { "".to_string() @@ -1465,11 +1342,7 @@ impl InputState { // Check if row_offset_y is out of the viewport // If row offset is not in the viewport, scroll to make it visible - let edge_height = if direction.is_some() && self.mode.is_code_editor() { - 3 * line_height - } else { - line_height - }; + let edge_height = line_height; if row_offset_y - edge_height + line_height < -scroll_offset.y { // Scroll up scroll_offset.y = -row_offset_y + edge_height - line_height; @@ -1749,34 +1622,6 @@ impl InputState { self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end) } - /// If offset falls on a hidden (folded) line, clamp backward to the end of - /// the fold header line (last visible position before the fold). - fn clamp_offset_to_visible_backward(&self, offset: usize) -> usize { - let line = self.text.offset_to_point(offset).row; - if self.display_map.is_buffer_line_hidden(line) { - for fold in self.display_map.folded_ranges() { - if line > fold.start_line && line <= fold.end_line { - return self.text.line_end_offset(fold.start_line); - } - } - } - offset - } - - /// If offset falls on a hidden (folded) line, clamp forward to the start of - /// the fold end line (first visible position after the fold). - fn clamp_offset_to_visible_forward(&self, offset: usize) -> usize { - let line = self.text.offset_to_point(offset).row; - if self.display_map.is_buffer_line_hidden(line) { - for fold in self.display_map.folded_ranges() { - if line > fold.start_line && line <= fold.end_line { - return self.text.line_start_offset(fold.end_line); - } - } - } - offset - } - pub(super) fn previous_boundary(&self, offset: usize) -> usize { let mut offset = self.text.clip_offset(offset.saturating_sub(1), Bias::Left); if let Some(ch) = self.text.char_at(offset) @@ -1785,7 +1630,7 @@ impl InputState { offset -= 1; } - self.clamp_offset_to_visible_backward(offset) + offset } pub(super) fn next_boundary(&self, offset: usize) -> usize { @@ -1796,7 +1641,7 @@ impl InputState { offset += 1; } - self.clamp_offset_to_visible_forward(offset) + offset } /// Returns the true to let InputElement to render cursor, when Input is focused and current BlinkCursor is visible. diff --git a/crates/ui/src/list/list.rs b/crates/ui/src/list/list.rs index d0ab462..8b2d4d6 100644 --- a/crates/ui/src/list/list.rs +++ b/crates/ui/src/list/list.rs @@ -1,5 +1,4 @@ use std::ops::Range; -use std::time::Duration; use gpui::prelude::FluentBuilder; use gpui::{ @@ -9,6 +8,7 @@ use gpui::{ SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, UniformListScrollHandle, Window, div, px, size, uniform_list, }; +use instant::Duration; use theme::ActiveTheme; use crate::actions::{Cancel, Confirm, SelectDown, SelectUp}; diff --git a/crates/ui/src/modal.rs b/crates/ui/src/modal.rs index afa2b75..b93bfc0 100644 --- a/crates/ui/src/modal.rs +++ b/crates/ui/src/modal.rs @@ -1,5 +1,5 @@ use std::rc::Rc; -use std::time::Duration; +use instant::Duration; use gpui::prelude::FluentBuilder; use gpui::{ diff --git a/crates/ui/src/notification.rs b/crates/ui/src/notification.rs index 4779a37..b297d4e 100644 --- a/crates/ui/src/notification.rs +++ b/crates/ui/src/notification.rs @@ -1,7 +1,7 @@ use std::any::TypeId; use std::collections::{HashMap, VecDeque}; use std::rc::Rc; -use std::time::Duration; +use instant::Duration; use gpui::prelude::FluentBuilder; use gpui::{ diff --git a/crates/ui/src/resizable/resize_handle.rs b/crates/ui/src/resizable/resize_handle.rs index e982c9f..2cbd353 100644 --- a/crates/ui/src/resizable/resize_handle.rs +++ b/crates/ui/src/resizable/resize_handle.rs @@ -117,7 +117,7 @@ impl Element for ResizeHandle { cx.theme().border }; - let mut el = div() + let mut ele = div() .id(self.id.clone()) .occlude() .absolute() @@ -160,16 +160,15 @@ impl Element for ResizeHandle { }) .child( div() - .bg(bg_color) .group_hover("handle", |this| this.bg(bg_color)) .when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE)) .when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)), ) .into_any_element(); - let layout_id = el.request_layout(window, cx); + let layout_id = ele.request_layout(window, cx); - ((layout_id, el), state) + ((layout_id, ele), state) }) } diff --git a/crates/ui/src/scroll/scrollbar.rs b/crates/ui/src/scroll/scrollbar.rs index da946fb..bce26e9 100644 --- a/crates/ui/src/scroll/scrollbar.rs +++ b/crates/ui/src/scroll/scrollbar.rs @@ -2,7 +2,6 @@ use std::cell::Cell; use std::ops::Deref; use std::panic::Location; use std::rc::Rc; -use std::time::{Duration, Instant}; use gpui::{ Anchor, App, Axis, BorderStyle, Bounds, ContentMask, CursorStyle, Edges, Element, ElementId, @@ -11,6 +10,7 @@ use gpui::{ Position, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, fill, point, px, relative, size, }; +use instant::{Duration, Instant}; use theme::{ActiveTheme, AxisExt, ScrollbarMode}; /// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH) @@ -157,7 +157,7 @@ impl ScrollbarStateInner { let mut state = *self; state.hovered_axis = axis; if axis.is_some() { - state.last_scroll_time = Some(std::time::Instant::now()); + state.last_scroll_time = Some(instant::Instant::now()); } state } @@ -166,7 +166,7 @@ impl ScrollbarStateInner { let mut state = *self; state.hovered_on_thumb = axis; if self.is_scrollbar_visible() && axis.is_some() { - state.last_scroll_time = Some(std::time::Instant::now()); + state.last_scroll_time = Some(instant::Instant::now()); } state } diff --git a/crates/ui/src/skeleton.rs b/crates/ui/src/skeleton.rs index 441296d..ebb2bc8 100644 --- a/crates/ui/src/skeleton.rs +++ b/crates/ui/src/skeleton.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use instant::Duration; use gpui::{ bounce, div, ease_in_out, Animation, AnimationExt, IntoElement, RenderOnce, StyleRefinement, diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index 8f3f981..45d04b6 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -87,7 +87,7 @@ pub trait StyledExt: Styled + Sized { .border_1() .border_color(cx.theme().border) .shadow_md() - .rounded(cx.theme().radius) + .rounded(cx.theme().radius_lg) } } diff --git a/crates/ui/src/switch.rs b/crates/ui/src/switch.rs index 70aa3b6..375da35 100644 --- a/crates/ui/src/switch.rs +++ b/crates/ui/src/switch.rs @@ -1,6 +1,6 @@ use std::cell::RefCell; use std::rc::Rc; -use std::time::Duration; +use instant::Duration; use gpui::prelude::FluentBuilder as _; use gpui::{ diff --git a/crates/ui/src/tab/mod.rs b/crates/ui/src/tab/mod.rs index 25d0025..0bf2665 100644 --- a/crates/ui/src/tab/mod.rs +++ b/crates/ui/src/tab/mod.rs @@ -2,396 +2,15 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ - AnyElement, App, ClickEvent, Div, Edges, Hsla, InteractiveElement, IntoElement, MouseButton, - ParentElement, Pixels, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, - div, px, relative, + AnyElement, App, ClickEvent, Div, InteractiveElement, IntoElement, MouseButton, ParentElement, + RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, px, relative, }; -use theme::ActiveTheme; +use theme::{ActiveTheme, TABBAR_HEIGHT}; -use crate::{Icon, IconName, Selectable, Sizable, Size, StyledExt, h_flex}; +use crate::{Icon, IconName, Selectable, h_flex}; pub mod tab_bar; -/// Tab variants. -#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)] -pub enum TabVariant { - #[default] - Tab, - Outline, - Pill, - Segmented, - Underline, -} - -impl TabVariant { - fn height(&self, size: Size) -> Pixels { - match size { - Size::XSmall => match self { - TabVariant::Underline => px(26.), - _ => px(20.), - }, - Size::Small => match self { - TabVariant::Underline => px(30.), - _ => px(24.), - }, - Size::Large => match self { - TabVariant::Underline => px(44.), - _ => px(36.), - }, - _ => match self { - TabVariant::Underline => px(36.), - _ => px(32.), - }, - } - } - - fn inner_height(&self, size: Size) -> Pixels { - match size { - Size::XSmall => match self { - TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(18.), - TabVariant::Segmented => px(16.), - TabVariant::Underline => px(20.), - }, - Size::Small => match self { - TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(22.), - TabVariant::Segmented => px(18.), - TabVariant::Underline => px(22.), - }, - Size::Large => match self { - TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(36.), - TabVariant::Segmented => px(28.), - TabVariant::Underline => px(32.), - }, - _ => match self { - TabVariant::Tab => px(30.), - TabVariant::Outline | TabVariant::Pill => px(26.), - TabVariant::Segmented => px(24.), - TabVariant::Underline => px(26.), - }, - } - } - - /// Default px(12) to match panel px_3, See [`crate::dock::TabPanel`] - fn inner_paddings(&self, size: Size) -> Edges { - let mut padding_x = match size { - Size::XSmall => px(8.), - Size::Small => px(10.), - Size::Large => px(16.), - _ => px(12.), - }; - - if matches!(self, TabVariant::Underline) { - padding_x = px(0.); - } - - Edges { - left: padding_x, - right: padding_x, - ..Default::default() - } - } - - fn inner_margins(&self, size: Size) -> Edges { - match size { - Size::XSmall => match self { - TabVariant::Underline => Edges { - top: px(1.), - bottom: px(2.), - ..Default::default() - }, - _ => Edges::all(px(0.)), - }, - Size::Small => match self { - TabVariant::Underline => Edges { - top: px(2.), - bottom: px(3.), - ..Default::default() - }, - _ => Edges::all(px(0.)), - }, - Size::Large => match self { - TabVariant::Underline => Edges { - top: px(5.), - bottom: px(6.), - ..Default::default() - }, - _ => Edges::all(px(0.)), - }, - _ => match self { - TabVariant::Underline => Edges { - top: px(3.), - bottom: px(4.), - ..Default::default() - }, - _ => Edges::all(px(0.)), - }, - } - } - - fn normal(&self, cx: &App) -> TabStyle { - match self { - TabVariant::Tab => TabStyle { - fg: cx.theme().tab_foreground, - bg: gpui::transparent_black(), - borders: Edges { - left: px(1.), - right: px(1.), - ..Default::default() - }, - border_color: gpui::transparent_black(), - ..Default::default() - }, - TabVariant::Outline => TabStyle { - fg: cx.theme().tab_foreground, - bg: gpui::transparent_black(), - borders: Edges::all(px(1.)), - border_color: cx.theme().border, - ..Default::default() - }, - TabVariant::Pill => TabStyle { - fg: cx.theme().text, - bg: gpui::transparent_black(), - ..Default::default() - }, - TabVariant::Segmented => TabStyle { - fg: cx.theme().tab_foreground, - bg: gpui::transparent_black(), - ..Default::default() - }, - TabVariant::Underline => TabStyle { - fg: cx.theme().tab_foreground, - bg: gpui::transparent_black(), - inner_bg: gpui::transparent_black(), - borders: Edges { - bottom: px(2.), - ..Default::default() - }, - border_color: gpui::transparent_black(), - ..Default::default() - }, - } - } - - fn hovered(&self, selected: bool, cx: &App) -> TabStyle { - match self { - TabVariant::Tab => TabStyle { - fg: cx.theme().tab_foreground, - bg: gpui::transparent_black(), - borders: Edges { - left: px(1.), - right: px(1.), - ..Default::default() - }, - border_color: gpui::transparent_black(), - ..Default::default() - }, - TabVariant::Outline => TabStyle { - fg: cx.theme().secondary_foreground, - bg: cx.theme().secondary_hover, - borders: Edges::all(px(1.)), - border_color: cx.theme().border, - ..Default::default() - }, - TabVariant::Pill => TabStyle { - fg: cx.theme().secondary_foreground, - bg: cx.theme().secondary_background, - ..Default::default() - }, - TabVariant::Segmented => TabStyle { - fg: cx.theme().tab_foreground, - bg: gpui::transparent_black(), - inner_bg: if selected { - cx.theme().background - } else { - gpui::transparent_black() - }, - ..Default::default() - }, - TabVariant::Underline => TabStyle { - fg: cx.theme().tab_foreground, - bg: gpui::transparent_black(), - inner_bg: gpui::transparent_black(), - borders: Edges { - bottom: px(2.), - ..Default::default() - }, - border_color: gpui::transparent_black(), - ..Default::default() - }, - } - } - - fn selected(&self, cx: &App) -> TabStyle { - match self { - TabVariant::Tab => TabStyle { - fg: cx.theme().tab_active_foreground, - bg: cx.theme().tab_active_background, - borders: Edges { - left: px(1.), - right: px(1.), - ..Default::default() - }, - border_color: cx.theme().border, - ..Default::default() - }, - TabVariant::Outline => TabStyle { - fg: cx.theme().text_accent, - bg: gpui::transparent_black(), - borders: Edges::all(px(1.)), - border_color: cx.theme().element_active, - ..Default::default() - }, - TabVariant::Pill => TabStyle { - fg: cx.theme().element_foreground, - bg: cx.theme().element_background, - ..Default::default() - }, - TabVariant::Segmented => TabStyle { - fg: cx.theme().tab_active_foreground, - bg: gpui::transparent_black(), - inner_bg: cx.theme().background, - shadow: true, - ..Default::default() - }, - TabVariant::Underline => TabStyle { - fg: cx.theme().tab_active_foreground, - bg: gpui::transparent_black(), - borders: Edges { - bottom: px(2.), - ..Default::default() - }, - border_color: cx.theme().element_active, - ..Default::default() - }, - } - } - - fn disabled(&self, selected: bool, cx: &App) -> TabStyle { - match self { - TabVariant::Tab => TabStyle { - fg: cx.theme().text_muted, - bg: gpui::transparent_black(), - border_color: if selected { - cx.theme().border - } else { - gpui::transparent_black() - }, - borders: Edges { - left: px(1.), - right: px(1.), - ..Default::default() - }, - ..Default::default() - }, - TabVariant::Outline => TabStyle { - fg: cx.theme().text_muted, - bg: gpui::transparent_black(), - borders: Edges::all(px(1.)), - border_color: if selected { - cx.theme().element_active - } else { - cx.theme().border - }, - ..Default::default() - }, - TabVariant::Pill => TabStyle { - fg: if selected { - cx.theme().element_foreground.opacity(0.5) - } else { - cx.theme().text_muted - }, - bg: if selected { - cx.theme().element_background.opacity(0.5) - } else { - gpui::transparent_black() - }, - ..Default::default() - }, - TabVariant::Segmented => TabStyle { - fg: cx.theme().text_muted, - bg: cx.theme().tab_background, - inner_bg: if selected { - cx.theme().background - } else { - gpui::transparent_black() - }, - ..Default::default() - }, - TabVariant::Underline => TabStyle { - fg: cx.theme().text_muted, - bg: gpui::transparent_black(), - border_color: if selected { - cx.theme().border - } else { - gpui::transparent_black() - }, - borders: Edges { - bottom: px(2.), - ..Default::default() - }, - ..Default::default() - }, - } - } - - pub(super) fn tab_bar_radius(&self, size: Size, cx: &App) -> Pixels { - if *self != TabVariant::Segmented { - return px(0.); - } - - match size { - Size::XSmall | Size::Small => cx.theme().radius, - Size::Large => cx.theme().radius_lg, - _ => cx.theme().radius_lg, - } - } - - fn radius(&self, size: Size, cx: &App) -> Pixels { - match self { - TabVariant::Outline | TabVariant::Pill => px(99.), - TabVariant::Segmented => match size { - Size::XSmall | Size::Small => cx.theme().radius, - Size::Large => cx.theme().radius_lg, - _ => cx.theme().radius_lg, - }, - _ => px(0.), - } - } - - fn inner_radius(&self, size: Size, cx: &App) -> Pixels { - match self { - TabVariant::Segmented => match size { - Size::Large => self.tab_bar_radius(size, cx) - px(3.), - _ => self.tab_bar_radius(size, cx) - px(2.), - }, - _ => px(0.), - } - } -} - -#[allow(dead_code)] -struct TabStyle { - borders: Edges, - border_color: Hsla, - bg: Hsla, - fg: Hsla, - shadow: bool, - inner_bg: Hsla, -} - -impl Default for TabStyle { - fn default() -> Self { - TabStyle { - borders: Edges::all(px(0.)), - border_color: gpui::transparent_white(), - bg: gpui::transparent_white(), - fg: gpui::transparent_white(), - shadow: false, - inner_bg: gpui::transparent_white(), - } - } -} - #[allow(clippy::type_complexity)] /// A Tab element for the [`super::TabBar`]. #[derive(IntoElement)] @@ -404,8 +23,6 @@ pub struct Tab { pub(super) tab_bar_prefix: Option, suffix: Option, children: Vec, - variant: TabVariant, - size: Size, pub(super) disabled: bool, pub(super) selected: bool, on_click: Option>, @@ -454,8 +71,6 @@ impl Default for Tab { selected: false, prefix: None, suffix: None, - variant: TabVariant::default(), - size: Size::default(), on_click: None, } } @@ -479,36 +94,6 @@ impl Tab { self } - /// Set Tab Variant. - pub fn with_variant(mut self, variant: TabVariant) -> Self { - self.variant = variant; - self - } - - /// Use Pill variant. - pub fn pill(mut self) -> Self { - self.variant = TabVariant::Pill; - self - } - - /// Use outline variant. - pub fn outline(mut self) -> Self { - self.variant = TabVariant::Outline; - self - } - - /// Use Segmented variant. - pub fn segmented(mut self) -> Self { - self.variant = TabVariant::Segmented; - self - } - - /// Use Underline variant. - pub fn underline(mut self) -> Self { - self.variant = TabVariant::Underline; - self - } - /// Set the left side of the tab pub fn prefix(mut self, prefix: impl IntoElement) -> Self { self.prefix = Some(prefix.into_any_element()); @@ -580,42 +165,16 @@ impl Styled for Tab { } } -impl Sizable for Tab { - fn with_size(mut self, size: impl Into) -> Self { - self.size = size.into(); - self - } -} - impl RenderOnce for Tab { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let mut tab_style = if self.selected { - self.variant.selected(cx) + let fg = if self.disabled { + cx.theme().text_muted + } else if self.selected { + cx.theme().tab_active_foreground } else { - self.variant.normal(cx) + cx.theme().tab_foreground }; - let mut hover_style = self.variant.hovered(self.selected, cx); - - if self.disabled { - tab_style = self.variant.disabled(self.selected, cx); - hover_style = self.variant.disabled(self.selected, cx); - } - - let tab_bar_prefix = self.tab_bar_prefix.unwrap_or_default(); - - if !tab_bar_prefix && self.ix == 0 && self.variant == TabVariant::Tab { - tab_style.borders.left = px(0.); - hover_style.borders.left = px(0.); - } - - let radius = self.variant.radius(self.size, cx); - let inner_radius = self.variant.inner_radius(self.size, cx); - let inner_paddings = self.variant.inner_paddings(self.size); - let inner_margins = self.variant.inner_margins(self.size); - let inner_height = self.variant.inner_height(self.size); - let height = self.variant.height(self.size); - self.base .id(self.ix) .flex() @@ -623,74 +182,39 @@ impl RenderOnce for Tab { .gap_1() .items_center() .flex_shrink_0() - .h(height) + .h(TABBAR_HEIGHT) .overflow_hidden() - .text_color(tab_style.fg) - .map(|this| match self.size { - Size::XSmall => this.text_xs(), - Size::Large => this.text_base(), - _ => this.text_sm(), - }) - .bg(tab_style.bg) - .border_l(tab_style.borders.left) - .border_r(tab_style.borders.right) - .border_t(tab_style.borders.top) - .border_b(tab_style.borders.bottom) - .border_color(tab_style.border_color) - .rounded(radius) + .text_color(fg) + .text_sm() .when(!self.selected && !self.disabled, |this| { - this.hover(|this| { - this.text_color(hover_style.fg) - .bg(hover_style.bg) - .border_l(hover_style.borders.left) - .border_r(hover_style.borders.right) - .border_t(hover_style.borders.top) - .border_b(hover_style.borders.bottom) - .border_color(hover_style.border_color) - .rounded(radius) - }) + this.hover(|this| this.text_color(cx.theme().secondary_foreground)) }) .when_some(self.prefix, |this, prefix| this.child(prefix)) .child( h_flex() .flex_1() - .h(inner_height) + .h(px(30.)) .line_height(relative(1.)) .whitespace_nowrap() .items_center() .justify_center() .overflow_hidden() - .margins(inner_margins) .flex_shrink_0() + .px_3() .map(|this| match self.icon { - Some(icon) => { - this.w(inner_height * 1.25) - .child(icon.map(|this| match self.size { - Size::XSmall => this.size_2p5(), - Size::Small => this.size_3p5(), - Size::Large => this.size_4(), - _ => this.size_4(), - })) - } + Some(icon) => this.w(px(38.)).child(icon.size_4()), None => this - .paddings(inner_paddings) .map(|this| match self.label { Some(label) => this.child(label), None => this, }) .children(self.children), - }) - .bg(tab_style.inner_bg) - .rounded(inner_radius) - .when(tab_style.shadow, |this| this.shadow_xs()) - .hover(|this| this.bg(hover_style.inner_bg).rounded(inner_radius)), + }), ) .when_some(self.suffix, |this, suffix| { this.child(div().pr_2().child(suffix)) }) - .on_mouse_down(MouseButton::Left, |_, _, cx| { - // Stop propagation behavior, for works on TitleBar. - // https://github.com/longbridge/gpui-component/issues/1836 + .on_mouse_down(MouseButton::Left, |_ev, _window, cx| { cx.stop_propagation(); }) .when(!self.disabled, |this| { diff --git a/crates/ui/src/tab/tab_bar.rs b/crates/ui/src/tab/tab_bar.rs index 71e6036..11a6463 100644 --- a/crates/ui/src/tab/tab_bar.rs +++ b/crates/ui/src/tab/tab_bar.rs @@ -2,19 +2,17 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ - Anchor, AnyElement, App, Div, Edges, ElementId, InteractiveElement, IntoElement, ParentElement, + Anchor, AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, ScrollHandle, Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled, Window, div, px, }; use smallvec::SmallVec; -use theme::ActiveTheme; -use super::{Tab, TabVariant}; +use super::Tab; use crate::button::{Button, ButtonVariants as _}; use crate::menu::{DropdownMenu as _, PopupMenuItem}; -use crate::{IconName, Selectable, Sizable, Size, StyledExt, h_flex}; +use crate::{IconName, Selectable, Sizable, StyledExt, h_flex}; -#[allow(clippy::type_complexity)] /// A TabBar element that contains multiple [`Tab`] items. #[derive(IntoElement)] pub struct TabBar { @@ -26,9 +24,8 @@ pub struct TabBar { children: SmallVec<[Tab; 2]>, last_empty_space: AnyElement, selected_index: Option, - variant: TabVariant, - size: Size, menu: bool, + #[allow(clippy::type_complexity)] on_click: Option>, } @@ -42,8 +39,6 @@ impl TabBar { scroll_handle: None, prefix: None, suffix: None, - variant: TabVariant::default(), - size: Size::default(), last_empty_space: div().w_3().into_any_element(), selected_index: None, on_click: None, @@ -51,36 +46,6 @@ impl TabBar { } } - /// Set the Tab variant, all children will inherit the variant. - pub fn with_variant(mut self, variant: TabVariant) -> Self { - self.variant = variant; - self - } - - /// Set the Tab variant to Pill, all children will inherit the variant. - pub fn pill(mut self) -> Self { - self.variant = TabVariant::Pill; - self - } - - /// Set the Tab variant to Outline, all children will inherit the variant. - pub fn outline(mut self) -> Self { - self.variant = TabVariant::Outline; - self - } - - /// Set the Tab variant to Segmented, all children will inherit the variant. - pub fn segmented(mut self) -> Self { - self.variant = TabVariant::Segmented; - self - } - - /// Set the Tab variant to Underline, all children will inherit the variant. - pub fn underline(mut self) -> Self { - self.variant = TabVariant::Underline; - self - } - /// Set whether to show the menu button when tabs overflow, default is false. pub fn menu(mut self, menu: bool) -> Self { self.menu = menu; @@ -105,13 +70,13 @@ impl TabBar { self } - /// Add children of the TabBar, all children will inherit the variant. + /// Add children of the TabBar. pub fn children(mut self, children: impl IntoIterator>) -> Self { self.children.extend(children.into_iter().map(Into::into)); self } - /// Add child of the TabBar, tab will inherit the variant. + /// Add child of the TabBar. pub fn child(mut self, child: impl Into) -> Self { self.children.push(child.into()); self @@ -147,60 +112,8 @@ impl Styled for TabBar { } } -impl Sizable for TabBar { - fn with_size(mut self, size: impl Into) -> Self { - self.size = size.into(); - self - } -} - impl RenderOnce for TabBar { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let default_gap = match self.size { - Size::Small | Size::XSmall => px(8.), - Size::Large => px(16.), - _ => px(12.), - }; - let (bg, paddings, gap) = match self.variant { - TabVariant::Tab => { - let padding = Edges::all(px(0.)); - (cx.theme().tab_background, padding, px(0.)) - } - TabVariant::Outline => { - let padding = Edges::all(px(0.)); - (gpui::transparent_black(), padding, default_gap) - } - TabVariant::Pill => { - let padding = Edges::all(px(0.)); - (gpui::transparent_black(), padding, px(4.)) - } - TabVariant::Segmented => { - let padding_x = match self.size { - Size::XSmall => px(2.), - Size::Small => px(3.), - _ => px(4.), - }; - let padding = Edges { - left: padding_x, - right: padding_x, - ..Default::default() - }; - - (cx.theme().tab_background, padding, px(2.)) - } - TabVariant::Underline => { - // This gap is same as the tab inner_paddings - let gap = match self.size { - Size::XSmall => px(10.), - Size::Small => px(12.), - Size::Large => px(20.), - _ => px(16.), - }; - - (gpui::transparent_black(), Edges::all(px(0.)), gap) - } - }; - + fn render(self, _: &mut Window, _cx: &mut App) -> impl IntoElement { let mut item_labels = Vec::new(); let selected_index = self.selected_index; let on_click = self.on_click.clone(); @@ -210,25 +123,6 @@ impl RenderOnce for TabBar { .relative() .flex() .items_center() - .bg(bg) - .text_color(cx.theme().tab_foreground) - .when( - self.variant == TabVariant::Underline || self.variant == TabVariant::Tab, - |this| { - this.child( - div() - .id("border-b") - .absolute() - .left_0() - .bottom_0() - .size_full() - .border_b_1() - .border_color(cx.theme().border), - ) - }, - ) - .rounded(self.variant.tab_bar_radius(self.size, cx)) - .paddings(paddings) .refine_style(&self.style) .when_some(self.prefix, |this, prefix| this.child(prefix)) .child( @@ -239,15 +133,13 @@ impl RenderOnce for TabBar { .when_some(self.scroll_handle, |this, scroll_handle| { this.track_scroll(&scroll_handle) }) - .gap(gap) + .gap(px(0.)) .children(self.children.into_iter().enumerate().map(|(ix, child)| { item_labels.push((child.label.clone(), child.disabled)); let tab_bar_prefix = child.tab_bar_prefix.unwrap_or(true); child .ix(ix) .tab_bar_prefix(tab_bar_prefix) - .with_variant(self.variant) - .with_size(self.size) .when_some(self.selected_index, |this, selected_ix| { this.selected(selected_ix == ix) }) diff --git a/crates/ui/src/title_bar.rs b/crates/ui/src/title_bar.rs index 79e60e0..83783f5 100644 --- a/crates/ui/src/title_bar.rs +++ b/crates/ui/src/title_bar.rs @@ -12,7 +12,6 @@ use theme::ActiveTheme; use crate::{Icon, IconName, InteractiveElementExt as _, Sizable as _, StyledExt, h_flex}; pub const TITLE_BAR_HEIGHT: Pixels = px(34.); -#[cfg(target_os = "macos")] pub const TRAFFIC_LIGHT_PADDING: f32 = 80.; /// TitleBar used to customize the appearance of the title bar. @@ -284,8 +283,6 @@ impl RenderOnce for TitleBar { this.px_2() } }) - .border_b_1() - .border_color(cx.theme().border) .bg(cx.theme().title_bar) .refine_style(&self.style) .when(is_linux, |this| { diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index f37ed1e..61bb5c6 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -17,6 +17,7 @@ person = { path = "../person" } gpui.workspace = true nostr-sdk.workspace = true +instant.workspace = true nostr-connect.workspace = true anyhow.workspace = true diff --git a/crates/workspace/src/dialogs/import.rs b/crates/workspace/src/dialogs/import.rs index fedc53e..06c64e2 100644 --- a/crates/workspace/src/dialogs/import.rs +++ b/crates/workspace/src/dialogs/import.rs @@ -1,11 +1,10 @@ -use std::time::Duration; - use anyhow::{Error, anyhow}; use gpui::prelude::FluentBuilder; use gpui::{ AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, }; +use instant::Duration; use nostr_connect::prelude::*; use state::{CoopAuthUrlHandler, NostrRegistry, USER_KEYRING}; use theme::ActiveTheme; @@ -36,7 +35,7 @@ pub struct ImportIdentity { impl ImportIdentity { pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let key_input = cx.new(|cx| InputState::new(window, cx)); + let key_input = cx.new(|cx| InputState::new(window, cx).placeholder("nsec or bunker://")); let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true)); let error = cx.new(|_| None); @@ -135,7 +134,6 @@ impl ImportIdentity { })?; } } - Ok(()) })); } @@ -146,7 +144,7 @@ impl ImportIdentity { let password = uri.to_string(); let save = cx.write_credentials(USER_KEYRING, "bunker", password.as_bytes()); - cx.spawn_in(window, async move |_this, cx| { + self.tasks.push(cx.spawn_in(window, async move |_this, cx| { let keys = master_keys.await; let timeout = Duration::from_secs(30); @@ -162,9 +160,8 @@ impl ImportIdentity { cx.notify(); }); - Ok::<(), anyhow::Error>(()) - }) - .detach(); + Ok(()) + })); } fn set_loading(&mut self, status: bool, cx: &mut Context) { @@ -209,40 +206,45 @@ impl Render for ImportIdentity { v_flex() .size_full() - .gap_2() + .gap_4() + .text_sm() .child( v_flex() - .gap_1() - .text_color(cx.theme().text_muted) - .child("Continue with existing key or bunker connection") - .child(Input::new(&self.key_input)), + .gap_2() + .child( + v_flex() + .gap_1() + .text_color(cx.theme().text_muted) + .child("Continue with existing key or bunker connection") + .child(Input::new(&self.key_input)), + ) + .when(require_password, |this| { + this.child( + v_flex() + .gap_1() + .text_color(cx.theme().text_muted) + .child("Decrypt Password:") + .child(Input::new(&self.pass_input)), + ) + }) + .when(key_warning, |this| { + this.child( + div() + .text_xs() + .text_color(cx.theme().text_warning) + .child(div().font_semibold().child("Warning")) + .child(div().child(MSG)), + ) + }), ) - .when(require_password, |this| { - this.child( - v_flex() - .gap_1() - .text_color(cx.theme().text_muted) - .child("Decrypt Password:") - .child(Input::new(&self.pass_input)), - ) - }) - .when(key_warning, |this| { - this.child( - div() - .v_flex() - .text_xs() - .text_color(cx.theme().text_warning) - .child(div().font_semibold().child("Warning")) - .child(div().child(MSG)), - ) - }) .child( Button::new("login") .label("Continue") .primary() + .font_semibold() .loading(self.loading) .disabled(self.loading) - .on_click(cx.listener(move |this, _, window, cx| { + .on_click(cx.listener(move |this, _ev, window, cx| { this.login(window, cx); })), ) diff --git a/crates/workspace/src/dialogs/restore.rs b/crates/workspace/src/dialogs/restore.rs index b51424b..958d387 100644 --- a/crates/workspace/src/dialogs/restore.rs +++ b/crates/workspace/src/dialogs/restore.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use instant::Duration; use anyhow::Error; use device::DeviceRegistry; diff --git a/crates/workspace/src/dialogs/screening.rs b/crates/workspace/src/dialogs/screening.rs index 45a61a0..d0f0b1e 100644 --- a/crates/workspace/src/dialogs/screening.rs +++ b/crates/workspace/src/dialogs/screening.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::time::Duration; use anyhow::Error; use common::TimestampExt; @@ -8,6 +7,7 @@ use gpui::{ App, AppContext, Context, Div, Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, px, relative, uniform_list, }; +use instant::Duration; use nostr_sdk::prelude::*; use person::{Person, PersonRegistry, shorten_pubkey}; use smallvec::{SmallVec, smallvec}; @@ -55,7 +55,7 @@ impl Screening { window.close_all_modals(cx); })); - cx.defer_in(window, move |this, _window, cx| { + cx.defer_in(window, |this, _window, cx| { this.check_contact(cx); this.check_wot(cx); this.check_last_activity(cx); @@ -234,7 +234,8 @@ impl Screening { } .to_tag(); - let event = EventBuilder::report(vec![tag], "") + let event = EventBuilder::new(Kind::Reporting, "") + .tag(tag) .finalize_async(&signer) .await?; diff --git a/crates/workspace/src/dialogs/settings.rs b/crates/workspace/src/dialogs/settings.rs index 5bf9081..185c814 100644 --- a/crates/workspace/src/dialogs/settings.rs +++ b/crates/workspace/src/dialogs/settings.rs @@ -3,7 +3,7 @@ use gpui::{ App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, Window, div, px, }; -use settings::{AppSettings, AuthMode}; +use settings::AppSettings; use theme::{ActiveTheme, Theme, ThemeMode}; use ui::button::{Button, ButtonVariants}; use ui::group_box::{GroupBox, GroupBoxVariants}; @@ -60,13 +60,11 @@ impl Render for Preferences { const AVATAR: &str = "Hide all avatar pictures to improve performance."; const MODE: &str = "Use the selected light or dark theme, or to follow the OS."; const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages."; - const AUTH: &str = "Choose the authentication behavior for relays."; const RESET: &str = "Reset the theme to the default one."; let screening = AppSettings::get_screening(cx); let hide_avatar = AppSettings::get_hide_avatar(cx); let nip4e = AppSettings::get_nip4e(cx); - let auth_mode = AppSettings::get_auth_mode(cx); let theme_mode = AppSettings::get_theme_mode(cx); v_flex() @@ -93,52 +91,6 @@ impl Render for Preferences { .on_click(move |_, _window, cx| { AppSettings::update_hide_avatar(!hide_avatar, cx); }), - ) - .child( - h_flex() - .gap_3() - .justify_between() - .child( - v_flex() - .child( - div() - .text_sm() - .child(SharedString::from("Relay authentication")), - ) - .child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .child(SharedString::from(AUTH)), - ), - ) - .child( - Button::new("auth") - .label(auth_mode.to_string()) - .ghost_alt() - .small() - .dropdown_menu(|this, _window, _cx| { - this.min_w(px(256.)) - .item( - PopupMenuItem::new("Auto authentication").on_click( - |_ev, _window, cx| { - AppSettings::update_auth_mode( - AuthMode::Auto, - cx, - ); - }, - ), - ) - .item(PopupMenuItem::new("Ask every time").on_click( - |_ev, _window, cx| { - AppSettings::update_auth_mode( - AuthMode::Manual, - cx, - ); - }, - )) - }), - ), ), ) .child( diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 6d80c60..cd26935 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -1,14 +1,14 @@ use std::sync::Arc; use ::settings::AppSettings; +use anyhow::Error; use chat::{ChatEvent, ChatRegistry}; use common::{CoopImageCache, download_dir}; use device::{DeviceEvent, DeviceRegistry}; use gpui::prelude::FluentBuilder; use gpui::{ Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement, - Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, div, - image_cache, px, relative, + Render, SharedString, Styled, Subscription, Task, Window, div, image_cache, px, }; use nostr_sdk::prelude::*; use person::{PersonRegistry, shorten_pubkey}; @@ -26,7 +26,8 @@ use ui::{Icon, IconName, Root, Sizable, TitleBar, WindowExtension, h_flex, v_fle use crate::dialogs::import::ImportIdentity; use crate::dialogs::restore::RestoreEncryption; use crate::dialogs::settings; -use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list, trash}; +use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list}; +use crate::sidebar::Sidebar; mod dialogs; mod panels; @@ -59,12 +60,16 @@ enum Command { } pub struct Workspace { + sidebar: Entity, /// App's Dock Area dock: Entity, /// App's Image Cache image_cache: Entity, + /// Async tasks + tasks: Vec>>, + /// Event subscriptions _subscriptions: SmallVec<[Subscription; 6]>, } @@ -75,20 +80,12 @@ impl Workspace { let device = DeviceRegistry::global(cx); let nostr = NostrRegistry::global(cx); + let sidebar = cx.new(|cx| Sidebar::new(window, cx)); let dock = cx.new(|cx| DockArea::new(window, cx)); let image_cache = CoopImageCache::new(IMAGE_CACHE_SIZE, cx); let mut subscriptions = smallvec![]; - subscriptions.push( - // Observe sign in state changes - cx.observe_in(&nostr, window, move |this, nostr, window, cx| { - if nostr.read(cx).current_user().is_some() { - this.set_center_layout(window, cx); - } - }), - ); - subscriptions.push( // Observe system appearance and update theme cx.observe_window_appearance(window, |_this, window, cx| { @@ -101,11 +98,7 @@ impl Workspace { cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| { match event { StateEvent::SignerChanged => { - this.set_center_layout(window, cx); - - cx.defer_in(window, |_this, window, cx| { - window.close_all_modals(cx); - }); + window.close_all_modals(cx); } StateEvent::NoSigner => { this.import_identity(window, cx); @@ -226,25 +219,22 @@ impl Workspace { }), ); - subscriptions.push( - // Observe the chat registry - cx.observe(&chat, move |this, chat, cx| { - let ids = this.panel_ids(cx); - - chat.update(cx, |this, cx| { - this.refresh_rooms(&ids, cx); - }); - }), - ); - - // Set the layout at the end of cycle cx.defer_in(window, |this, window, cx| { - this.set_layout(window, cx); + let dock = this.dock.downgrade(); + let greeter = Arc::new(greeter::init(window, cx)); + let tabs = DockItem::tabs(vec![greeter], None, &dock, window, cx); + let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx); + + this.dock.update(cx, |this, cx| { + this.set_center(center, window, cx); + }); }); Self { + sidebar, dock, image_cache, + tasks: vec![], _subscriptions: subscriptions, } } @@ -265,40 +255,6 @@ impl Workspace { } } - /// Get all panel ids - fn panel_ids(&self, cx: &App) -> Vec { - self.dock - .read(cx) - .items - .panel_ids(cx) - .into_iter() - .filter_map(|panel| panel.parse::().ok()) - .collect() - } - - /// Set the dock layout - fn set_layout(&mut self, window: &mut Window, cx: &mut Context) { - let left = DockItem::panel(Arc::new(sidebar::init(window, cx))); - - // Update the dock layout with sidebar on the left - self.dock.update(cx, |this, cx| { - this.set_left_dock(left, Some(SIDEBAR_WIDTH), true, window, cx); - }); - } - - /// Set the center dock layout - fn set_center_layout(&mut self, window: &mut Window, cx: &mut Context) { - let dock = self.dock.downgrade(); - let greeter = Arc::new(greeter::init(window, cx)); - let tabs = DockItem::tabs(vec![greeter], None, &dock, window, cx); - let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx); - - // Update the layout with center dock - self.dock.update(cx, |this, cx| { - this.set_center(center, window, cx); - }); - } - /// Handle command events fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context) { match command { @@ -320,7 +276,7 @@ impl Workspace { self.dock.update(cx, |this, cx| { this.add_panel( Arc::new(profile::init(public_key, window, cx)), - DockPlacement::Right, + DockPlacement::Left, window, cx, ); @@ -331,7 +287,7 @@ impl Workspace { self.dock.update(cx, |this, cx| { this.add_panel( Arc::new(contact_list::init(window, cx)), - DockPlacement::Right, + DockPlacement::Left, window, cx, ); @@ -341,7 +297,7 @@ impl Workspace { self.dock.update(cx, |this, cx| { this.add_panel( Arc::new(backup::init(window, cx)), - DockPlacement::Right, + DockPlacement::Left, window, cx, ); @@ -351,7 +307,7 @@ impl Workspace { self.dock.update(cx, |this, cx| { this.add_panel( Arc::new(messaging_relays::init(window, cx)), - DockPlacement::Right, + DockPlacement::Left, window, cx, ); @@ -361,7 +317,7 @@ impl Workspace { let chat = ChatRegistry::global(cx); // Trigger a refresh of the chat registry chat.update(cx, |this, cx| { - this.refresh(cx); + this.reload(cx); }); } Command::ShowRelayList => { @@ -390,7 +346,7 @@ impl Workspace { let device = DeviceRegistry::global(cx).downgrade(); let save_dialog = cx.prompt_for_new_path(download_dir(), Some("encryption.txt")); - cx.spawn_in(window, async move |_this, cx| { + self.tasks.push(cx.spawn_in(window, async move |_this, cx| { // Get the output path from the save dialog let output_path = match save_dialog.await { Ok(Ok(Some(path))) => path, @@ -417,9 +373,8 @@ impl Workspace { cx.open_with_system(output_path.as_path()); })?; - Ok::<_, anyhow::Error>(()) - }) - .detach(); + Ok(()) + })); } Command::ImportEncryption => { self.import_encryption(window, cx); @@ -479,10 +434,11 @@ impl Workspace { let import = cx.new(|cx| ImportIdentity::new(window, cx)); window.open_modal(cx, move |this, _window, _cx| { - this.width(px(420.)) + this.width(px(450.)) .show_close(false) .overlay_closable(false) - .title("Import Identity") + .keyboard(false) + .title("Onboarding") .child(import.clone()) }); } @@ -642,9 +598,7 @@ impl Workspace { fn titlebar_right(&mut self, cx: &mut Context) -> impl IntoElement { let chat = ChatRegistry::global(cx); - let trash_messages = chat.read(cx).count_trash_messages(cx); - - let is_nip4e_enabled = AppSettings::get_nip4e(cx); + let nip4e_enabled = AppSettings::get_nip4e(cx); let nostr = NostrRegistry::global(cx); let Some(public_key) = nostr.read(cx).current_user() else { @@ -658,39 +612,7 @@ impl Workspace { h_flex() .when(!cx.theme().platform.is_mac(), |this| this.pr_2()) .gap_2() - .when(trash_messages > 0, |this| { - this.child( - h_flex() - .id("trash-messages") - .h_6() - .px_1() - .gap_1() - .rounded(cx.theme().radius) - .hover(|this| this.bg(cx.theme().ghost_element_hover)) - .child( - Icon::new(IconName::Warning) - .small() - .text_color(cx.theme().text_danger), - ) - .child( - div() - .text_xs() - .line_height(relative(1.)) - .child(format!("{trash_messages}")), - ) - .on_click(move |_ev, window, cx| { - cx.stop_propagation(); - // Add the trash panel to the center workspace - Self::add_panel( - trash::init(window, cx), - DockPlacement::Center, - window, - cx, - ); - }), - ) - }) - .when(is_nip4e_enabled, |this| { + .when(nip4e_enabled, |this| { this.child( Button::new("key") .icon(IconName::UserKey) @@ -782,17 +704,7 @@ impl Workspace { .w_full() .text_sm() .justify_between() - .child( - h_flex() - .gap_2() - .child( - div() - .size_1p5() - .rounded_full() - .bg(cx.theme().icon_accent), - ) - .child(url.clone()), - ) + .child(url.clone()) .child( div() .text_xs() @@ -804,11 +716,6 @@ impl Workspace { // Footer menu.separator() - .menu_with_icon( - "Reload", - IconName::Refresh, - Box::new(Command::RefreshMessagingRelays), - ) .menu_with_icon( "Manage gossip relays", IconName::Relay, @@ -816,9 +723,15 @@ impl Workspace { ) .menu_with_icon( "Manage messaging relays", - IconName::Settings, + IconName::Relay, Box::new(Command::ShowMessaging), ) + .separator() + .menu_with_icon( + "Reload", + IconName::Refresh, + Box::new(Command::RefreshMessagingRelays), + ) }), ) } @@ -830,7 +743,7 @@ impl Render for Workspace { let notification_layer = Root::render_notification_layer(window, cx); div() - .id(SharedString::from("workspace")) + .id("workspace") .on_action(cx.listener(Self::on_command)) .relative() .size_full() @@ -847,8 +760,19 @@ impl Render for Workspace { .child(self.titlebar_left(cx)) .child(self.titlebar_right(cx)), ) - // Dock - .child(self.dock.clone()), + // Main + .child( + h_flex() + .size_full() + .child( + div() + .flex_shrink_0() + .h_full() + .w(SIDEBAR_WIDTH) + .child(self.sidebar.clone()), + ) + .child(self.dock.clone()), + ), ), ) // Notifications diff --git a/crates/workspace/src/panels/backup.rs b/crates/workspace/src/panels/backup.rs index 498ba46..51dd1ca 100644 --- a/crates/workspace/src/panels/backup.rs +++ b/crates/workspace/src/panels/backup.rs @@ -1,10 +1,9 @@ -use std::time::Duration; - use anyhow::Error; use gpui::{ AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div, }; +use instant::Duration; use nostr_sdk::prelude::*; use state::USER_KEYRING; use theme::ActiveTheme; @@ -155,12 +154,7 @@ impl Render for BackupPanel { .text_color(cx.theme().text_muted) .child(SharedString::from("Public Key:")), ) - .child( - Input::new(&self.npub_input) - .small() - .bordered(false) - .disabled(true), - ), + .child(Input::new(&self.npub_input).small().disabled(true)), ) .child( v_flex() @@ -173,12 +167,7 @@ impl Render for BackupPanel { .text_color(cx.theme().text_muted) .child(SharedString::from("Secret Key:")), ) - .child( - Input::new(&self.nsec_input) - .small() - .bordered(false) - .disabled(true), - ), + .child(Input::new(&self.nsec_input).small().disabled(true)), ) .child( Button::new("copy") diff --git a/crates/workspace/src/panels/contact_list.rs b/crates/workspace/src/panels/contact_list.rs index 47188e1..1152688 100644 --- a/crates/workspace/src/panels/contact_list.rs +++ b/crates/workspace/src/panels/contact_list.rs @@ -1,5 +1,4 @@ use std::collections::HashSet; -use std::time::Duration; use anyhow::Error; use gpui::prelude::FluentBuilder; @@ -8,6 +7,7 @@ use gpui::{ InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, TextAlign, Window, div, rems, }; +use instant::Duration; use nostr_sdk::prelude::*; use person::PersonRegistry; use smallvec::{SmallVec, smallvec}; @@ -17,6 +17,7 @@ use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::dock::{Panel, PanelEvent}; use ui::input::{Input, InputEvent, InputState}; +use ui::scroll::ScrollableElement; use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; pub fn init(window: &mut Window, cx: &mut App) -> Entity { @@ -220,8 +221,7 @@ impl ContactListPanel { .px_2() .justify_between() .rounded(cx.theme().radius) - .bg(cx.theme().secondary_background) - .text_color(cx.theme().secondary_foreground) + .hover(|this| this.bg(cx.theme().ghost_element_hover)) .child( h_flex() .gap_2() @@ -283,79 +283,78 @@ impl Focusable for ContactListPanel { impl Render for ContactListPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex().p_3().gap_3().w_full().child( - v_flex() - .gap_2() - .flex_1() - .w_full() - .text_sm() - .child( - div() - .text_xs() - .font_semibold() - .text_color(cx.theme().text_muted) - .child(SharedString::from("New contact:")), - ) - .child( - v_flex() - .gap_1() - .child( - h_flex() - .gap_1() - .w_full() - .child( - Input::new(&self.input) - .small() - .bordered(false) - .cleanable(true), - ) - .child( - Button::new("add") - .icon(IconName::Plus) - .tooltip("Add contact") - .ghost() - .size(rems(2.)) - .on_click(cx.listener(move |this, _, window, cx| { - this.add(window, cx); - })), - ), - ) - .when_some(self.error.as_ref(), |this, error| { - this.child( - div() - .italic() - .text_xs() - .text_color(cx.theme().text_danger) - .child(error.clone()), + v_flex() + .p_3() + .gap_3() + .w_full() + .overflow_y_scrollbar() + .child( + v_flex() + .gap_2() + .flex_1() + .w_full() + .text_sm() + .child( + div() + .text_xs() + .font_semibold() + .text_color(cx.theme().text_muted) + .child("New contact:"), + ) + .child( + v_flex() + .gap_1() + .child( + h_flex() + .gap_1() + .w_full() + .child(Input::new(&self.input).small().cleanable(true)) + .child( + Button::new("add") + .icon(IconName::Plus) + .tooltip("Add contact") + .ghost() + .size(rems(2.)) + .on_click(cx.listener(move |this, _, window, cx| { + this.add(window, cx); + })), + ), ) - }), - ) - .map(|this| { - if self.contacts.is_empty() { - this.child(self.render_empty(window, cx)) - } else { - this.child( - v_flex() - .gap_1() - .flex_1() - .w_full() - .children(self.render_list_items(cx)), - ) - } - }) - .child( - Button::new("submit") - .icon(IconName::CheckCircle) - .label("Update") - .primary() - .small() - .font_semibold() - .loading(self.updating) - .disabled(self.updating) - .on_click(cx.listener(move |this, _ev, window, cx| { - this.update(window, cx); - })), - ), - ) + .when_some(self.error.as_ref(), |this, error| { + this.child( + div() + .italic() + .text_xs() + .text_color(cx.theme().text_danger) + .child(error.clone()), + ) + }), + ) + .map(|this| { + if self.contacts.is_empty() { + this.child(self.render_empty(window, cx)) + } else { + this.child( + v_flex() + .gap_1() + .flex_1() + .w_full() + .children(self.render_list_items(cx)), + ) + } + }) + .child( + Button::new("submit") + .icon(IconName::CheckCircle) + .label("Update") + .primary() + .font_semibold() + .loading(self.updating) + .disabled(self.updating) + .on_click(cx.listener(move |this, _ev, window, cx| { + this.update(window, cx); + })), + ), + ) } } diff --git a/crates/workspace/src/panels/greeter.rs b/crates/workspace/src/panels/greeter.rs index 69ddad0..7e6dccd 100644 --- a/crates/workspace/src/panels/greeter.rs +++ b/crates/workspace/src/panels/greeter.rs @@ -1,6 +1,7 @@ +use anyhow::Error; use gpui::{ AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, - IntoElement, ParentElement, Render, SharedString, Styled, Window, div, svg, + IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div, svg, }; use state::NostrRegistry; use theme::ActiveTheme; @@ -8,8 +9,8 @@ use ui::button::{Button, ButtonVariants}; use ui::dock::{DockPlacement, Panel, PanelEvent}; use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex}; -use crate::Workspace; use crate::panels::profile; +use crate::{Command, Workspace}; pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| GreeterPanel::new(window, cx)) @@ -18,6 +19,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity { pub struct GreeterPanel { name: SharedString, focus_handle: FocusHandle, + tasks: Vec>>, } impl GreeterPanel { @@ -25,6 +27,7 @@ impl GreeterPanel { Self { name: "Onboarding".into(), focus_handle: cx.focus_handle(), + tasks: vec![], } } @@ -32,7 +35,7 @@ impl GreeterPanel { let nostr = NostrRegistry::global(cx); if let Some(public_key) = nostr.read(cx).current_user() { - cx.spawn_in(window, async move |_this, cx| { + self.tasks.push(cx.spawn_in(window, async move |_this, cx| { cx.update(|window, cx| { Workspace::add_panel( profile::init(public_key, window, cx), @@ -42,8 +45,9 @@ impl GreeterPanel { ); }) .ok(); - }) - .detach(); + + Ok(()) + })); } } } @@ -142,17 +146,20 @@ impl Render for GreeterPanel { .ghost() .small() .justify_start() - .on_click(cx.listener(move |this, _ev, window, cx| { + .on_click(cx.listener(move |this, _, window, cx| { this.add_profile_panel(window, cx) })), ) .child( - Button::new("invite") - .icon(Icon::new(IconName::Invite)) - .label("Invite friends") + Button::new("theme") + .icon(Icon::new(IconName::Moon)) + .label("Change theme") .ghost() .small() - .justify_start(), + .justify_start() + .on_click(cx.listener(move |_, _, _, cx| { + cx.dispatch_action(&Command::ToggleTheme); + })), ), ), ), diff --git a/crates/workspace/src/panels/messaging_relays.rs b/crates/workspace/src/panels/messaging_relays.rs index b89ca81..9b40de3 100644 --- a/crates/workspace/src/panels/messaging_relays.rs +++ b/crates/workspace/src/panels/messaging_relays.rs @@ -1,5 +1,4 @@ use std::collections::HashSet; -use std::time::Duration; use anyhow::{Error, anyhow}; use gpui::prelude::FluentBuilder; @@ -8,6 +7,7 @@ use gpui::{ InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, TextAlign, Window, div, rems, }; +use instant::Duration; use nostr_sdk::prelude::*; use smallvec::{SmallVec, smallvec}; use state::NostrRegistry; @@ -320,12 +320,7 @@ impl Render for MessagingRelayPanel { h_flex() .gap_1() .w_full() - .child( - Input::new(&self.input) - .small() - .bordered(false) - .cleanable(true), - ) + .child(Input::new(&self.input).small().cleanable(true)) .child( Button::new("add") .icon(IconName::Plus) @@ -365,7 +360,6 @@ impl Render for MessagingRelayPanel { .icon(IconName::CheckCircle) .label("Update") .primary() - .small() .font_semibold() .loading(self.updating) .disabled(self.updating) diff --git a/crates/workspace/src/panels/mod.rs b/crates/workspace/src/panels/mod.rs index e223f2f..bb47e07 100644 --- a/crates/workspace/src/panels/mod.rs +++ b/crates/workspace/src/panels/mod.rs @@ -4,4 +4,3 @@ pub mod greeter; pub mod messaging_relays; pub mod profile; pub mod relay_list; -pub mod trash; diff --git a/crates/workspace/src/panels/profile.rs b/crates/workspace/src/panels/profile.rs index d8da358..a944c9c 100644 --- a/crates/workspace/src/panels/profile.rs +++ b/crates/workspace/src/panels/profile.rs @@ -1,5 +1,4 @@ use std::str::FromStr; -use std::time::Duration; use anyhow::{Context as AnyhowContext, Error}; use gpui::{ @@ -7,6 +6,7 @@ use gpui::{ Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task, Window, div, }; +use instant::Duration; use nostr_sdk::prelude::*; use person::{Person, PersonRegistry, shorten_pubkey}; use settings::AppSettings; @@ -132,7 +132,7 @@ impl ProfilePanel { cx.notify(); if status { - cx.spawn_in(window, async move |this, cx| { + self.tasks.push(cx.spawn_in(window, async move |this, cx| { cx.background_executor().timer(Duration::from_secs(2)).await; // Reset the copied state after a delay @@ -143,8 +143,9 @@ impl ProfilePanel { .ok(); }) .ok(); - }) - .detach(); + + Ok(()) + })); } } @@ -352,7 +353,7 @@ impl Render for ProfilePanel { .text_color(cx.theme().text_muted) .child(SharedString::from("What should people call you?")), ) - .child(Input::new(&self.name_input).bordered(false).small()), + .child(Input::new(&self.name_input).small()), ) .child( v_flex() @@ -363,7 +364,7 @@ impl Render for ProfilePanel { .text_color(cx.theme().text_muted) .child(SharedString::from("A short introduction about you:")), ) - .child(Input::new(&self.bio_input).bordered(false).small()), + .child(Input::new(&self.bio_input).small()), ) .child( v_flex() @@ -374,7 +375,7 @@ impl Render for ProfilePanel { .text_color(cx.theme().text_muted) .child(SharedString::from("Website:")), ) - .child(Input::new(&self.website_input).bordered(false).small()), + .child(Input::new(&self.website_input).small()), ) .child( v_flex() @@ -418,7 +419,6 @@ impl Render for ProfilePanel { .icon(IconName::CheckCircle) .label("Update") .primary() - .small() .font_semibold() .loading(self.updating) .disabled(self.updating) diff --git a/crates/workspace/src/panels/relay_list.rs b/crates/workspace/src/panels/relay_list.rs index df3eabe..6b51cbf 100644 --- a/crates/workspace/src/panels/relay_list.rs +++ b/crates/workspace/src/panels/relay_list.rs @@ -1,5 +1,4 @@ use std::collections::HashSet; -use std::time::Duration; use anyhow::{Error, anyhow}; use gpui::prelude::FluentBuilder; @@ -8,6 +7,7 @@ use gpui::{ InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, TextAlign, Window, div, px, rems, }; +use instant::Duration; use nostr_sdk::prelude::*; use serde::Deserialize; use smallvec::{SmallVec, smallvec}; @@ -216,7 +216,7 @@ impl RelayListPanel { self.set_updating(true, cx); let task: Task> = cx.background_spawn(async move { - let event = EventBuilder::relay_list(relays) + let event = nip65::RelayList::new(relays) .finalize_async(&signer) .await?; @@ -371,12 +371,7 @@ impl Render for RelayListPanel { h_flex() .gap_1() .w_full() - .child( - Input::new(&self.input) - .small() - .bordered(false) - .cleanable(true), - ) + .child(Input::new(&self.input).small().cleanable(true)) .child( Button::new("metadata") .map(|this| { @@ -434,7 +429,6 @@ impl Render for RelayListPanel { .icon(IconName::CheckCircle) .label("Update") .primary() - .small() .font_semibold() .loading(self.updating) .disabled(self.updating) diff --git a/crates/workspace/src/panels/trash.rs b/crates/workspace/src/panels/trash.rs deleted file mode 100644 index 2a2220c..0000000 --- a/crates/workspace/src/panels/trash.rs +++ /dev/null @@ -1,152 +0,0 @@ -use chat::ChatRegistry; -use gpui::{ - AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, - Focusable, InteractiveElement, IntoElement, ListAlignment, ListState, ParentElement, Render, - SharedString, Styled, Window, div, list, px, relative, -}; -use theme::ActiveTheme; -use ui::button::{Button, ButtonVariants}; -use ui::dock::{Panel, PanelEvent}; -use ui::scroll::Scrollbar; -use ui::{Icon, IconName, Sizable, h_flex, v_flex}; - -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| TrashPanel::new(window, cx)) -} - -pub struct TrashPanel { - name: SharedString, - focus_handle: FocusHandle, - - /// List state for messages - list_state: ListState, -} - -impl TrashPanel { - fn new(_window: &mut Window, cx: &mut App) -> Self { - let chat = ChatRegistry::global(cx); - let count = chat.read(cx).count_trash_messages(cx); - let list_state = ListState::new(count, ListAlignment::Bottom, px(1024.)); - - Self { - name: "Trash".into(), - focus_handle: cx.focus_handle(), - list_state, - } - } - - fn copy(&self, ix: usize, cx: &App) { - let chat = ChatRegistry::global(cx); - let trashes = chat.read(cx).trashes(); - - if let Some(message) = trashes.read(cx).iter().nth(ix) { - let item = ClipboardItem::new_string(message.raw_event.to_string()); - cx.write_to_clipboard(item); - } - } - - fn render_list_item( - &mut self, - ix: usize, - _window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - let chat = ChatRegistry::global(cx); - let trashes = chat.read(cx).trashes(); - - if let Some(message) = trashes.read(cx).iter().nth(ix) { - v_flex() - .id(ix) - .p_2() - .w_full() - .child( - v_flex() - .p_2() - .w_full() - .gap_1() - .rounded(cx.theme().radius_lg) - .bg(cx.theme().surface_background) - .text_sm() - .child( - div() - .text_color(cx.theme().text_danger) - .child(message.reason.clone()), - ) - .child( - h_flex() - .h_10() - .w_full() - .px_2() - .justify_between() - .bg(cx.theme().elevated_surface_background) - .border_1() - .border_color(cx.theme().border) - .rounded(cx.theme().radius) - .child( - div() - .truncate() - .text_ellipsis() - .text_xs() - .line_height(relative(1.)) - .child(message.raw_event.clone()), - ) - .child( - Button::new(format!("copy-{ix}")) - .icon(IconName::Copy) - .ghost() - .small() - .on_click(cx.listener(move |this, _ev, _window, cx| { - this.copy(ix, cx); - })), - ), - ), - ) - .into_any_element() - } else { - div().id(ix).into_any_element() - } - } -} - -impl Panel for TrashPanel { - fn panel_id(&self) -> SharedString { - self.name.clone() - } - - fn title(&self, _cx: &App) -> AnyElement { - h_flex() - .gap_1() - .text_sm() - .child(Icon::new(IconName::Warning).small()) - .child("Errors") - .into_any_element() - } -} - -impl EventEmitter for TrashPanel {} - -impl Focusable for TrashPanel { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for TrashPanel { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex().size_full().relative().child( - v_flex() - .flex_1() - .relative() - .child( - list( - self.list_state.clone(), - cx.processor(move |this, ix, window, cx| { - this.render_list_item(ix, window, cx) - }), - ) - .size_full(), - ) - .child(Scrollbar::vertical(&self.list_state)), - ) - } -} diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index 0b884ec..587c8f7 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -1,6 +1,5 @@ use std::collections::HashSet; use std::ops::Range; -use std::time::Duration; use anyhow::Error; use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; @@ -12,11 +11,12 @@ use gpui::{ ParentElement, Render, SharedString, Styled, Subscription, Task, UniformListScrollHandle, Window, div, uniform_list, }; +use instant::Duration; use nostr_sdk::prelude::*; use person::PersonRegistry; use smallvec::{SmallVec, smallvec}; use state::{FIND_DELAY, IMAGE_CACHE_SIZE, NostrRegistry}; -use theme::{ActiveTheme, SIDEBAR_WIDTH, TABBAR_HEIGHT}; +use theme::{ActiveTheme, SIDEBAR_WIDTH}; use ui::button::{Button, ButtonVariants}; use ui::dock::{Panel, PanelEvent}; use ui::indicator::Indicator; @@ -29,13 +29,8 @@ mod entry; const INPUT_PLACEHOLDER: &str = "Find or start a conversation"; -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| Sidebar::new(window, cx)) -} - /// Sidebar. pub struct Sidebar { - name: SharedString, focus_handle: FocusHandle, scroll_handle: UniformListScrollHandle, @@ -80,7 +75,7 @@ pub struct Sidebar { } impl Sidebar { - fn new(window: &mut Window, cx: &mut Context) -> Self { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); let filter = cx.new(|_| RoomKind::Ongoing); let contact_list = cx.new(|_| None); @@ -135,7 +130,6 @@ impl Sidebar { ); Self { - name: "Sidebar".into(), focus_handle: cx.focus_handle(), scroll_handle: UniformListScrollHandle::new(), find_input, @@ -335,7 +329,7 @@ impl Sidebar { .organize(&public_key) .kind(RoomKind::Ongoing) }); - this.emit_room(&room, cx); + this.emit_room(&room, _window, cx); })?; // Reset the find panel @@ -379,9 +373,9 @@ impl Sidebar { let room = item.read(cx); let room_clone = item.clone(); let public_key = room.display_member(cx).public_key(); - let handler = cx.listener(move |_this, _ev, _window, cx| { + let handler = cx.listener(move |_this, _ev, window, cx| { ChatRegistry::global(cx).update(cx, |s, cx| { - s.emit_room(&room_clone, cx); + s.emit_room(&room_clone, window, cx); }); }); @@ -474,7 +468,7 @@ impl Sidebar { impl Panel for Sidebar { fn panel_id(&self) -> SharedString { - self.name.clone() + "Sidebar".into() } } @@ -488,8 +482,11 @@ impl Focusable for Sidebar { impl Render for Sidebar { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let nostr = NostrRegistry::global(cx); let chat = ChatRegistry::global(cx); - let loading = chat.read(cx).loading(); + let logged_in = nostr.read(cx).current_user().is_some(); + let loading = chat.read(cx).loading() && logged_in; + let total_rooms = chat.read(cx).count(self.filter.read(cx), cx); // Whether the find panel should be shown @@ -507,27 +504,21 @@ impl Render for Sidebar { .size_full() .gap_2() .child( - h_flex() - .h(TABBAR_HEIGHT) - .border_b_1() - .border_color(cx.theme().border) - .bg(cx.theme().tab_background) - .child( - Input::new(&self.find_input) - .appearance(false) - .bordered(false) - .small() - .text_xs() - .when(!self.find_input.read(cx).loading, |this| { - this.suffix( - Button::new("find-icon") - .icon(IconName::Search) - .tooltip("Press Enter to search") - .transparent() - .small(), - ) - }), - ), + h_flex().px_2().py_1().child( + Input::new(&self.find_input) + .small() + .text_xs() + .disabled(loading) + .when(!self.find_input.read(cx).loading, |this| { + this.suffix( + Button::new("find-icon") + .icon(IconName::Search) + .tooltip("Press Enter to search") + .transparent() + .small(), + ) + }), + ), ) .child( h_flex() @@ -539,7 +530,6 @@ impl Render for Sidebar { Button::new("search-results") .icon(IconName::Search) .tooltip("All search results") - .small() .ghost_alt() .font_semibold() .flex_1() @@ -555,9 +545,8 @@ impl Render for Sidebar { this.icon(IconName::Inbox) } }) - .when(!show_find_panel, |this| this.label("Inbox")) + .when(!show_find_panel, |this| this.label("Inbox").small()) .tooltip("All ongoing conversations") - .small() .ghost_alt() .font_semibold() .flex_1() @@ -577,9 +566,8 @@ impl Render for Sidebar { this.icon(IconName::Fistbump) } }) - .when(!show_find_panel, |this| this.label("Requests")) + .when(!show_find_panel, |this| this.label("Requests").small()) .tooltip("Incoming new conversations") - .small() .ghost_alt() .font_semibold() .flex_1() @@ -608,17 +596,13 @@ impl Render for Sidebar { .items_center() .justify_center() .text_center() + .child(div().text_sm().font_semibold().child("No conversations")) .child( div() - .text_sm() - .font_semibold() - .child(SharedString::from("No conversations")), - ) - .child(div().text_xs().text_color(cx.theme().text_muted).child( - SharedString::from( - "Start a conversation with someone to get started.", - ), - )), + .text_xs() + .text_color(cx.theme().text_muted) + .child("Start a conversation with someone to get started."), + ), ), ) }) @@ -643,7 +627,7 @@ impl Render for Sidebar { .font_semibold() .text_color(cx.theme().text_muted) .child(Icon::new(IconName::ChevronDown)) - .child(SharedString::from("Results")), + .child("Results"), ) .child( uniform_list( @@ -670,7 +654,7 @@ impl Render for Sidebar { .font_semibold() .text_color(cx.theme().text_muted) .child(Icon::new(IconName::ChevronDown).small()) - .child(SharedString::from("Suggestions")), + .child("Contacts"), ) .child( uniform_list( @@ -707,17 +691,17 @@ impl Render for Sidebar { this.child( div() .absolute() - .bottom_0() + .bottom_2() .left_0() .h_9() .w_full() - .px_2() + .px_4() .child( Button::new("create") .label(button_label) .primary() - .small() - .shadow_lg() + .rounded() + .shadow_md() .on_click(cx.listener(move |this, _ev, window, cx| { this.create_room(window, cx); })), @@ -740,15 +724,13 @@ impl Render for Sidebar { .h_9() .justify_center() .bg(cx.theme().background.opacity(0.85)) - .border_color(cx.theme().border_disabled) - .border_1() - .when(cx.theme().shadow, |this| this.shadow_xs()) + .when(cx.theme().shadow, |this| this.shadow_md()) .rounded_full() .text_xs() .font_semibold() .text_color(cx.theme().text_muted) .child(Indicator::new().small().color(cx.theme().icon_accent)) - .child(SharedString::from("Getting messages...")), + .child("Getting messages..."), ), ) }) diff --git a/desktop/src/main.rs b/desktop/src/main.rs index ff147c6..5b6a5d5 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -43,7 +43,7 @@ fn main() { }]); // Set up the window bounds - let bounds = Bounds::centered(None, size(px(920.0), px(700.0)), cx); + let bounds = Bounds::centered(None, size(px(960.0), px(720.0)), cx); // Set up the window options let opts = WindowOptions { @@ -62,41 +62,39 @@ fn main() { // Open a window with default options cx.open_window(opts, |window, cx| { - // Bring the app to the foreground - cx.activate(true); + // Initialize components + ui::init(cx); - cx.new(|cx| { - // Initialize components - ui::init(cx); + // Initialize theme registry + theme::init(cx); - // Initialize theme registry - theme::init(cx); + // Initialize settings + settings::init(window, cx); - // Initialize settings - settings::init(window, cx); + // Initialize the nostr client + state::init(window, cx); - // Initialize the nostr client - state::init(window, cx); + // Initialize person registry + person::init(window, cx); - // Initialize person registry - person::init(window, cx); + // Initialize device signer + // + // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + device::init(window, cx); - // Initialize device signer - // - // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md - device::init(window, cx); + // Initialize app registry + chat::init(window, cx); - // Initialize app registry - chat::init(window, cx); + // Initialize auto update + auto_update::init(window, cx); - // Initialize auto update - auto_update::init(window, cx); - - // Root Entity - Root::new(workspace::init(window, cx).into(), window, cx) - }) + // Root view + cx.new(|cx| Root::new(workspace::init(window, cx).into(), window, cx)) }) .expect("Failed to open window. Please restart the application."); + + // Bring the app to the foreground + cx.activate(true); }); } diff --git a/web/Cargo.toml b/web/Cargo.toml index 5560d3a..6959776 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -4,6 +4,9 @@ version.workspace = true edition.workspace = true publish.workspace = true +[lib] +crate-type = ["cdylib", "rlib"] + [dependencies] workspace = { path = "../crates/workspace" } assets = { path = "../crates/assets" } @@ -21,15 +24,15 @@ gpui_platform.workspace = true gpui_web = { git = "https://github.com/zed-industries/zed" } log.workspace = true +instant = { workspace = true, features = ["wasm-bindgen"] } console_error_panic_hook = "0.1" tracing-wasm = "0.2" console_log = "1.0" wasm-bindgen = "0.2" +universal-time = { git = "https://github.com/shadowylab/universal-time" } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom_0_2 = { package = "getrandom", version = "0.2", features = ["js"] } getrandom_0_3 = { package = "getrandom", version = "0.3", features = ["wasm_js"] } getrandom_0_4 = { package = "getrandom", version = "0.4", features = ["wasm_js"] } -tokio = { version = "1", default-features = false, features = ["sync", "macros", "io-util", "rt", "time"] } -errno = { version = "0.3.14", default-features = false } diff --git a/web/Makefile b/web/Makefile index 0b2a314..df98c60 100644 --- a/web/Makefile +++ b/web/Makefile @@ -38,7 +38,7 @@ preview: ## Preview production build clean: ## Clean build artifacts @echo "Cleaning build artifacts..." @rm -rf www/dist - @rm -rf www/src/wasm/*.js www/src/wasm/*.wasm + @rm -rf www/wasm/*.js www/wasm/*.wasm @cargo clean watch-wasm: ## Watch Rust code changes and auto-rebuild WASM diff --git a/web/script/build-wasm.sh b/web/script/build-wasm.sh index 7ef3e53..220b8bf 100755 --- a/web/script/build-wasm.sh +++ b/web/script/build-wasm.sh @@ -20,9 +20,26 @@ if [[ "$1" == "--release" ]]; then echo -e "${YELLOW}Building in release mode${NC}" fi +# macOS: ensure LLVM from Homebrew is available +if [[ "$(uname)" == "Darwin" ]]; then + echo -e "${GREEN}Detected macOS, setting up LLVM...${NC}" + if brew list llvm &>/dev/null; then + echo -e "${GREEN}LLVM already installed, skipping install${NC}" + else + echo -e "${YELLOW}LLVM not found, installing via Homebrew...${NC}" + brew install llvm + fi + LLVM_PATH=$(brew --prefix llvm) + export AR="${LLVM_PATH}/bin/llvm-ar" + export CC="${LLVM_PATH}/bin/clang" + echo -e "${GREEN}LLVM path: ${LLVM_PATH}${NC}" +fi + # Step 1: Build WASM echo -e "${GREEN}Step 1: Building WASM...${NC}" cd "$PROJECT_ROOT" +export CARGO_TARGET_DIR="$PROJECT_ROOT/target" +RUSTFLAGS='-C target-feature=+bulk-memory -C link-arg=--max-memory=4294967296' \ cargo build --target wasm32-unknown-unknown $RELEASE_FLAG # Determine the build directory @@ -33,8 +50,7 @@ else fi # WASM file is in workspace target directory -WORKSPACE_ROOT="$PROJECT_ROOT/../.." -WASM_PATH="$WORKSPACE_ROOT/target/wasm32-unknown-unknown/$BUILD_MODE/coop_web.wasm" +WASM_PATH="$PROJECT_ROOT/target/wasm32-unknown-unknown/$BUILD_MODE/coop_web.wasm" # Check if WASM file exists if [[ ! -f "$WASM_PATH" ]]; then @@ -45,7 +61,7 @@ fi # Step 2: Generate JavaScript bindings echo -e "${GREEN}Step 2: Generating JavaScript bindings...${NC}" wasm-bindgen "$WASM_PATH" \ - --out-dir "$PROJECT_ROOT/www/src/wasm" \ + --out-dir "$PROJECT_ROOT/www/wasm" \ --target web \ --no-typescript diff --git a/web/src/lib.rs b/web/src/lib.rs index ca86a18..d2a02a4 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -1,7 +1,24 @@ use gpui::*; use ui::Root; +use universal_time::{Instant, MonotonicClock, SystemTime, WallClock, define_time_provider}; use wasm_bindgen::prelude::*; +struct CustomTimeProvider; + +impl WallClock for CustomTimeProvider { + fn system_time(&self) -> SystemTime { + SystemTime::from_unix_duration(instant::Duration::from_secs(0)) + } +} + +impl MonotonicClock for CustomTimeProvider { + fn instant(&self) -> Instant { + Instant::from_ticks(instant::Duration::from_secs(0)) + } +} + +define_time_provider!(CustomTimeProvider); + #[wasm_bindgen] pub fn run() -> Result<(), JsValue> { console_error_panic_hook::set_once(); @@ -15,37 +32,51 @@ pub fn run() -> Result<(), JsValue> { #[cfg(target_family = "wasm")] gpui_platform::web_init(); - gpui_platform::application().run(|cx| { + #[cfg(not(target_family = "wasm"))] + let app = gpui_platform::application(); + + #[cfg(target_family = "wasm")] + let app = { + let app = gpui_platform::single_threaded_web(); + + // Temporary fix: intentionally leak the `Rc` to keep the application alive + struct WasmApplication(std::rc::Rc); + let wasm_app = unsafe { std::mem::transmute::(app) }; + std::mem::forget(wasm_app.0.clone()); + unsafe { std::mem::transmute::(wasm_app) } + }; + + app.run(|cx| { + // Open the root window cx.open_window(WindowOptions::default(), |window, cx| { - cx.new(|cx| { - // Initialize components - ui::init(cx); + // Initialize components + ui::init(cx); - // Initialize theme registry - theme::init(cx); + // Initialize theme registry + theme::init(cx); - // Initialize settings - settings::init(window, cx); + // Initialize settings + settings::init(window, cx); - // Initialize the nostr client - state::init(window, cx); + // Initialize the nostr client + state::init(window, cx); - // Initialize person registry - person::init(window, cx); + // Initialize person registry + person::init(window, cx); - // Initialize device signer - // - // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md - device::init(window, cx); + // Initialize device signer + // + // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + device::init(window, cx); - // Initialize app registry - chat::init(window, cx); + // Initialize app registry + chat::init(window, cx); - // Root Entity - Root::new(workspace::init(window, cx).into(), window, cx) - }) + // Root view + cx.new(|cx| Root::new(workspace::init(window, cx).into(), window, cx)) }) .expect("Failed to open window. Please restart the application."); + cx.activate(true); }); diff --git a/web/www/deno.lock b/web/www/deno.lock new file mode 100644 index 0000000..8378c05 --- /dev/null +++ b/web/www/deno.lock @@ -0,0 +1,396 @@ +{ + "version": "5", + "specifiers": { + "npm:vite-plugin-static-copy@^3.2.0": "3.4.0_vite@8.1.5", + "npm:vite-plugin-wasm@^3.3.0": "3.6.0_vite@8.1.5", + "npm:vite@8": "8.1.5" + }, + "npm": { + "@emnapi/core@1.11.1": { + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dependencies": [ + "@emnapi/wasi-threads", + "tslib" + ] + }, + "@emnapi/runtime@1.11.1": { + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dependencies": [ + "tslib" + ] + }, + "@emnapi/wasi-threads@1.2.2": { + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dependencies": [ + "tslib" + ] + }, + "@napi-rs/wasm-runtime@1.1.6_@emnapi+core@1.11.1_@emnapi+runtime@1.11.1": { + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dependencies": [ + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util" + ] + }, + "@oxc-project/types@0.139.0": { + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==" + }, + "@rolldown/binding-android-arm64@1.1.5": { + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@rolldown/binding-darwin-arm64@1.1.5": { + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@rolldown/binding-darwin-x64@1.1.5": { + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@rolldown/binding-freebsd-x64@1.1.5": { + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@rolldown/binding-linux-arm-gnueabihf@1.1.5": { + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@rolldown/binding-linux-arm64-gnu@1.1.5": { + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rolldown/binding-linux-arm64-musl@1.1.5": { + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rolldown/binding-linux-ppc64-gnu@1.1.5": { + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@rolldown/binding-linux-s390x-gnu@1.1.5": { + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@rolldown/binding-linux-x64-gnu@1.1.5": { + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rolldown/binding-linux-x64-musl@1.1.5": { + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rolldown/binding-openharmony-arm64@1.1.5": { + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@rolldown/binding-wasm32-wasi@1.1.5": { + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "dependencies": [ + "@emnapi/core", + "@emnapi/runtime", + "@napi-rs/wasm-runtime" + ], + "cpu": ["wasm32"] + }, + "@rolldown/binding-win32-arm64-msvc@1.1.5": { + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@rolldown/binding-win32-x64-msvc@1.1.5": { + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@rolldown/pluginutils@1.0.1": { + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==" + }, + "@tybys/wasm-util@0.10.3": { + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dependencies": [ + "tslib" + ] + }, + "anymatch@3.1.3": { + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": [ + "normalize-path", + "picomatch@2.3.2" + ] + }, + "binary-extensions@2.3.0": { + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==" + }, + "braces@3.0.3": { + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": [ + "fill-range" + ] + }, + "chokidar@3.6.0": { + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": [ + "anymatch", + "braces", + "glob-parent", + "is-binary-path", + "is-glob", + "normalize-path", + "readdirp" + ], + "optionalDependencies": [ + "fsevents" + ] + }, + "detect-libc@2.1.2": { + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" + }, + "fdir@6.5.0_picomatch@4.0.5": { + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dependencies": [ + "picomatch@4.0.5" + ], + "optionalPeers": [ + "picomatch@4.0.5" + ] + }, + "fill-range@7.1.1": { + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": [ + "to-regex-range" + ] + }, + "fsevents@2.3.3": { + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "os": ["darwin"], + "scripts": true + }, + "glob-parent@5.1.2": { + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": [ + "is-glob" + ] + }, + "is-binary-path@2.1.0": { + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": [ + "binary-extensions" + ] + }, + "is-extglob@2.1.1": { + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-glob@4.0.3": { + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": [ + "is-extglob" + ] + }, + "is-number@7.0.0": { + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "lightningcss-android-arm64@1.33.0": { + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "os": ["android"], + "cpu": ["arm64"] + }, + "lightningcss-darwin-arm64@1.33.0": { + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "lightningcss-darwin-x64@1.33.0": { + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "lightningcss-freebsd-x64@1.33.0": { + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "lightningcss-linux-arm-gnueabihf@1.33.0": { + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "os": ["linux"], + "cpu": ["arm"] + }, + "lightningcss-linux-arm64-gnu@1.33.0": { + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "lightningcss-linux-arm64-musl@1.33.0": { + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "lightningcss-linux-x64-gnu@1.33.0": { + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "os": ["linux"], + "cpu": ["x64"] + }, + "lightningcss-linux-x64-musl@1.33.0": { + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "os": ["linux"], + "cpu": ["x64"] + }, + "lightningcss-win32-arm64-msvc@1.33.0": { + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "lightningcss-win32-x64-msvc@1.33.0": { + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "os": ["win32"], + "cpu": ["x64"] + }, + "lightningcss@1.33.0": { + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dependencies": [ + "detect-libc" + ], + "optionalDependencies": [ + "lightningcss-android-arm64", + "lightningcss-darwin-arm64", + "lightningcss-darwin-x64", + "lightningcss-freebsd-x64", + "lightningcss-linux-arm-gnueabihf", + "lightningcss-linux-arm64-gnu", + "lightningcss-linux-arm64-musl", + "lightningcss-linux-x64-gnu", + "lightningcss-linux-x64-musl", + "lightningcss-win32-arm64-msvc", + "lightningcss-win32-x64-msvc" + ] + }, + "nanoid@3.3.16": { + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "bin": true + }, + "normalize-path@3.0.0": { + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "p-map@7.0.6": { + "integrity": "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==" + }, + "picocolors@1.1.1": { + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch@2.3.2": { + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==" + }, + "picomatch@4.0.5": { + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==" + }, + "postcss@8.5.23": { + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "dependencies": [ + "nanoid", + "picocolors", + "source-map-js" + ] + }, + "readdirp@3.6.0": { + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": [ + "picomatch@2.3.2" + ] + }, + "rolldown@1.1.5": { + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dependencies": [ + "@oxc-project/types", + "@rolldown/pluginutils" + ], + "optionalDependencies": [ + "@rolldown/binding-android-arm64", + "@rolldown/binding-darwin-arm64", + "@rolldown/binding-darwin-x64", + "@rolldown/binding-freebsd-x64", + "@rolldown/binding-linux-arm-gnueabihf", + "@rolldown/binding-linux-arm64-gnu", + "@rolldown/binding-linux-arm64-musl", + "@rolldown/binding-linux-ppc64-gnu", + "@rolldown/binding-linux-s390x-gnu", + "@rolldown/binding-linux-x64-gnu", + "@rolldown/binding-linux-x64-musl", + "@rolldown/binding-openharmony-arm64", + "@rolldown/binding-wasm32-wasi", + "@rolldown/binding-win32-arm64-msvc", + "@rolldown/binding-win32-x64-msvc" + ], + "bin": true + }, + "source-map-js@1.2.1": { + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "tinyglobby@0.2.17": { + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dependencies": [ + "fdir", + "picomatch@4.0.5" + ] + }, + "to-regex-range@5.0.1": { + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": [ + "is-number" + ] + }, + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "vite-plugin-static-copy@3.4.0_vite@8.1.5": { + "integrity": "sha512-ekryzCw0ouAOE8tw4RvVL/dfqguXzumsV3FBKoKso4MQ1MUUrUXtl5RI4KpJQUNGqFEsg9kxl4EvDl02YtA9VQ==", + "dependencies": [ + "chokidar", + "p-map", + "picocolors", + "tinyglobby", + "vite" + ] + }, + "vite-plugin-wasm@3.6.0_vite@8.1.5": { + "integrity": "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==", + "dependencies": [ + "vite" + ] + }, + "vite@8.1.5": { + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dependencies": [ + "lightningcss", + "picomatch@4.0.5", + "postcss", + "rolldown", + "tinyglobby" + ], + "optionalDependencies": [ + "fsevents" + ], + "bin": true + } + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:vite-plugin-static-copy@^3.2.0", + "npm:vite-plugin-wasm@^3.3.0", + "npm:vite@8" + ] + } + } +} diff --git a/web/src/www/index.html b/web/www/index.html similarity index 100% rename from web/src/www/index.html rename to web/www/index.html diff --git a/web/src/www/main.js b/web/www/main.js similarity index 100% rename from web/src/www/main.js rename to web/www/main.js diff --git a/web/src/www/package.json b/web/www/package.json similarity index 100% rename from web/src/www/package.json rename to web/www/package.json diff --git a/web/src/www/vite.config.js b/web/www/vite.config.js similarity index 97% rename from web/src/www/vite.config.js rename to web/www/vite.config.js index 34f3075..b5b0203 100644 --- a/web/src/www/vite.config.js +++ b/web/www/vite.config.js @@ -1,5 +1,4 @@ import { defineConfig } from "vite"; -import wasm from "vite-plugin-wasm"; import { viteStaticCopy } from "vite-plugin-static-copy"; import path from "path"; import { fileURLToPath } from "url"; @@ -8,7 +7,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); export default defineConfig({ plugins: [ - wasm(), viteStaticCopy({ targets: [ {