7 Commits
Author SHA1 Message Date
reya 880ba30d20 chore: bump version 2025-09-28 08:01:41 +07:00
reyaandGitHub d889f9b25d chore: always call get_public_key on nip46 (#171)
* get public key on login

* .
2025-09-28 07:57:04 +07:00
reyaandGitHub 0de1b20951 feat: add context menu for quick profile viewing (#170)
* add profile context menu

* add context menu for avatar
2025-09-27 15:15:00 +07:00
reyaandGitHub 338a947b57 chore: fix duplicate reply (#169)
* prevent duplicate reply

* .
2025-09-27 08:02:16 +07:00
reyaandGitHub 98ce928f0c chore: fix double message on sent (#166)
* .

* fix

* update
2025-09-26 14:17:31 +07:00
reyaandGitHub 61cad5dd96 chore: refactor the input component (#165)
* refactor the input component

* fix clippy

* clean up
2025-09-25 08:03:14 +07:00
reya a87184214f chore: add release script 2025-09-23 09:36:49 +07:00
36 changed files with 2968 additions and 1899 deletions
Generated
+164 -123
View File
@@ -4,9 +4,9 @@ version = 4
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.24.2" version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
dependencies = [ dependencies = [
"gimli", "gimli",
] ]
@@ -190,7 +190,7 @@ dependencies = [
[[package]] [[package]]
name = "assets" name = "assets"
version = "0.2.9" version = "0.2.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -224,9 +224,9 @@ dependencies = [
[[package]] [[package]]
name = "async-compression" name = "async-compression"
version = "0.4.30" version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "977eb15ea9efd848bb8a4a1a2500347ed7f0bf794edf0dc3ddcf439f43d36b23" checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0"
dependencies = [ dependencies = [
"compression-codecs", "compression-codecs",
"compression-core", "compression-core",
@@ -275,7 +275,7 @@ dependencies = [
"polling", "polling",
"rustix 1.1.2", "rustix 1.1.2",
"slab", "slab",
"windows-sys 0.61.0", "windows-sys 0.61.1",
] ]
[[package]] [[package]]
@@ -344,7 +344,7 @@ dependencies = [
"rustix 1.1.2", "rustix 1.1.2",
"signal-hook-registry", "signal-hook-registry",
"slab", "slab",
"windows-sys 0.61.0", "windows-sys 0.61.1",
] ]
[[package]] [[package]]
@@ -428,7 +428,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "auto_update" name = "auto_update"
version = "0.2.9" version = "0.2.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo-packager-updater", "cargo-packager-updater",
@@ -472,9 +472,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-lc-rs" name = "aws-lc-rs"
version = "1.14.0" version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b8ff6c09cd57b16da53641caa860168b88c172a5ee163b0288d3d6eea12786" checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d"
dependencies = [ dependencies = [
"aws-lc-sys", "aws-lc-sys",
"zeroize", "zeroize",
@@ -482,22 +482,23 @@ dependencies = [
[[package]] [[package]]
name = "aws-lc-sys" name = "aws-lc-sys"
version = "0.31.0" version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e44d16778acaf6a9ec9899b92cebd65580b83f685446bf2e1f5d3d732f99dcd" checksum = "1ba2e2516bdf37af57fc6ff047855f54abad0066e5c4fdaaeb76dabb2e05bcf5"
dependencies = [ dependencies = [
"bindgen 0.72.1", "bindgen 0.72.1",
"cc", "cc",
"cmake", "cmake",
"dunce", "dunce",
"fs_extra", "fs_extra",
"libloading",
] ]
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.75" version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
dependencies = [ dependencies = [
"addr2line", "addr2line",
"cfg-if", "cfg-if",
@@ -505,7 +506,7 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
"object", "object",
"rustc-demangle", "rustc-demangle",
"windows-targets 0.52.6", "windows-link 0.2.0",
] ]
[[package]] [[package]]
@@ -909,9 +910,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.38" version = "1.2.39"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@@ -1026,7 +1027,7 @@ dependencies = [
[[package]] [[package]]
name = "client_keys" name = "client_keys"
version = "0.2.9" version = "0.2.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"global", "global",
@@ -1128,7 +1129,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
@@ -1163,7 +1164,7 @@ dependencies = [
[[package]] [[package]]
name = "common" name = "common"
version = "0.2.9" version = "0.2.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -1184,9 +1185,9 @@ dependencies = [
[[package]] [[package]]
name = "compression-codecs" name = "compression-codecs"
version = "0.4.30" version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "485abf41ac0c8047c07c87c72c8fb3eb5197f6e9d7ded615dfd1a00ae00a0f64" checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23"
dependencies = [ dependencies = [
"compression-core", "compression-core",
"deflate64", "deflate64",
@@ -1236,7 +1237,7 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]] [[package]]
name = "coop" name = "coop"
version = "0.2.9" version = "0.2.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assets", "assets",
@@ -1546,9 +1547,9 @@ checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.5.3" version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071"
dependencies = [ dependencies = [
"powerfmt", "powerfmt",
] ]
@@ -1569,7 +1570,7 @@ dependencies = [
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1837,7 +1838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.0", "windows-sys 0.61.1",
] ]
[[package]] [[package]]
@@ -1979,9 +1980,9 @@ checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
[[package]] [[package]]
name = "flatbuffers" name = "flatbuffers"
version = "25.2.10" version = "25.9.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1" checksum = "09b6620799e7340ebd9968d2e0708eb82cf1971e9a16821e2091b6d6e475eed5"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.9.4",
"rustc_version", "rustc_version",
@@ -2015,6 +2016,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
[[package]]
name = "fluent-uri"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
dependencies = [
"bitflags 1.3.2",
]
[[package]] [[package]]
name = "flume" name = "flume"
version = "0.11.1" version = "0.11.1"
@@ -2391,9 +2401,9 @@ dependencies = [
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.31.1" version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]] [[package]]
name = "glob" name = "glob"
@@ -2403,7 +2413,7 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]] [[package]]
name = "global" name = "global"
version = "0.2.9" version = "0.2.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dirs 5.0.1", "dirs 5.0.1",
@@ -2496,7 +2506,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
@@ -2590,7 +2600,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macros" name = "gpui_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -2602,7 +2612,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_tokio" name = "gpui_tokio"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -2822,7 +2832,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client" name = "http_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -2842,7 +2852,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client_tls" name = "http_client_tls"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-platform-verifier", "rustls-platform-verifier",
@@ -2945,7 +2955,7 @@ dependencies = [
[[package]] [[package]]
name = "i18n" name = "i18n"
version = "0.2.9" version = "0.2.10"
dependencies = [ dependencies = [
"rust-i18n", "rust-i18n",
] ]
@@ -2962,7 +2972,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"wasm-bindgen", "wasm-bindgen",
"windows-core 0.62.0", "windows-core 0.62.1",
] ]
[[package]] [[package]]
@@ -3139,9 +3149,9 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
[[package]] [[package]]
name = "imgref" name = "imgref"
version = "1.11.0" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
@@ -3340,9 +3350,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.80" version = "0.3.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@@ -3401,9 +3411,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.175" version = "0.2.176"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
[[package]] [[package]]
name = "libfuzzer-sys" name = "libfuzzer-sys"
@@ -3417,12 +3427,12 @@ dependencies = [
[[package]] [[package]]
name = "libloading" name = "libloading"
version = "0.8.9" version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-link 0.2.0", "windows-targets 0.53.4",
] ]
[[package]] [[package]]
@@ -3544,6 +3554,19 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "lsp-types"
version = "0.97.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071"
dependencies = [
"bitflags 1.3.2",
"fluent-uri",
"serde",
"serde_json",
"serde_repr",
]
[[package]] [[package]]
name = "lyon" name = "lyon"
version = "1.0.16" version = "1.0.16"
@@ -3634,7 +3657,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen 0.71.1", "bindgen 0.71.1",
@@ -3649,9 +3672,9 @@ dependencies = [
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.5" version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]] [[package]]
name = "memmap2" name = "memmap2"
@@ -3876,13 +3899,13 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b" checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b"
dependencies = [ dependencies = [
"windows-sys 0.61.0", "windows-sys 0.61.1",
] ]
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#418e34bcab62869c80cd86bf7c6ace1b9960b09d" source = "git+https://github.com/rust-nostr/nostr#75345c65c645d33d15f6a5ec10fa6ffd0d8f4cac"
dependencies = [ dependencies = [
"aes", "aes",
"base64", "base64",
@@ -3906,7 +3929,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#418e34bcab62869c80cd86bf7c6ace1b9960b09d" source = "git+https://github.com/rust-nostr/nostr#75345c65c645d33d15f6a5ec10fa6ffd0d8f4cac"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -3918,7 +3941,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-database" name = "nostr-database"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#418e34bcab62869c80cd86bf7c6ace1b9960b09d" source = "git+https://github.com/rust-nostr/nostr#75345c65c645d33d15f6a5ec10fa6ffd0d8f4cac"
dependencies = [ dependencies = [
"flatbuffers", "flatbuffers",
"lru", "lru",
@@ -3929,7 +3952,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-lmdb" name = "nostr-lmdb"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#418e34bcab62869c80cd86bf7c6ace1b9960b09d" source = "git+https://github.com/rust-nostr/nostr#75345c65c645d33d15f6a5ec10fa6ffd0d8f4cac"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"flume", "flume",
@@ -3943,7 +3966,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-relay-pool" name = "nostr-relay-pool"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#418e34bcab62869c80cd86bf7c6ace1b9960b09d" source = "git+https://github.com/rust-nostr/nostr#75345c65c645d33d15f6a5ec10fa6ffd0d8f4cac"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@@ -3960,7 +3983,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#418e34bcab62869c80cd86bf7c6ace1b9960b09d" source = "git+https://github.com/rust-nostr/nostr#75345c65c645d33d15f6a5ec10fa6ffd0d8f4cac"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -4236,9 +4259,9 @@ dependencies = [
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.7" version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -4477,7 +4500,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "perf" name = "perf"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"collections", "collections",
"serde", "serde",
@@ -4619,7 +4642,7 @@ dependencies = [
"hermit-abi", "hermit-abi",
"pin-project-lite", "pin-project-lite",
"rustix 1.1.2", "rustix 1.1.2",
"windows-sys 0.61.0", "windows-sys 0.61.1",
] ]
[[package]] [[package]]
@@ -5085,7 +5108,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
"workspace-hack", "workspace-hack",
@@ -5093,9 +5116,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.2" version = "1.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -5105,9 +5128,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.10" version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -5122,7 +5145,7 @@ checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]] [[package]]
name = "registry" name = "registry"
version = "0.2.9" version = "0.2.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"common", "common",
@@ -5239,7 +5262,7 @@ dependencies = [
[[package]] [[package]]
name = "reqwest_client" name = "reqwest_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -5291,6 +5314,21 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rope"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [
"arrayvec",
"log",
"rayon",
"smallvec",
"sum_tree",
"unicode-segmentation",
"util",
"workspace-hack",
]
[[package]] [[package]]
name = "roxmltree" name = "roxmltree"
version = "0.20.0" version = "0.20.0"
@@ -5446,7 +5484,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.11.0", "linux-raw-sys 0.11.0",
"windows-sys 0.61.0", "windows-sys 0.61.1",
] ]
[[package]] [[package]]
@@ -5627,7 +5665,7 @@ version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
dependencies = [ dependencies = [
"windows-sys 0.61.0", "windows-sys 0.61.1",
] ]
[[package]] [[package]]
@@ -5774,7 +5812,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
[[package]] [[package]]
name = "semantic_version" name = "semantic_version"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@@ -5789,9 +5827,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.226" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [ dependencies = [
"serde_core", "serde_core",
"serde_derive", "serde_derive",
@@ -5799,18 +5837,18 @@ dependencies = [
[[package]] [[package]]
name = "serde_core" name = "serde_core"
version = "1.0.226" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.226" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -5920,7 +5958,7 @@ dependencies = [
[[package]] [[package]]
name = "settings" name = "settings"
version = "0.2.9" version = "0.2.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"global", "global",
@@ -5987,7 +6025,7 @@ dependencies = [
[[package]] [[package]]
name = "signer_proxy" name = "signer_proxy"
version = "0.2.9" version = "0.2.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"atomic-destructor", "atomic-destructor",
@@ -6226,7 +6264,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sum_tree" name = "sum_tree"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@@ -6496,15 +6534,15 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.22.0" version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [ dependencies = [
"fastrand 2.3.0", "fastrand 2.3.0",
"getrandom 0.3.3", "getrandom 0.3.3",
"once_cell", "once_cell",
"rustix 1.1.2", "rustix 1.1.2",
"windows-sys 0.61.0", "windows-sys 0.61.1",
] ]
[[package]] [[package]]
@@ -6529,7 +6567,7 @@ dependencies = [
[[package]] [[package]]
name = "theme" name = "theme"
version = "0.2.9" version = "0.2.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -6692,7 +6730,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "title_bar" name = "title_bar"
version = "0.2.9" version = "0.2.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"common", "common",
@@ -6750,9 +6788,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.26.3" version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [ dependencies = [
"rustls", "rustls",
"tokio", "tokio",
@@ -7075,7 +7113,7 @@ dependencies = [
[[package]] [[package]]
name = "ui" name = "ui"
version = "0.2.9" version = "0.2.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"common", "common",
@@ -7086,15 +7124,18 @@ dependencies = [
"itertools 0.13.0", "itertools 0.13.0",
"linkify", "linkify",
"log", "log",
"lsp-types",
"nostr-sdk", "nostr-sdk",
"once_cell", "once_cell",
"regex", "regex",
"registry", "registry",
"rope",
"rust-i18n", "rust-i18n",
"serde", "serde",
"serde_json", "serde_json",
"smallvec", "smallvec",
"smol", "smol",
"sum_tree",
"theme", "theme",
"unicode-segmentation", "unicode-segmentation",
"uuid", "uuid",
@@ -7269,7 +7310,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs", "async-fs",
@@ -7304,7 +7345,7 @@ dependencies = [
[[package]] [[package]]
name = "util_macros" name = "util_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#681a4adc42df0c93b95fd1a36a3ad47eaca086c0" source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263"
dependencies = [ dependencies = [
"perf", "perf",
"quote", "quote",
@@ -7473,9 +7514,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.103" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -7486,9 +7527,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.103" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
@@ -7500,9 +7541,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.53" version = "0.4.54"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
@@ -7513,9 +7554,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.103" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -7523,9 +7564,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.103" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -7536,9 +7577,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.103" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -7643,9 +7684,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.80" version = "0.3.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -7752,7 +7793,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.61.0", "windows-sys 0.61.1",
] ]
[[package]] [[package]]
@@ -7824,8 +7865,8 @@ version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [ dependencies = [
"windows-implement 0.60.0", "windows-implement 0.60.1",
"windows-interface 0.59.1", "windows-interface 0.59.2",
"windows-link 0.1.3", "windows-link 0.1.3",
"windows-result 0.3.4", "windows-result 0.3.4",
"windows-strings 0.4.2", "windows-strings 0.4.2",
@@ -7833,12 +7874,12 @@ dependencies = [
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.62.0" version = "0.62.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9"
dependencies = [ dependencies = [
"windows-implement 0.60.0", "windows-implement 0.60.1",
"windows-interface 0.59.1", "windows-interface 0.59.2",
"windows-link 0.2.0", "windows-link 0.2.0",
"windows-result 0.4.0", "windows-result 0.4.0",
"windows-strings 0.5.0", "windows-strings 0.5.0",
@@ -7868,9 +7909,9 @@ dependencies = [
[[package]] [[package]]
name = "windows-implement" name = "windows-implement"
version = "0.60.0" version = "0.60.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -7890,9 +7931,9 @@ dependencies = [
[[package]] [[package]]
name = "windows-interface" name = "windows-interface"
version = "0.59.1" version = "0.59.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -7929,7 +7970,7 @@ checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [ dependencies = [
"windows-result 0.3.4", "windows-result 0.3.4",
"windows-strings 0.3.1", "windows-strings 0.3.1",
"windows-targets 0.53.3", "windows-targets 0.53.4",
] ]
[[package]] [[package]]
@@ -8039,14 +8080,14 @@ version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [ dependencies = [
"windows-targets 0.53.3", "windows-targets 0.53.4",
] ]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.0" version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f"
dependencies = [ dependencies = [
"windows-link 0.2.0", "windows-link 0.2.0",
] ]
@@ -8099,11 +8140,11 @@ dependencies = [
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.53.3" version = "0.53.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b"
dependencies = [ dependencies = [
"windows-link 0.1.3", "windows-link 0.2.0",
"windows_aarch64_gnullvm 0.53.0", "windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0", "windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0", "windows_i686_gnu 0.53.0",
+1 -1
View File
@@ -4,7 +4,7 @@ members = ["crates/*"]
default-members = ["crates/coop"] default-members = ["crates/coop"]
[workspace.package] [workspace.package]
version = "0.2.9" version = "0.2.10"
edition = "2021" edition = "2021"
publish = false publish = false
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17.75 19.25h2.596c1.163 0 2.106-1.001 1.788-2.12-.733-2.573-2.465-4.38-5.134-4.38-.446 0-.866.05-1.26.147M11.25 7a3.25 3.25 0 1 1-6.5 0 3.25 3.25 0 0 1 6.5 0Zm8.5.5a2.75 2.75 0 1 1-5.5 0 2.75 2.75 0 0 1 5.5 0ZM2.08 18.126c.78-3.14 2.78-5.376 5.92-5.376s5.14 2.237 5.918 5.376c.28 1.128-.658 2.124-1.82 2.124H3.901c-1.162 0-2.1-.996-1.82-2.124Z"/>
</svg>

After

Width:  |  Height:  |  Size: 550 B

+1 -1
View File
@@ -14,7 +14,7 @@ product-name = "Coop"
description = "Chat Freely, Stay Private on Nostr" description = "Chat Freely, Stay Private on Nostr"
identifier = "su.reya.coop" identifier = "su.reya.coop"
category = "SocialNetworking" category = "SocialNetworking"
version = "0.2.9" version = "0.2.10"
out-dir = "../../dist" out-dir = "../../dist"
before-packaging-command = "cargo build --release" before-packaging-command = "cargo build --release"
resources = ["Cargo.toml", "src"] resources = ["Cargo.toml", "src"]
+14 -9
View File
@@ -16,8 +16,8 @@ use global::constants::{
use global::{app_state, nostr_client, AuthRequest, Notice, SignalKind, UnwrappingStatus}; use global::{app_state, nostr_client, AuthRequest, Notice, SignalKind, UnwrappingStatus};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
deferred, div, px, rems, App, AppContext, AsyncWindowContext, Axis, Context, Entity, deferred, div, px, rems, App, AppContext, AsyncWindowContext, Axis, ClipboardItem, Context,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window, StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window,
}; };
use i18n::{shared_t, t}; use i18n::{shared_t, t};
@@ -30,7 +30,7 @@ use signer_proxy::{BrowserSignerProxy, BrowserSignerProxyOptions};
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use theme::{ActiveTheme, Theme, ThemeMode}; use theme::{ActiveTheme, Theme, ThemeMode};
use title_bar::TitleBar; use title_bar::TitleBar;
use ui::actions::OpenProfile; use ui::actions::{CopyPublicKey, OpenPublicKey};
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::dock::DockPlacement; use ui::dock_area::dock::DockPlacement;
@@ -676,7 +676,7 @@ impl ChatSpace {
} }
/// Stores an unwrapped event in local database with reference to original /// Stores an unwrapped event in local database with reference to original
async fn set_unwrapped_event(root: EventId, unwrapped: &Event) -> Result<(), Error> { async fn set_unwrapped_event(gift_wrap: EventId, unwrapped: &Event) -> Result<(), Error> {
let client = nostr_client(); let client = nostr_client();
// Save unwrapped event // Save unwrapped event
@@ -684,7 +684,7 @@ impl ChatSpace {
// Create a reference event pointing to the unwrapped event // Create a reference event pointing to the unwrapped event
let event = EventBuilder::new(Kind::ApplicationSpecificData, "") let event = EventBuilder::new(Kind::ApplicationSpecificData, "")
.tags(vec![Tag::identifier(root), Tag::event(unwrapped.id)]) .tags(vec![Tag::identifier(gift_wrap), Tag::event(unwrapped.id)])
.sign(&Keys::generate()) .sign(&Keys::generate())
.await?; .await?;
@@ -744,8 +744,6 @@ impl ChatSpace {
match event.created_at >= app_state.init_at { match event.created_at >= app_state.init_at {
// New message: send a signal to notify the UI // New message: send a signal to notify the UI
true => { true => {
// A small delay to prevent UI flickering
smol::Timer::after(Duration::from_millis(200)).await;
app_state app_state
.signal .signal
.send(SignalKind::NewMessage((target.id, event))) .send(SignalKind::NewMessage((target.id, event)))
@@ -1179,7 +1177,7 @@ impl ChatSpace {
.detach(); .detach();
} }
fn on_open_profile(&mut self, ev: &OpenProfile, window: &mut Window, cx: &mut Context<Self>) { fn on_open_pubkey(&mut self, ev: &OpenPublicKey, window: &mut Window, cx: &mut Context<Self>) {
let public_key = ev.0; let public_key = ev.0;
let profile = user_profile::init(public_key, window, cx); let profile = user_profile::init(public_key, window, cx);
@@ -1197,6 +1195,12 @@ impl ChatSpace {
}); });
} }
fn on_copy_pubkey(&mut self, ev: &CopyPublicKey, window: &mut Window, cx: &mut Context<Self>) {
let Ok(bech32) = ev.0.to_bech32();
cx.write_to_clipboard(ClipboardItem::new_string(bech32));
window.push_notification(t!("common.copied"), cx);
}
fn render_proxy_modal(&mut self, window: &mut Window, cx: &mut App) { fn render_proxy_modal(&mut self, window: &mut Window, cx: &mut App) {
window.open_modal(cx, |this, _window, _cx| { window.open_modal(cx, |this, _window, _cx| {
this.overlay_closable(false) this.overlay_closable(false)
@@ -1498,7 +1502,8 @@ impl Render for ChatSpace {
.on_action(cx.listener(Self::on_settings)) .on_action(cx.listener(Self::on_settings))
.on_action(cx.listener(Self::on_dark_mode)) .on_action(cx.listener(Self::on_dark_mode))
.on_action(cx.listener(Self::on_sign_out)) .on_action(cx.listener(Self::on_sign_out))
.on_action(cx.listener(Self::on_open_profile)) .on_action(cx.listener(Self::on_open_pubkey))
.on_action(cx.listener(Self::on_copy_pubkey))
.on_action(cx.listener(Self::on_reload_metadata)) .on_action(cx.listener(Self::on_reload_metadata))
.relative() .relative()
.size_full() .size_full()
+77 -79
View File
@@ -1,4 +1,4 @@
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use common::display::{RenderedProfile, RenderedTimestamp}; use common::display::{RenderedProfile, RenderedTimestamp};
use common::nip96::nip96_upload; use common::nip96::nip96_upload;
@@ -24,8 +24,10 @@ use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use smol::fs; use smol::fs;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::actions::{CopyPublicKey, OpenPublicKey};
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::context_menu::ContextMenuExt;
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock_area::panel::{Panel, PanelEvent};
use ui::emoji_picker::EmojiPicker; use ui::emoji_picker::EmojiPicker;
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
@@ -61,7 +63,7 @@ pub struct Chat {
// New Message // New Message
input: Entity<InputState>, input: Entity<InputState>,
replies_to: Entity<Vec<EventId>>, replies_to: Entity<HashSet<EventId>>,
// Media Attachment // Media Attachment
attachments: Entity<Vec<Url>>, attachments: Entity<Vec<Url>>,
@@ -79,7 +81,7 @@ pub struct Chat {
impl Chat { impl Chat {
pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let attachments = cx.new(|_| vec![]); let attachments = cx.new(|_| vec![]);
let replies_to = cx.new(|_| vec![]); let replies_to = cx.new(|_| HashSet::new());
let relays = cx.new(|_| { let relays = cx.new(|_| {
let this: HashMap<PublicKey, Vec<RelayUrl>> = HashMap::new(); let this: HashMap<PublicKey, Vec<RelayUrl>> = HashMap::new();
@@ -89,11 +91,8 @@ impl Chat {
let input = cx.new(|cx| { let input = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
.placeholder(t!("chat.placeholder")) .placeholder(t!("chat.placeholder"))
.multi_line()
.prevent_new_line_on_enter()
.rows(1)
.multi_line()
.auto_grow(1, 20) .auto_grow(1, 20)
.prevent_new_line_on_enter()
.clean_on_escape() .clean_on_escape()
}); });
@@ -155,14 +154,8 @@ impl Chat {
&input, &input,
window, window,
move |this: &mut Self, _input, event, window, cx| { move |this: &mut Self, _input, event, window, cx| {
match event { if let InputEvent::PressEnter { .. } = event {
InputEvent::PressEnter { .. } => { this.send_message(window, cx);
this.send_message(window, cx);
}
InputEvent::Change(_) => {
// this.mention_popup(text, input, cx);
}
_ => {}
}; };
}, },
), ),
@@ -173,9 +166,21 @@ impl Chat {
cx.subscribe_in(&room, window, move |this, _, signal, window, cx| { cx.subscribe_in(&room, window, move |this, _, signal, window, cx| {
match signal { match signal {
RoomSignal::NewMessage((gift_wrap_id, event)) => { RoomSignal::NewMessage((gift_wrap_id, event)) => {
if !this.is_sent_by_coop(gift_wrap_id) { let gift_wrap_id = gift_wrap_id.to_owned();
this.insert_message(Message::user(event), false, cx); let message = Message::user(event);
}
cx.spawn_in(window, async move |this, cx| {
let app_state = app_state();
let sent_ids = app_state.sent_ids.read().await;
this.update_in(cx, |this, _window, cx| {
if !sent_ids.contains(&gift_wrap_id) {
this.insert_message(message, false, cx);
}
})
.ok();
})
.detach();
} }
RoomSignal::Refresh => { RoomSignal::Refresh => {
this.load_messages(window, cx); this.load_messages(window, cx);
@@ -251,20 +256,19 @@ impl Chat {
self._tasks.push( self._tasks.push(
// Run the task in the background // Run the task in the background
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
match load_messages.await { let result = load_messages.await;
Ok(events) => {
this.update(cx, |this, cx| { this.update_in(cx, |this, window, cx| {
match result {
Ok(events) => {
this.insert_messages(events, cx); this.insert_messages(events, cx);
}) }
.ok(); Err(e) => {
} window.push_notification(Notification::error(e.to_string()), cx);
Err(e) => { }
cx.update(|window, cx| { };
window.push_notification(e.to_string(), cx); })
}) .ok();
.ok();
}
};
}), }),
); );
} }
@@ -298,11 +302,6 @@ impl Chat {
content content
} }
/// Check if the event is sent by Coop
fn is_sent_by_coop(&self, gift_wrap_id: &EventId) -> bool {
app_state().sent_ids.read_blocking().contains(gift_wrap_id)
}
/// Send a message to all members of the chat /// Send a message to all members of the chat
fn send_message(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn send_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// Get the message which includes all attachments // Get the message which includes all attachments
@@ -314,17 +313,11 @@ impl Chat {
return; return;
} }
// Temporary disable input
self.input.update(cx, |this, cx| {
this.set_loading(true, cx);
this.set_disabled(true, cx);
});
// Get the backup setting // Get the backup setting
let backup = AppSettings::get_backup_messages(cx); let backup = AppSettings::get_backup_messages(cx);
// Get replies_to if it's present // Get replies_to if it's present
let replies = self.replies_to.read(cx).clone(); let replies = self.replies_to.read(cx).iter().copied().collect_vec();
// Get the current room entity // Get the current room entity
let room = self.room.read(cx); let room = self.room.read(cx);
@@ -337,29 +330,27 @@ impl Chat {
// Create a task for sending the message in the background // Create a task for sending the message in the background
let send_message = room.send_in_background(&content, replies, backup, cx); let send_message = room.send_in_background(&content, replies, backup, cx);
cx.defer_in(window, |this, window, cx| { // Optimistically update message list
// Optimistically update message list self.insert_message(Message::user(temp_message), true, cx);
this.insert_message(Message::user(temp_message), true, cx);
// Remove all replies // Remove all replies
this.remove_all_replies(cx); self.remove_all_replies(cx);
// remove all attachments // remove all attachments
this.remove_all_attachments(cx); self.remove_all_attachments(cx);
// Reset the input state // Reset the input state
this.input.update(cx, |this, cx| { self.input.update(cx, |this, cx| {
this.set_loading(false, cx); this.set_value("", window, cx);
this.set_disabled(false, cx);
this.set_value("", window, cx);
});
}); });
// Continue sending the message in the background // Continue sending the message in the background
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
match send_message.await { let result = send_message.await;
Ok(reports) => {
this.update(cx, |this, cx| { this.update_in(cx, |this, window, cx| {
match result {
Ok(reports) => {
this.room.update(cx, |this, cx| { this.room.update(cx, |this, cx| {
if this.kind != RoomKind::Ongoing { if this.kind != RoomKind::Ongoing {
// Update the room kind to ongoing // Update the room kind to ongoing
@@ -375,16 +366,13 @@ impl Chat {
this.reports_by_id.insert(temp_id, reports); this.reports_by_id.insert(temp_id, reports);
cx.notify(); cx.notify();
}) }
.ok(); Err(e) => {
}
Err(e) => {
cx.update(|window, cx| {
window.push_notification(e.to_string(), cx); window.push_notification(e.to_string(), cx);
}) }
.ok();
} }
} })
.ok();
}) })
.detach(); .detach();
} }
@@ -439,8 +427,9 @@ impl Chat {
item_ix: self.list_state.item_count(), item_ix: self.list_state.item_count(),
offset_in_item: px(0.0), offset_in_item: px(0.0),
}); });
cx.notify();
} }
cx.notify();
} }
} }
@@ -448,11 +437,12 @@ impl Chat {
fn insert_messages(&mut self, events: Vec<Event>, cx: &mut Context<Self>) { fn insert_messages(&mut self, events: Vec<Event>, cx: &mut Context<Self>) {
for event in events.into_iter() { for event in events.into_iter() {
let m = Message::user(event); let m = Message::user(event);
// Bulk inserting messages, so no need to scroll to the latest message
self.insert_message(m, false, cx); self.insert_message(m, false, cx);
} }
cx.notify();
} }
/// Insert a warning message into the chat panel
fn insert_warning(&mut self, content: impl Into<String>, cx: &mut Context<Self>) { fn insert_warning(&mut self, content: impl Into<String>, cx: &mut Context<Self>) {
let m = Message::warning(content.into()); let m = Message::warning(content.into());
self.insert_message(m, true, cx); self.insert_message(m, true, cx);
@@ -515,7 +505,7 @@ impl Chat {
fn reply_to(&mut self, id: &EventId, cx: &mut Context<Self>) { fn reply_to(&mut self, id: &EventId, cx: &mut Context<Self>) {
if let Some(text) = self.message(id) { if let Some(text) = self.message(id) {
self.replies_to.update(cx, |this, cx| { self.replies_to.update(cx, |this, cx| {
this.push(text.id); this.insert(text.id);
cx.notify(); cx.notify();
}); });
} }
@@ -523,16 +513,14 @@ impl Chat {
fn remove_reply(&mut self, id: &EventId, cx: &mut Context<Self>) { fn remove_reply(&mut self, id: &EventId, cx: &mut Context<Self>) {
self.replies_to.update(cx, |this, cx| { self.replies_to.update(cx, |this, cx| {
if let Some(ix) = this.iter().position(|this| this == id) { this.remove(id);
this.remove(ix); cx.notify();
cx.notify();
}
}); });
} }
fn remove_all_replies(&mut self, cx: &mut Context<Self>) { fn remove_all_replies(&mut self, cx: &mut Context<Self>) {
self.replies_to.update(cx, |this, cx| { self.replies_to.update(cx, |this, cx| {
*this = vec![]; this.clear();
cx.notify(); cx.notify();
}); });
} }
@@ -707,6 +695,7 @@ impl Chat {
let id = message.id; let id = message.id;
let author = self.profile(&message.author, cx); let author = self.profile(&message.author, cx);
let public_key = author.public_key();
let replies = message.replies_to.as_slice(); let replies = message.replies_to.as_slice();
let has_replies = !replies.is_empty(); let has_replies = !replies.is_empty();
@@ -729,7 +718,18 @@ impl Chat {
.flex() .flex()
.gap_3() .gap_3()
.when(!hide_avatar, |this| { .when(!hide_avatar, |this| {
this.child(Avatar::new(author.avatar(proxy)).size(rems(2.))) this.child(
div()
.id(SharedString::from(format!("{ix}-avatar")))
.child(Avatar::new(author.avatar(proxy)).size(rems(2.)))
.context_menu(move |this, _window, _cx| {
let view = Box::new(OpenPublicKey(public_key));
let copy = Box::new(CopyPublicKey(public_key));
this.menu(t!("profile.view"), view)
.menu(t!("profile.copy"), copy)
}),
)
}) })
.child( .child(
v_flex() v_flex()
@@ -1332,9 +1332,7 @@ impl Panel for Chat {
let label = this.display_name(cx); let label = this.display_name(cx);
let url = this.display_image(proxy, cx); let url = this.display_image(proxy, cx);
div() h_flex()
.flex()
.items_center()
.gap_1p5() .gap_1p5()
.child(Avatar::new(url).size(rems(1.25))) .child(Avatar::new(url).size(rems(1.25)))
.child(label) .child(label)
+1 -1
View File
@@ -424,7 +424,7 @@ impl Compose {
impl Render for Compose { impl Render for Compose {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let error = self.error_message.read(cx).as_ref(); let error = self.error_message.read(cx).as_ref();
let loading = self.user_input.read(cx).loading(cx); let loading = self.user_input.read(cx).loading;
let contacts = self.contacts.read(cx); let contacts = self.contacts.read(cx);
v_flex() v_flex()
+38 -42
View File
@@ -6,7 +6,8 @@ use global::nostr_client;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, div, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task,
Window,
}; };
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
@@ -292,30 +293,22 @@ impl Login {
// Handle connection // Handle connection
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let client = nostr_client();
match signer.bunker_uri().await { match signer.bunker_uri().await {
Ok(uri) => { Ok(uri) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.write_uri_to_disk(&uri, cx); this.write_uri_to_disk(signer, uri, cx);
}) })
.ok(); .ok();
// Set the client's signer with the current nostr connect instance
client.set_signer(signer).await;
} }
Err(error) => { Err(error) => {
cx.update(|window, cx| { this.update_in(cx, |this, window, cx| {
this.update(cx, |this, cx| { this.set_error(error.to_string(), window, cx);
this.set_error(error.to_string(), window, cx); // Force reset the client keys
// Force reset the client keys //
// // This step is necessary to ensure that user can retry the connection
// This step is necessary to ensure that user can retry the connection client_keys.update(cx, |this, cx| {
client_keys.update(cx, |this, cx| { this.force_new_keys(cx);
this.force_new_keys(cx); });
});
})
.ok();
}) })
.ok(); .ok();
} }
@@ -324,38 +317,41 @@ impl Login {
.detach(); .detach();
} }
fn write_uri_to_disk(&mut self, uri: &NostrConnectURI, cx: &mut Context<Self>) { fn write_uri_to_disk(
let Some(public_key) = uri.remote_signer_public_key().cloned() else { &mut self,
log::error!("Remote Signer's public key not found"); signer: NostrConnect,
return; uri: NostrConnectURI,
}; cx: &mut Context<Self>,
) {
let mut uri_without_secret = uri.to_string();
let mut value = uri.to_string(); // Clear the secret parameter in the URI if it exists
// Clear the secret param if it exists
if let Some(secret) = uri.secret() { if let Some(secret) = uri.secret() {
value = value.replace(secret, ""); uri_without_secret = uri_without_secret.replace(secret, "");
} }
cx.background_spawn(async move { let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
let keys = Keys::generate();
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
let kind = Kind::ApplicationSpecificData;
let builder = EventBuilder::new(kind, value) // Update the client's signer
.tags(tags) client.set_signer(signer).await;
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let event = EventBuilder::new(Kind::ApplicationSpecificData, uri_without_secret)
.tags(vec![Tag::identifier(ACCOUNT_IDENTIFIER)])
.build(public_key) .build(public_key)
.sign(&keys) .sign(&Keys::generate())
.await; .await?;
if let Ok(event) = builder { // Save the event to the database
if let Err(e) = client.database().save_event(&event).await { client.database().save_event(&event).await?;
log::error!("Failed to save event: {e}");
}; Ok(())
} });
})
.detach(); task.detach();
} }
pub fn write_keys_to_disk(&self, keys: &Keys, password: String, cx: &mut Context<Self>) { pub fn write_keys_to_disk(&self, keys: &Keys, password: String, cx: &mut Context<Self>) {
+28 -29
View File
@@ -135,7 +135,6 @@ impl Onboarding {
self._tasks.push( self._tasks.push(
// Wait for Nostr Connect approval // Wait for Nostr Connect approval
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let client = nostr_client();
let connect = this.read_with(cx, |this, cx| this.nostr_connect.read(cx).clone()); let connect = this.read_with(cx, |this, cx| this.nostr_connect.read(cx).clone());
if let Ok(Some(signer)) = connect { if let Ok(Some(signer)) = connect {
@@ -143,12 +142,9 @@ impl Onboarding {
Ok(uri) => { Ok(uri) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_connecting(cx); this.set_connecting(cx);
this.write_uri_to_disk(&uri, cx); this.write_uri_to_disk(signer, uri, cx);
}) })
.ok(); .ok();
// Set the client's signer with the current nostr connect instance
client.set_signer(signer).await;
} }
Err(e) => { Err(e) => {
this.update_in(cx, |_, window, cx| { this.update_in(cx, |_, window, cx| {
@@ -169,38 +165,41 @@ impl Onboarding {
ChatSpace::proxy_signer(window, cx); ChatSpace::proxy_signer(window, cx);
} }
fn write_uri_to_disk(&mut self, uri: &NostrConnectURI, cx: &mut Context<Self>) { fn write_uri_to_disk(
let Some(public_key) = uri.remote_signer_public_key().cloned() else { &mut self,
log::error!("Remote Signer's public key not found"); signer: NostrConnect,
return; uri: NostrConnectURI,
}; cx: &mut Context<Self>,
) {
let mut uri_without_secret = uri.to_string();
let mut value = uri.to_string(); // Clear the secret parameter in the URI if it exists
// Clear the secret param if it exists
if let Some(secret) = uri.secret() { if let Some(secret) = uri.secret() {
value = value.replace(secret, ""); uri_without_secret = uri_without_secret.replace(secret, "");
} }
cx.background_spawn(async move { let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
let keys = Keys::generate();
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
let kind = Kind::ApplicationSpecificData;
let builder = EventBuilder::new(kind, value) // Update the client's signer
.tags(tags) client.set_signer(signer).await;
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let event = EventBuilder::new(Kind::ApplicationSpecificData, uri_without_secret)
.tags(vec![Tag::identifier(ACCOUNT_IDENTIFIER)])
.build(public_key) .build(public_key)
.sign(&keys) .sign(&Keys::generate())
.await; .await?;
if let Ok(event) = builder { // Save the event to the database
if let Err(e) = client.database().save_event(&event).await { client.database().save_event(&event).await?;
log::error!("Failed to save event: {e}");
}; Ok(())
} });
})
.detach(); task.detach();
} }
fn copy_uri(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn copy_uri(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+1 -1
View File
@@ -204,7 +204,7 @@ impl Render for Preferences {
.on_click(move |_, _window, cx| { .on_click(move |_, _window, cx| {
if let Some(input) = input_state.upgrade() { if let Some(input) = input_state.upgrade() {
let Ok(url) = let Ok(url) =
Url::parse(input.read(cx).value()) Url::parse(&input.read(cx).value())
else { else {
return; return;
}; };
@@ -11,7 +11,9 @@ use registry::room::RoomKind;
use registry::Registry; use registry::Registry;
use settings::AppSettings; use settings::AppSettings;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::actions::{CopyPublicKey, OpenPublicKey};
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::context_menu::ContextMenuExt;
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::skeleton::Skeleton; use ui::skeleton::Skeleton;
use ui::{h_flex, ContextModal, StyledExt}; use ui::{h_flex, ContextModal, StyledExt};
@@ -166,6 +168,10 @@ impl RenderOnce for RoomListItem {
), ),
) )
.hover(|this| this.bg(cx.theme().elevated_surface_background)) .hover(|this| this.bg(cx.theme().elevated_surface_background))
.context_menu(move |this, _window, _cx| {
this.menu(t!("profile.view"), Box::new(OpenPublicKey(public_key)))
.menu(t!("profile.copy"), Box::new(CopyPublicKey(public_key)))
})
.on_click(move |event, window, cx| { .on_click(move |event, window, cx| {
handler(event, window, cx); handler(event, window, cx);
+4 -3
View File
@@ -100,12 +100,12 @@ impl Sidebar {
subscriptions.push( subscriptions.push(
// Subscribe for find input events // Subscribe for find input events
cx.subscribe_in(&find_input, window, |this, _state, event, window, cx| { cx.subscribe_in(&find_input, window, |this, state, event, window, cx| {
match event { match event {
InputEvent::PressEnter { .. } => this.search(window, cx), InputEvent::PressEnter { .. } => this.search(window, cx),
InputEvent::Change(text) => { InputEvent::Change => {
// Clear the result when input is empty // Clear the result when input is empty
if text.is_empty() { if state.read(cx).value().is_empty() {
this.clear_search_results(window, cx); this.clear_search_results(window, cx);
} else { } else {
// Run debounced search // Run debounced search
@@ -722,6 +722,7 @@ impl Render for Sidebar {
.small() .small()
.cleanable() .cleanable()
.appearance(true) .appearance(true)
.text_xs()
.suffix( .suffix(
Button::new("find") Button::new("find")
.icon(IconName::Search) .icon(IconName::Search)
+15 -19
View File
@@ -17,7 +17,7 @@ use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, StyledExt}; use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt};
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<UserProfile> { pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<UserProfile> {
cx.new(|cx| UserProfile::new(public_key, window, cx)) cx.new(|cx| UserProfile::new(public_key, window, cx))
@@ -32,27 +32,24 @@ pub struct UserProfile {
} }
impl UserProfile { impl UserProfile {
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(target: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
let registry = Registry::read_global(cx); let registry = Registry::read_global(cx);
let identity = registry.identity(cx).public_key(); let profile = registry.get_person(&target, cx);
let profile = registry.get_person(&public_key, cx);
let mut tasks = smallvec![]; let mut tasks = smallvec![];
let check_follow: Task<bool> = cx.background_spawn(async move { let check_follow: Task<Result<bool, Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
let filter = Filter::new() let signer = client.signer().await?;
.kind(Kind::ContactList) let public_key = signer.get_public_key().await?;
.author(identity) let contact_list = client.database().contacts_public_keys(public_key).await?;
.pubkey(public_key)
.limit(1);
client.database().count(filter).await.unwrap_or(0) >= 1 Ok(contact_list.contains(&target))
}); });
let verify_nip05 = if let Some(address) = profile.metadata().nip05 { let verify_nip05 = if let Some(address) = profile.metadata().nip05 {
Some(Tokio::spawn(cx, async move { Some(Tokio::spawn(cx, async move {
nip05_verify(public_key, &address).await.unwrap_or(false) nip05_verify(target, &address).await.unwrap_or(false)
})) }))
} else { } else {
None None
@@ -61,7 +58,7 @@ impl UserProfile {
tasks.push( tasks.push(
// Load user profile data // Load user profile data
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let followed = check_follow.await; let followed = check_follow.await.unwrap_or(false);
// Update the followed status // Update the followed status
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
@@ -133,6 +130,7 @@ impl Render for UserProfile {
v_flex() v_flex()
.gap_4() .gap_4()
.text_sm()
.child( .child(
v_flex() v_flex()
.gap_3() .gap_3()
@@ -188,10 +186,8 @@ impl Render for UserProfile {
.child( .child(
v_flex() v_flex()
.gap_1() .gap_1()
.text_sm()
.child( .child(
div() div()
.block()
.text_color(cx.theme().text_muted) .text_color(cx.theme().text_muted)
.child(SharedString::from("Public Key:")), .child(SharedString::from("Public Key:")),
) )
@@ -201,12 +197,13 @@ impl Render for UserProfile {
.child( .child(
div() div()
.p_2() .p_2()
.h_9() .h_7()
.rounded_md() .rounded_md()
.bg(cx.theme().elevated_surface_background) .bg(cx.theme().elevated_surface_background)
.truncate() .truncate()
.text_ellipsis() .text_ellipsis()
.line_clamp(1) .line_clamp(1)
.line_height(relative(1.))
.child(shared_bech32), .child(shared_bech32),
) )
.child( .child(
@@ -218,8 +215,8 @@ impl Render for UserProfile {
IconName::Copy IconName::Copy
} }
}) })
.ghost() .cta()
.disabled(self.copied) .ghost_alt()
.on_click(cx.listener(move |this, _e, window, cx| { .on_click(cx.listener(move |this, _e, window, cx| {
this.copy_pubkey(window, cx); this.copy_pubkey(window, cx);
})), })),
@@ -229,7 +226,6 @@ impl Render for UserProfile {
.child( .child(
v_flex() v_flex()
.gap_1() .gap_1()
.text_sm()
.child( .child(
div() div()
.text_color(cx.theme().text_muted) .text_color(cx.theme().text_muted)
+3 -3
View File
@@ -159,10 +159,10 @@ pub struct AppState {
pub auto_close_opts: Option<SubscribeAutoCloseOptions>, pub auto_close_opts: Option<SubscribeAutoCloseOptions>,
pub seen_on_relays: RwLock<HashMap<EventId, HashSet<RelayUrl>>>,
pub sent_ids: RwLock<HashSet<EventId>>, pub sent_ids: RwLock<HashSet<EventId>>,
pub seen_on_relays: RwLock<HashMap<EventId, HashSet<RelayUrl>>>,
pub resent_ids: RwLock<Vec<Output<EventId>>>, pub resent_ids: RwLock<Vec<Output<EventId>>>,
pub resend_queue: RwLock<HashMap<EventId, RelayUrl>>, pub resend_queue: RwLock<HashMap<EventId, RelayUrl>>,
@@ -196,8 +196,8 @@ impl AppState {
gift_wrap_sub_id: SubscriptionId::new("inbox"), gift_wrap_sub_id: SubscriptionId::new("inbox"),
gift_wrap_processing: AtomicBool::new(false), gift_wrap_processing: AtomicBool::new(false),
auto_close_opts: Some(opts), auto_close_opts: Some(opts),
seen_on_relays: RwLock::new(HashMap::new()),
sent_ids: RwLock::new(HashSet::new()), sent_ids: RwLock::new(HashSet::new()),
seen_on_relays: RwLock::new(HashMap::new()),
resent_ids: RwLock::new(Vec::new()), resent_ids: RwLock::new(Vec::new()),
resend_queue: RwLock::new(HashMap::new()), resend_queue: RwLock::new(HashMap::new()),
} }
+19 -17
View File
@@ -21,20 +21,26 @@ impl Message {
pub fn system() -> Self { pub fn system() -> Self {
Self::System(Timestamp::default()) Self::System(Timestamp::default())
} }
fn timestamp(&self) -> &Timestamp {
match self {
Message::User(msg) => &msg.created_at,
Message::Warning(_, ts) => ts,
Message::System(ts) => ts,
}
}
} }
impl Ord for Message { impl Ord for Message {
fn cmp(&self, other: &Self) -> std::cmp::Ordering { fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) { match (self, other) {
(Message::User(a), Message::User(b)) => a.cmp(b), // System always comes first
(Message::System(a), Message::System(b)) => a.cmp(b), (Message::System(_), Message::System(_)) => self.timestamp().cmp(other.timestamp()),
(Message::User(a), Message::System(b)) => a.created_at.cmp(b), (Message::System(_), _) => std::cmp::Ordering::Less,
(Message::System(a), Message::User(b)) => a.cmp(&b.created_at), (_, Message::System(_)) => std::cmp::Ordering::Greater,
(Message::Warning(_, a), Message::Warning(_, b)) => a.cmp(b),
(Message::Warning(_, a), Message::User(b)) => a.cmp(&b.created_at), // For non-system messages, compare by timestamp
(Message::User(a), Message::Warning(_, b)) => a.created_at.cmp(b), _ => self.timestamp().cmp(other.timestamp()),
(Message::Warning(_, a), Message::System(b)) => a.cmp(b),
(Message::System(a), Message::Warning(_, b)) => a.cmp(b),
} }
} }
} }
@@ -151,18 +157,14 @@ fn extract_reply_ids(inner: &Tags) -> Vec<EventId> {
let mut replies_to = vec![]; let mut replies_to = vec![];
for tag in inner.filter(TagKind::e()) { for tag in inner.filter(TagKind::e()) {
if let Some(content) = tag.content() { if let Some(id) = tag.content().and_then(|id| EventId::parse(id).ok()) {
if let Ok(id) = EventId::from_hex(content) { replies_to.push(id);
replies_to.push(id);
}
} }
} }
for tag in inner.filter(TagKind::q()) { for tag in inner.filter(TagKind::q()) {
if let Some(content) = tag.content() { if let Some(id) = tag.content().and_then(|id| EventId::parse(id).ok()) {
if let Ok(id) = EventId::from_hex(content) { replies_to.push(id);
replies_to.push(id);
}
} }
} }
+36 -6
View File
@@ -365,21 +365,51 @@ impl Room {
cx.background_spawn(async move { cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
let public_key = members[members.len() - 1]; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let sent_ids = app_state()
.sent_ids
.read()
.await
.iter()
.copied()
.collect_vec();
let sent = Filter::new() // Get seen events from database
let filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.identifiers(sent_ids);
let seen_events = client.database().query(filter).await?;
// Extract seen event IDs
let seen_ids: Vec<EventId> = seen_events
.into_iter()
.filter_map(|event| event.tags.event_ids().next().copied())
.collect();
// Get events that sent by current user
let filter = Filter::new()
.kind(Kind::PrivateDirectMessage) .kind(Kind::PrivateDirectMessage)
.author(public_key) .author(public_key)
.pubkeys(members.clone()); .pubkeys(members.clone());
let recv = Filter::new() let sent_events = client.database().query(filter).await?;
// Get events that received by current user
let filter = Filter::new()
.kind(Kind::PrivateDirectMessage) .kind(Kind::PrivateDirectMessage)
.authors(members) .authors(members)
.pubkey(public_key); .pubkey(public_key);
let sent_events = client.database().query(sent).await?; let recv_events = client.database().query(filter).await?;
let recv_events = client.database().query(recv).await?;
let events: Vec<Event> = sent_events.merge(recv_events).into_iter().collect(); // Merge events
let events: Vec<Event> = sent_events
.merge(recv_events)
.into_iter()
.filter(|event| !seen_ids.contains(&event.id))
.collect();
Ok(events) Ok(events)
}) })
+3
View File
@@ -28,3 +28,6 @@ uuid = "1.10"
once_cell = "1.19.0" once_cell = "1.19.0"
image = "0.25.1" image = "0.25.1"
linkify = "0.10.0" linkify = "0.10.0"
lsp-types = "0.97.0"
rope = { git = "https://github.com/zed-industries/zed.git" }
sum_tree = { git = "https://github.com/zed-industries/zed.git" }
+8 -3
View File
@@ -2,10 +2,15 @@ use gpui::{actions, Action};
use nostr_sdk::prelude::PublicKey; use nostr_sdk::prelude::PublicKey;
use serde::Deserialize; use serde::Deserialize;
/// Define a open profile action /// Define a open public key action
#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)] #[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)]
#[action(namespace = profile, no_json)] #[action(namespace = pubkey, no_json)]
pub struct OpenProfile(pub PublicKey); pub struct OpenPublicKey(pub PublicKey);
/// Define a copy inline public key action
#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)]
#[action(namespace = pubkey, no_json)]
pub struct CopyPublicKey(pub PublicKey);
/// Define a custom confirm action /// Define a custom confirm action
#[derive(Clone, Action, PartialEq, Eq, Deserialize)] #[derive(Clone, Action, PartialEq, Eq, Deserialize)]
+1 -1
View File
@@ -22,10 +22,10 @@ pub struct History<I: HistoryItem> {
redos: Vec<I>, redos: Vec<I>,
last_changed_at: Instant, last_changed_at: Instant,
version: usize, version: usize,
pub(crate) ignore: bool,
max_undo: usize, max_undo: usize,
group_interval: Option<Duration>, group_interval: Option<Duration>,
unique: bool, unique: bool,
pub ignore: bool,
} }
impl<I> History<I> impl<I> History<I>
+2
View File
@@ -45,6 +45,7 @@ pub enum IconName {
Plus, Plus,
PlusFill, PlusFill,
PlusCircleFill, PlusCircleFill,
Group,
ResizeCorner, ResizeCorner,
Reply, Reply,
Report, Report,
@@ -106,6 +107,7 @@ impl IconName {
Self::Plus => "icons/plus.svg", Self::Plus => "icons/plus.svg",
Self::PlusFill => "icons/plus-fill.svg", Self::PlusFill => "icons/plus-fill.svg",
Self::PlusCircleFill => "icons/plus-circle-fill.svg", Self::PlusCircleFill => "icons/plus-circle-fill.svg",
Self::Group => "icons/group.svg",
Self::ResizeCorner => "icons/resize-corner.svg", Self::ResizeCorner => "icons/resize-corner.svg",
Self::Reply => "icons/reply.svg", Self::Reply => "icons/reply.svg",
Self::Report => "icons/report.svg", Self::Report => "icons/report.svg",
+10 -5
View File
@@ -1,9 +1,10 @@
use std::time::Duration; use std::time::Duration;
use gpui::{Context, Timer}; use gpui::{px, Context, Pixels, Timer};
static INTERVAL: Duration = Duration::from_millis(500); static INTERVAL: Duration = Duration::from_millis(500);
static PAUSE_DELAY: Duration = Duration::from_millis(300); static PAUSE_DELAY: Duration = Duration::from_millis(300);
pub(super) const CURSOR_WIDTH: Pixels = px(1.5);
/// To manage the Input cursor blinking. /// To manage the Input cursor blinking.
/// ///
@@ -11,7 +12,7 @@ static PAUSE_DELAY: Duration = Duration::from_millis(300);
/// Every loop will notify the view to update the `visible`, and Input will observe this update to touch repaint. /// Every loop will notify the view to update the `visible`, and Input will observe this update to touch repaint.
/// ///
/// The input painter will check if this in visible state, then it will draw the cursor. /// The input painter will check if this in visible state, then it will draw the cursor.
pub(crate) struct BlinkCursor { pub struct BlinkCursor {
visible: bool, visible: bool,
paused: bool, paused: bool,
epoch: usize, epoch: usize,
@@ -52,10 +53,8 @@ impl BlinkCursor {
// Schedule the next blink // Schedule the next blink
let epoch = self.next_epoch(); let epoch = self.next_epoch();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
Timer::after(INTERVAL).await; Timer::after(INTERVAL).await;
if let Some(this) = this.upgrade() { if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| this.blink(epoch, cx)).ok(); this.update(cx, |this, cx| this.blink(epoch, cx)).ok();
} }
@@ -71,11 +70,11 @@ impl BlinkCursor {
/// Pause the blinking, and delay 500ms to resume the blinking. /// Pause the blinking, and delay 500ms to resume the blinking.
pub fn pause(&mut self, cx: &mut Context<Self>) { pub fn pause(&mut self, cx: &mut Context<Self>) {
self.paused = true; self.paused = true;
self.visible = true;
cx.notify(); cx.notify();
// delay 500ms to start the blinking // delay 500ms to start the blinking
let epoch = self.next_epoch(); let epoch = self.next_epoch();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
Timer::after(PAUSE_DELAY).await; Timer::after(PAUSE_DELAY).await;
@@ -90,3 +89,9 @@ impl BlinkCursor {
.detach(); .detach();
} }
} }
impl Default for BlinkCursor {
fn default() -> Self {
Self::new()
}
}
+7 -7
View File
@@ -1,28 +1,28 @@
use std::fmt::Debug; use std::fmt::Debug;
use std::ops::Range;
use crate::history::HistoryItem; use crate::history::HistoryItem;
use crate::input::cursor::Selection;
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct Change { pub struct Change {
pub(crate) old_range: Range<usize>, pub(crate) old_range: Selection,
pub(crate) old_text: String, pub(crate) old_text: String,
pub(crate) new_range: Range<usize>, pub(crate) new_range: Selection,
pub(crate) new_text: String, pub(crate) new_text: String,
version: usize, version: usize,
} }
impl Change { impl Change {
pub fn new( pub fn new(
old_range: Range<usize>, old_range: impl Into<Selection>,
old_text: &str, old_text: &str,
new_range: Range<usize>, new_range: impl Into<Selection>,
new_text: &str, new_text: &str,
) -> Self { ) -> Self {
Self { Self {
old_range, old_range: old_range.into(),
old_text: old_text.to_string(), old_text: old_text.to_string(),
new_range, new_range: new_range.into(),
new_text: new_text.to_string(), new_text: new_text.to_string(),
version: 0, version: 0,
} }
+4 -5
View File
@@ -1,16 +1,15 @@
use gpui::{App, Styled}; use gpui::{App, Styled};
use i18n::t;
use theme::ActiveTheme; use theme::ActiveTheme;
use crate::button::{Button, ButtonVariants as _}; use crate::button::{Button, ButtonVariants};
use crate::{Icon, IconName, Sizable as _}; use crate::{Icon, IconName, Sizable};
#[inline] #[inline]
pub(crate) fn clear_button(cx: &App) -> Button { pub(crate) fn clear_button(cx: &App) -> Button {
Button::new("clean") Button::new("clean")
.icon(Icon::new(IconName::CloseCircle)) .icon(Icon::new(IconName::CloseCircle))
.tooltip(t!("common.clear")) .tooltip("Clear")
.small() .small()
.text_color(cx.theme().text_muted)
.transparent() .transparent()
.text_color(cx.theme().text_muted)
} }
+46
View File
@@ -0,0 +1,46 @@
use std::ops::Range;
/// A selection in the text, represented by start and end byte indices.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub struct Selection {
pub start: usize,
pub end: usize,
}
impl Selection {
pub fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
pub fn len(&self) -> usize {
self.end.saturating_sub(self.start)
}
pub fn is_empty(&self) -> bool {
self.start == self.end
}
/// Clears the selection, setting start and end to 0.
pub fn clear(&mut self) {
self.start = 0;
self.end = 0;
}
/// Checks if the given offset is within the selection range.
pub fn contains(&self, offset: usize) -> bool {
offset >= self.start && offset < self.end
}
}
impl From<Range<usize>> for Selection {
fn from(value: Range<usize>) -> Self {
Self::new(value.start, value.end)
}
}
impl From<Selection> for Range<usize> {
fn from(value: Selection) -> Self {
value.start..value.end
}
}
pub type Position = lsp_types::Position;
+388 -241
View File
@@ -1,29 +1,34 @@
use std::{ops::Range, rc::Rc}; use std::ops::Range;
use std::rc::Rc;
use gpui::{ use gpui::{
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler, fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
Entity, GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, Path, Pixels, Entity, GlobalElementId, Half, Hitbox, IntoElement, LayoutId, MouseButton, MouseMoveEvent,
Point, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window, WrappedLine, Path, Pixels, Point, ShapedLine, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle,
Window,
}; };
use rope::Rope;
use smallvec::SmallVec; use smallvec::SmallVec;
use theme::ActiveTheme; use theme::ActiveTheme;
use super::{InputState, LastLayout}; use super::blink_cursor::CURSOR_WIDTH;
use super::rope_ext::RopeExt;
use super::state::{InputState, LastLayout};
use crate::Root; use crate::Root;
const CURSOR_THICKNESS: Pixels = px(2.); const BOTTOM_MARGIN_ROWS: usize = 3;
const RIGHT_MARGIN: Pixels = px(5.); pub(super) const RIGHT_MARGIN: Pixels = px(10.);
const BOTTOM_MARGIN_ROWS: usize = 1; pub(super) const LINE_NUMBER_RIGHT_MARGIN: Pixels = px(10.);
pub(super) struct TextElement { pub(super) struct TextElement {
input: Entity<InputState>, pub(crate) state: Entity<InputState>,
placeholder: SharedString, placeholder: SharedString,
} }
impl TextElement { impl TextElement {
pub(super) fn new(input: Entity<InputState>) -> Self { pub(super) fn new(state: Entity<InputState>) -> Self {
Self { Self {
input, state,
placeholder: SharedString::default(), placeholder: SharedString::default(),
} }
} }
@@ -36,12 +41,12 @@ impl TextElement {
fn paint_mouse_listeners(&mut self, window: &mut Window, _: &mut App) { fn paint_mouse_listeners(&mut self, window: &mut Window, _: &mut App) {
window.on_mouse_event({ window.on_mouse_event({
let input = self.input.clone(); let state = self.state.clone();
move |event: &MouseMoveEvent, _, window, cx| { move |event: &MouseMoveEvent, _, window, cx| {
if event.pressed_button == Some(MouseButton::Left) { if event.pressed_button == Some(MouseButton::Left) {
input.update(cx, |input, cx| { state.update(cx, |state, cx| {
input.on_drag_move(event, window, cx); state.on_drag_move(event, window, cx);
}); });
} }
} }
@@ -52,33 +57,44 @@ impl TextElement {
/// ///
/// - cursor bounds /// - cursor bounds
/// - scroll offset /// - scroll offset
/// - current line index /// - current row index (No only the visible lines, but all lines)
///
/// This method also will update for track scroll to cursor.
fn layout_cursor( fn layout_cursor(
&self, &self,
lines: &[WrappedLine], last_layout: &LastLayout,
line_height: Pixels,
bounds: &mut Bounds<Pixels>, bounds: &mut Bounds<Pixels>,
line_number_width: Pixels, _: &mut Window,
window: &mut Window,
cx: &mut App, cx: &mut App,
) -> (Option<Bounds<Pixels>>, Point<Pixels>, Option<usize>) { ) -> (Option<Bounds<Pixels>>, Point<Pixels>, Option<usize>) {
let input = self.input.read(cx); let state = self.state.read(cx);
let mut selected_range = input.selected_range.clone();
if let Some(marked_range) = &input.marked_range { let line_height = last_layout.line_height;
selected_range = marked_range.end..marked_range.end; let visible_range = &last_layout.visible_range;
let lines = &last_layout.lines;
let text_wrapper = &state.text_wrapper;
let line_number_width = last_layout.line_number_width;
let mut selected_range = state.selected_range;
if let Some(ime_marked_range) = &state.ime_marked_range {
selected_range = (ime_marked_range.end..ime_marked_range.end).into();
} }
let cursor_offset = input.cursor_offset(); let cursor = state.cursor();
let mut current_line_index = None; let mut current_row = None;
let mut scroll_offset = input.scroll_handle.offset(); let mut scroll_offset = state.scroll_handle.offset();
let mut cursor_bounds = None; let mut cursor_bounds = None;
// If the input has a fixed height (Otherwise is auto-grow), we need to add a bottom margin to the input. // If the input has a fixed height (Otherwise is auto-grow), we need to add a bottom margin to the input.
let bottom_margin = if input.is_auto_grow() { let top_bottom_margin = if state.mode.is_auto_grow() {
px(0.) + line_height #[allow(clippy::if_same_then_else)]
line_height
} else if visible_range.len() < BOTTOM_MARGIN_ROWS * 8 {
line_height
} else { } else {
BOTTOM_MARGIN_ROWS * line_height + line_height BOTTOM_MARGIN_ROWS * line_height
}; };
// The cursor corresponds to the current cursor position in the text no only the line. // The cursor corresponds to the current cursor position in the text no only the line.
let mut cursor_pos = None; let mut cursor_pos = None;
let mut cursor_start = None; let mut cursor_start = None;
@@ -86,68 +102,98 @@ impl TextElement {
let mut prev_lines_offset = 0; let mut prev_lines_offset = 0;
let mut offset_y = px(0.); let mut offset_y = px(0.);
for (line_ix, line) in lines.iter().enumerate() {
for (ix, wrap_line) in text_wrapper.lines.iter().enumerate() {
let row = ix;
let line_origin = point(px(0.), offset_y);
// break loop if all cursor positions are found // break loop if all cursor positions are found
if cursor_pos.is_some() && cursor_start.is_some() && cursor_end.is_some() { if cursor_pos.is_some() && cursor_start.is_some() && cursor_end.is_some() {
break; break;
} }
let line_origin = point(px(0.), offset_y); let in_visible_range = ix >= visible_range.start;
if cursor_pos.is_none() { if let Some(line) = in_visible_range
let offset = cursor_offset.saturating_sub(prev_lines_offset); .then(|| lines.get(ix.saturating_sub(visible_range.start)))
if let Some(pos) = line.position_for_index(offset, line_height) { .flatten()
current_line_index = Some(line_ix); {
cursor_pos = Some(line_origin + pos); // If in visible range lines
if cursor_pos.is_none() {
let offset = cursor.saturating_sub(prev_lines_offset);
if let Some(pos) = line.position_for_index(offset, line_height) {
current_row = Some(row);
cursor_pos = Some(line_origin + pos);
}
} }
} if cursor_start.is_none() {
if cursor_start.is_none() { let offset = selected_range.start.saturating_sub(prev_lines_offset);
let offset = selected_range.start.saturating_sub(prev_lines_offset); if let Some(pos) = line.position_for_index(offset, line_height) {
if let Some(pos) = line.position_for_index(offset, line_height) { cursor_start = Some(line_origin + pos);
cursor_start = Some(line_origin + pos); }
} }
} if cursor_end.is_none() {
if cursor_end.is_none() { let offset = selected_range.end.saturating_sub(prev_lines_offset);
let offset = selected_range.end.saturating_sub(prev_lines_offset); if let Some(pos) = line.position_for_index(offset, line_height) {
if let Some(pos) = line.position_for_index(offset, line_height) { cursor_end = Some(line_origin + pos);
cursor_end = Some(line_origin + pos); }
} }
}
offset_y += line.size(line_height).height; offset_y += line.size(line_height).height;
// +1 for skip the last `\n` // +1 for the last `\n`
prev_lines_offset += line.len() + 1; prev_lines_offset += line.len() + 1;
} else {
// If not in the visible range.
// Just increase the offset_y and prev_lines_offset.
// This will let the scroll_offset to track the cursor position correctly.
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() {
cursor_start = Some(line_origin);
}
if prev_lines_offset >= selected_range.end && cursor_end.is_none() {
cursor_end = Some(line_origin);
}
offset_y += wrap_line.height(line_height);
// +1 for the last `\n`
prev_lines_offset += wrap_line.len() + 1;
}
} }
if let (Some(cursor_pos), Some(cursor_start), Some(cursor_end)) = if let (Some(cursor_pos), Some(cursor_start), Some(cursor_end)) =
(cursor_pos, cursor_start, cursor_end) (cursor_pos, cursor_start, cursor_end)
{ {
let cursor_moved = input.last_cursor_offset != Some(cursor_offset); let selection_changed = state.last_selected_range != Some(selected_range);
let selection_changed = input.last_selected_range != Some(selected_range.clone()); if selection_changed {
scroll_offset.x = if scroll_offset.x + cursor_pos.x
if cursor_moved || selection_changed { > (bounds.size.width - line_number_width - RIGHT_MARGIN)
scroll_offset.x =
if scroll_offset.x + cursor_pos.x > (bounds.size.width - RIGHT_MARGIN) {
// cursor is out of right
bounds.size.width - RIGHT_MARGIN - cursor_pos.x
} else if scroll_offset.x + cursor_pos.x < px(0.) {
// cursor is out of left
scroll_offset.x - cursor_pos.x
} else {
scroll_offset.x
};
scroll_offset.y = if scroll_offset.y + cursor_pos.y + line_height
> bounds.size.height - bottom_margin
{ {
// cursor is out of bottom // cursor is out of right
bounds.size.height - bottom_margin - cursor_pos.y bounds.size.width - line_number_width - RIGHT_MARGIN - cursor_pos.x
} else if scroll_offset.y + cursor_pos.y < px(0.) { } else if scroll_offset.x + cursor_pos.x < px(0.) {
// cursor is out of top // cursor is out of left
scroll_offset.y - cursor_pos.y scroll_offset.x - cursor_pos.x
} else { } else {
scroll_offset.y scroll_offset.x
}; };
if input.selection_reversed { // If we change the scroll_offset.y, GPUI will render and trigger the next run loop.
// So, here we just adjust offset by `line_height` for move smooth.
scroll_offset.y =
if scroll_offset.y + cursor_pos.y > bounds.size.height - top_bottom_margin {
// cursor is out of bottom
scroll_offset.y - line_height
} else if scroll_offset.y + cursor_pos.y < top_bottom_margin {
// cursor is out of top
(scroll_offset.y + line_height).min(px(0.))
} else {
scroll_offset.y
};
if state.selection_reversed {
if scroll_offset.x + cursor_start.x < px(0.) { if scroll_offset.x + cursor_start.x < px(0.) {
// selection start is out of left // selection start is out of left
scroll_offset.x = -cursor_start.x; scroll_offset.x = -cursor_start.x;
@@ -168,54 +214,55 @@ impl TextElement {
} }
} }
if input.show_cursor(window, cx) { // cursor bounds
// cursor blink let cursor_height = line_height;
let cursor_height = line_height; cursor_bounds = Some(Bounds::new(
cursor_bounds = Some(Bounds::new( point(
point( bounds.left() + cursor_pos.x + line_number_width + scroll_offset.x,
bounds.left() + cursor_pos.x + line_number_width + scroll_offset.x, bounds.top() + cursor_pos.y + ((line_height - cursor_height) / 2.),
bounds.top() + cursor_pos.y + ((line_height - cursor_height) / 2.), ),
), size(CURSOR_WIDTH, cursor_height),
size(CURSOR_THICKNESS, cursor_height), ));
)); }
};
if let Some(deferred_scroll_offset) = state.deferred_scroll_offset {
scroll_offset = deferred_scroll_offset;
} }
bounds.origin += scroll_offset; bounds.origin += scroll_offset;
(cursor_bounds, scroll_offset, current_line_index) (cursor_bounds, scroll_offset, current_row)
} }
fn layout_selections( /// Layout the match range to a Path.
&self, pub(crate) fn layout_match_range(
lines: &[WrappedLine], range: Range<usize>,
line_height: Pixels, last_layout: &LastLayout,
bounds: &mut Bounds<Pixels>, bounds: &mut Bounds<Pixels>,
line_number_width: Pixels,
_: &mut Window,
cx: &mut App,
) -> Option<Path<Pixels>> { ) -> Option<Path<Pixels>> {
let input = self.input.read(cx); if range.is_empty() {
let mut selected_range = input.selected_range.clone();
if let Some(marked_range) = &input.marked_range {
if !marked_range.is_empty() {
selected_range = marked_range.end..marked_range.end;
}
}
if selected_range.is_empty() {
return None; return None;
} }
let (start_ix, end_ix) = if selected_range.start < selected_range.end { if range.start < last_layout.visible_range_offset.start
(selected_range.start, selected_range.end) || range.end > last_layout.visible_range_offset.end
} else { {
(selected_range.end, selected_range.start) return None;
}; }
let mut prev_lines_offset = 0; let line_height = last_layout.line_height;
let visible_top = last_layout.visible_top;
let visible_start_offset = last_layout.visible_range_offset.start;
let lines = &last_layout.lines;
let line_number_width = last_layout.line_number_width;
let start_ix = range.start;
let end_ix = range.end;
let mut prev_lines_offset = visible_start_offset;
let mut offset_y = visible_top;
let mut line_corners = vec![]; let mut line_corners = vec![];
let mut offset_y = px(0.);
for line in lines.iter() { for line in lines.iter() {
let line_size = line.size(line_height); let line_size = line.size(line_height);
let line_wrap_width = line_size.width; let line_wrap_width = line_size.width;
@@ -239,7 +286,6 @@ impl TextElement {
(end.y / line_height).ceil() as usize - (start.y / line_height).ceil() as usize; (end.y / line_height).ceil() as usize - (start.y / line_height).ceil() as usize;
let mut end_x = end.x; let mut end_x = end.x;
if wrapped_lines > 0 { if wrapped_lines > 0 {
end_x = line_wrap_width; end_x = line_wrap_width;
} }
@@ -322,39 +368,79 @@ impl TextElement {
builder.build().ok() builder.build().ok()
} }
fn layout_selections(
&self,
last_layout: &LastLayout,
bounds: &mut Bounds<Pixels>,
cx: &mut App,
) -> Option<Path<Pixels>> {
let state = self.state.read(cx);
let mut selected_range = state.selected_range;
if let Some(ime_marked_range) = &state.ime_marked_range {
if !ime_marked_range.is_empty() {
selected_range = (ime_marked_range.end..ime_marked_range.end).into();
}
}
if selected_range.is_empty() {
return None;
}
let (start_ix, end_ix) = if selected_range.start < selected_range.end {
(selected_range.start, selected_range.end)
} else {
(selected_range.end, selected_range.start)
};
let range = start_ix.max(last_layout.visible_range_offset.start)
..end_ix.min(last_layout.visible_range_offset.end);
Self::layout_match_range(range, last_layout, bounds)
}
/// Calculate the visible range of lines in the viewport. /// Calculate the visible range of lines in the viewport.
/// ///
/// The visible range is based on unwrapped lines (Zero based). /// Returns
///
/// - visible_range: The visible range is based on unwrapped lines (Zero based).
/// - visible_top: The top position of the first visible line in the scroll viewport.
fn calculate_visible_range( fn calculate_visible_range(
&self, &self,
state: &InputState, state: &InputState,
line_height: Pixels, line_height: Pixels,
input_height: Pixels, input_height: Pixels,
) -> Range<usize> { ) -> (Range<usize>, Pixels) {
if state.is_single_line() { // Add extra rows to avoid showing empty space when scroll to bottom.
return 0..1; let extra_rows = 1;
let mut visible_top = px(0.);
if state.mode.is_single_line() {
return (0..1, visible_top);
} }
let scroll_top = -state.scroll_handle.offset().y; let total_lines = state.text_wrapper.len();
let total_lines = state.text_wrapper.lines.len(); let scroll_top = if let Some(deferred_scroll_offset) = state.deferred_scroll_offset {
deferred_scroll_offset.y
} else {
state.scroll_handle.offset().y
};
let mut visible_range = 0..total_lines; let mut visible_range = 0..total_lines;
let mut line_top = px(0.); let mut line_bottom = px(0.);
for (ix, line) in state.text_wrapper.lines.iter().enumerate() { for (ix, line) in state.text_wrapper.lines.iter().enumerate() {
line_top += line.height(line_height); let wrapped_height = line.height(line_height);
line_bottom += wrapped_height;
if line_top < scroll_top { if line_bottom < -scroll_top {
visible_top = line_bottom - wrapped_height;
visible_range.start = ix; visible_range.start = ix;
} }
if line_top > scroll_top + input_height { if line_bottom + scroll_top >= input_height {
visible_range.end = (ix + 1).min(total_lines); visible_range.end = (ix + extra_rows).min(total_lines);
break; break;
} }
} }
visible_range (visible_range, visible_top)
} }
} }
@@ -362,13 +448,17 @@ pub(super) struct PrepaintState {
/// The lines of entire lines. /// The lines of entire lines.
last_layout: LastLayout, last_layout: LastLayout,
/// The lines only contains the visible lines in the viewport, based on `visible_range`. /// The lines only contains the visible lines in the viewport, based on `visible_range`.
line_numbers: Option<Vec<SmallVec<[WrappedLine; 1]>>>, ///
line_number_width: Pixels, /// The child is the soft lines.
line_numbers: Option<Vec<SmallVec<[ShapedLine; 1]>>>,
/// Size of the scrollable area by entire lines. /// Size of the scrollable area by entire lines.
scroll_size: Size<Pixels>, scroll_size: Size<Pixels>,
cursor_bounds: Option<Bounds<Pixels>>, cursor_bounds: Option<Bounds<Pixels>>,
cursor_scroll_offset: Point<Pixels>, cursor_scroll_offset: Point<Pixels>,
selection_path: Option<Path<Pixels>>, selection_path: Option<Path<Pixels>>,
hover_highlight_path: Option<Path<Pixels>>,
search_match_paths: Vec<(Path<Pixels>, bool)>,
hover_definition_hitbox: Option<Hitbox>,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
} }
@@ -380,34 +470,9 @@ impl IntoElement for TextElement {
} }
} }
/// A debug function to print points as SVG path.
#[allow(unused)]
fn print_points_as_svg_path(line_corners: &Vec<Corners<Point<Pixels>>>, points: &[Point<Pixels>]) {
for corners in line_corners {
println!(
"tl: ({}, {}), tr: ({}, {}), bl: ({}, {}), br: ({}, {})",
corners.top_left.x.0 as i32,
corners.top_left.y.0 as i32,
corners.top_right.x.0 as i32,
corners.top_right.y.0 as i32,
corners.bottom_left.x.0 as i32,
corners.bottom_left.y.0 as i32,
corners.bottom_right.x.0 as i32,
corners.bottom_right.y.0 as i32,
);
}
if !points.is_empty() {
println!("M{},{}", points[0].x.0 as i32, points[0].y.0 as i32);
for p in points.iter().skip(1) {
println!("L{},{}", p.x.0 as i32, p.y.0 as i32);
}
}
}
impl Element for TextElement { impl Element for TextElement {
type RequestLayoutState = ();
type PrepaintState = PrepaintState; type PrepaintState = PrepaintState;
type RequestLayoutState = ();
fn id(&self) -> Option<ElementId> { fn id(&self) -> Option<ElementId> {
None None
@@ -424,19 +489,20 @@ impl Element for TextElement {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) { ) -> (LayoutId, Self::RequestLayoutState) {
let input = self.input.read(cx); let state = self.state.read(cx);
let line_height = window.line_height(); let line_height = window.line_height();
let mut style = Style::default(); let mut style = Style::default();
style.size.width = relative(1.).into(); style.size.width = relative(1.).into();
if self.input.read(cx).is_multi_line() { if state.mode.is_multi_line() {
style.flex_grow = 1.0; style.flex_grow = 1.0;
if let Some(h) = input.mode.height() { style.size.height = relative(1.).into();
style.size.height = h.into(); if state.mode.is_auto_grow() {
style.min_size.height = line_height.into(); // Auto grow to let height match to rows, but not exceed max rows.
let rows = state.mode.max_rows().min(state.mode.rows());
style.min_size.height = (rows * line_height).into();
} else { } else {
style.size.height = relative(1.).into(); style.min_size.height = line_height.into();
style.min_size.height = (input.mode.rows() * line_height).into();
} }
} else { } else {
// For single-line inputs, the minimum height should be the line height // For single-line inputs, the minimum height should be the line height
@@ -455,11 +521,19 @@ impl Element for TextElement {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Self::PrepaintState { ) -> Self::PrepaintState {
let state = self.state.read(cx);
let line_height = window.line_height(); let line_height = window.line_height();
let input = self.input.read(cx);
let multi_line = input.is_multi_line(); let (visible_range, visible_top) =
let visible_range = self.calculate_visible_range(input, line_height, bounds.size.height); self.calculate_visible_range(state, line_height, bounds.size.height);
let text = input.text.clone(); let visible_start_offset = state.text.line_start_offset(visible_range.start);
let visible_end_offset = state
.text
.line_end_offset(visible_range.end.saturating_sub(1));
let state = self.state.read(cx);
let multi_line = state.mode.is_multi_line();
let text = state.text.clone();
let is_empty = text.is_empty(); let is_empty = text.is_empty();
let placeholder = self.placeholder.clone(); let placeholder = self.placeholder.clone();
let style = window.text_style(); let style = window.text_style();
@@ -467,9 +541,9 @@ impl Element for TextElement {
let mut bounds = bounds; let mut bounds = bounds;
let (display_text, text_color) = if is_empty { let (display_text, text_color) = if is_empty {
(placeholder, cx.theme().text_muted) (Rope::from(placeholder.as_str()), cx.theme().text_muted)
} else if input.masked { } else if state.masked {
("*".repeat(text.chars().count()).into(), cx.theme().text) (Rope::from("*".repeat(text.chars_count())), cx.theme().text)
} else { } else {
(text.clone(), cx.theme().text) (text.clone(), cx.theme().text)
}; };
@@ -500,20 +574,20 @@ impl Element for TextElement {
let runs = if !is_empty { let runs = if !is_empty {
vec![run] vec![run]
} else if let Some(marked_range) = &input.marked_range { } else if let Some(ime_marked_range) = &state.ime_marked_range {
// IME marked text // IME marked text
vec![ vec![
TextRun { TextRun {
len: marked_range.start, len: ime_marked_range.start,
..run.clone() ..run.clone()
}, },
TextRun { TextRun {
len: marked_range.end - marked_range.start, len: ime_marked_range.end - ime_marked_range.start,
underline: marked_run.underline, underline: marked_run.underline,
..run.clone() ..run.clone()
}, },
TextRun { TextRun {
len: display_text.len() - marked_range.end, len: display_text.len() - ime_marked_range.end,
..run.clone() ..run.clone()
}, },
] ]
@@ -524,35 +598,76 @@ impl Element for TextElement {
vec![run] vec![run]
}; };
let wrap_width = if multi_line { let wrap_width = if multi_line && state.soft_wrap {
Some(bounds.size.width - line_number_width - RIGHT_MARGIN) Some(bounds.size.width - line_number_width - RIGHT_MARGIN)
} else { } else {
None None
}; };
// NOTE: Here 50 lines about 150µs
// let measure = crate::Measure::new("shape_text");
let visible_text = display_text
.slice_rows(visible_range.start as u32..visible_range.end as u32)
.to_string();
let lines = window let lines = window
.text_system() .text_system()
.shape_text(display_text, font_size, &runs, wrap_width, None) .shape_text(visible_text.into(), font_size, &runs, wrap_width, None)
.expect("failed to shape text"); .expect("failed to shape text");
// measure.end();
let total_wrapped_lines = lines let mut longest_line_width = wrap_width.unwrap_or(px(0.));
.iter() if state.mode.is_multi_line() && !state.soft_wrap && lines.len() > 1 {
.map(|line| { let longtest_line: SharedString = state
// +1 is the first line, `wrap_boundaries` is the wrapped lines after the `\n`. .text
1 + line.wrap_boundaries.len() .line(state.text.summary().longest_row as usize)
}) .to_string()
.sum::<usize>(); .into();
longest_line_width = window
.text_system()
.shape_line(
longtest_line.clone(),
font_size,
&[TextRun {
len: longtest_line.len(),
font: style.font(),
color: gpui::black(),
background_color: None,
underline: None,
strikethrough: None,
}],
wrap_width,
)
.width;
}
let max_line_width = lines let total_wrapped_lines = state.text_wrapper.len();
.iter() let empty_bottom_height = bounds
.map(|line| line.width()) .size
.max() .height
.unwrap_or(bounds.size.width); .half()
.max(BOTTOM_MARGIN_ROWS * line_height);
let scroll_size = size( let scroll_size = size(
max_line_width + line_number_width + RIGHT_MARGIN, if longest_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width {
(total_wrapped_lines as f32 * line_height).max(bounds.size.height), longest_line_width + line_number_width + RIGHT_MARGIN
} else {
longest_line_width
},
(total_wrapped_lines as f32 * line_height + empty_bottom_height)
.max(bounds.size.height),
); );
let mut last_layout = LastLayout {
visible_range,
visible_top,
visible_range_offset: visible_start_offset..visible_end_offset,
line_height,
wrap_width,
line_number_width,
lines: Rc::new(lines),
cursor_bounds: None,
};
// `position_for_index` for example // `position_for_index` for example
// //
// #### text // #### text
@@ -584,37 +699,27 @@ impl Element for TextElement {
// Calculate the scroll offset to keep the cursor in view // Calculate the scroll offset to keep the cursor in view
let (cursor_bounds, cursor_scroll_offset, _) = self.layout_cursor( let (cursor_bounds, cursor_scroll_offset, _) =
&lines, self.layout_cursor(&last_layout, &mut bounds, window, cx);
line_height, last_layout.cursor_bounds = cursor_bounds;
&mut bounds,
line_number_width,
window,
cx,
);
let selection_path = self.layout_selections( let selection_path = self.layout_selections(&last_layout, &mut bounds, cx);
&lines, let search_match_paths = vec![];
line_height, let hover_highlight_path = None;
&mut bounds, let line_numbers = None;
line_number_width, let hover_definition_hitbox = None;
window,
cx,
);
PrepaintState { PrepaintState {
bounds, bounds,
last_layout: LastLayout { last_layout,
lines: Rc::new(lines),
line_height,
visible_range,
},
scroll_size, scroll_size,
line_numbers: None, line_numbers,
line_number_width,
cursor_bounds, cursor_bounds,
cursor_scroll_offset, cursor_scroll_offset,
selection_path, selection_path,
search_match_paths,
hover_highlight_path,
hover_definition_hitbox,
} }
} }
@@ -628,21 +733,21 @@ impl Element for TextElement {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) { ) {
let focus_handle = self.input.read(cx).focus_handle.clone(); let focus_handle = self.state.read(cx).focus_handle.clone();
let show_cursor = self.state.read(cx).show_cursor(window, cx);
let focused = focus_handle.is_focused(window); let focused = focus_handle.is_focused(window);
let bounds = prepaint.bounds; let bounds = prepaint.bounds;
let selected_range = self.input.read(cx).selected_range.clone(); let selected_range = self.state.read(cx).selected_range;
let visible_range = &prepaint.last_layout.visible_range;
window.handle_input( window.handle_input(
&focus_handle, &focus_handle,
ElementInputHandler::new(bounds, self.input.clone()), ElementInputHandler::new(bounds, self.state.clone()),
cx, cx,
); );
// Set Root focused_input when self is focused // Set Root focused_input when self is focused
if focused { if focused {
let state = self.input.clone(); let state = self.state.clone();
if Root::read(window, cx).focused_input.as_ref() != Some(&state) { if Root::read(window, cx).focused_input.as_ref() != Some(&state) {
Root::update(window, cx, |root, _, cx| { Root::update(window, cx, |root, _, cx| {
root.focused_input = Some(state); root.focused_input = Some(state);
@@ -653,7 +758,7 @@ impl Element for TextElement {
// And reset focused_input when next_frame start // And reset focused_input when next_frame start
window.on_next_frame({ window.on_next_frame({
let state = self.input.clone(); let state = self.state.clone();
move |window, cx| { move |window, cx| {
if !focused && Root::read(window, cx).focused_input.as_ref() == Some(&state) { if !focused && Root::read(window, cx).focused_input.as_ref() == Some(&state) {
Root::update(window, cx, |root, _, cx| { Root::update(window, cx, |root, _, cx| {
@@ -668,13 +773,10 @@ impl Element for TextElement {
let line_height = window.line_height(); let line_height = window.line_height();
let origin = bounds.origin; let origin = bounds.origin;
let mut invisible_top_padding = px(0.); let invisible_top_padding = prepaint.last_layout.visible_top;
for line in prepaint.last_layout.lines.iter().take(visible_range.start) {
invisible_top_padding += line.size(line_height).height;
}
let mut mask_offset_y = px(0.); let mut mask_offset_y = px(0.);
if self.input.read(cx).masked { if self.state.read(cx).masked {
// Move down offset for vertical centering the ***** // Move down offset for vertical centering the *****
if cfg!(target_os = "macos") { if cfg!(target_os = "macos") {
mask_offset_y = px(3.); mask_offset_y = px(3.);
@@ -683,60 +785,105 @@ impl Element for TextElement {
} }
} }
// Paint active line
let mut offset_y = px(0.); let mut offset_y = px(0.);
if let Some(line_numbers) = prepaint.line_numbers.as_ref() { if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
offset_y += invisible_top_padding; offset_y += invisible_top_padding;
// Each item is the normal lines. // Each item is the normal lines.
for lines in line_numbers.iter() { for lines in line_numbers.iter() {
for line in lines { let height = line_height * lines.len() as f32;
let p = point(origin.x, origin.y + offset_y); offset_y += height;
let line_size = line.size(line_height);
_ = line.paint(p, line_height, TextAlign::Left, None, window, cx);
offset_y += line_size.height;
}
} }
} }
// Paint selections // Paint selections
if let Some(path) = prepaint.selection_path.take() { if window.is_window_active() {
window.paint_path(path, cx.theme().selection); 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);
}
} }
// Paint text // Paint text
let mut offset_y = mask_offset_y + invisible_top_padding; let mut offset_y = mask_offset_y + invisible_top_padding;
for line in prepaint for line in prepaint.last_layout.lines.iter() {
.last_layout let p = point(
.iter() origin.x + prepaint.last_layout.line_number_width,
.skip(visible_range.start) origin.y + offset_y,
.take(visible_range.len()) );
{
let p = point(origin.x + prepaint.line_number_width, origin.y + offset_y);
_ = line.paint(p, line_height, TextAlign::Left, None, window, cx); _ = line.paint(p, line_height, TextAlign::Left, None, window, cx);
offset_y += line.size(line_height).height; offset_y += line.size(line_height).height;
} }
if focused { // Paint blinking cursor
if focused && show_cursor {
if let Some(mut cursor_bounds) = prepaint.cursor_bounds.take() { if let Some(mut cursor_bounds) = prepaint.cursor_bounds.take() {
cursor_bounds.origin.y += prepaint.cursor_scroll_offset.y; cursor_bounds.origin.y += prepaint.cursor_scroll_offset.y;
window.paint_quad(fill(cursor_bounds, cx.theme().cursor)); window.paint_quad(fill(cursor_bounds, cx.theme().cursor));
} }
} }
self.input.update(cx, |input, cx| { // Paint line numbers
input.last_layout = Some(prepaint.last_layout.clone()); let mut offset_y = px(0.);
input.last_bounds = Some(bounds); if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
input.last_cursor_offset = Some(input.cursor_offset()); offset_y += invisible_top_padding;
input.set_input_bounds(input_bounds, cx);
input.last_selected_range = Some(selected_range); // Paint line number background
input.scroll_size = prepaint.scroll_size; window.paint_quad(fill(
input.line_number_width = prepaint.line_number_width; Bounds {
input origin: input_bounds.origin,
size: size(
prepaint.last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN,
input_bounds.size.height,
),
},
cx.theme().background,
));
// Each item is the normal lines.
for lines in line_numbers.iter() {
let p = point(input_bounds.origin.x, origin.y + offset_y);
for line in lines {
_ = line.paint(p, line_height, window, cx);
offset_y += line_height;
}
}
}
self.state.update(cx, |state, cx| {
state.last_layout = Some(prepaint.last_layout.clone());
state.last_bounds = Some(bounds);
state.last_cursor = Some(state.cursor());
state.set_input_bounds(input_bounds, cx);
state.last_selected_range = Some(selected_range);
state.scroll_size = prepaint.scroll_size;
state
.scroll_handle .scroll_handle
.set_offset(prepaint.cursor_scroll_offset); .set_offset(prepaint.cursor_scroll_offset);
state.deferred_scroll_offset = None;
cx.notify(); cx.notify();
}); });
if let Some(hitbox) = prepaint.hover_definition_hitbox.as_ref() {
window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox);
}
self.paint_mouse_listeners(window, cx); self.paint_mouse_listeners(window, cx);
} }
} }
+410 -378
View File
@@ -1,378 +1,410 @@
use gpui::SharedString; use gpui::SharedString;
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub enum MaskToken { pub enum MaskToken {
/// Digit, equivalent to `[0-9]` /// 0 Digit, equivalent to `[0]`
Digit, // Digit0,
/// Letter, equivalent to `[a-zA-Z]` /// Digit, equivalent to `[0-9]`
Letter, Digit,
/// Letter or digit, equivalent to `[a-zA-Z0-9]` /// Letter, equivalent to `[a-zA-Z]`
LetterOrDigit, Letter,
/// Separator /// Letter or digit, equivalent to `[a-zA-Z0-9]`
Sep(char), LetterOrDigit,
/// Any character /// Separator
Any, Sep(char),
} /// Any character
Any,
#[allow(unused)] }
impl MaskToken {
/// Check if the token is any character. #[allow(unused)]
pub fn is_any(&self) -> bool { impl MaskToken {
matches!(self, MaskToken::Any) /// Check if the token is any character.
} pub fn is_any(&self) -> bool {
matches!(self, MaskToken::Any)
/// Check if the token is a match for the given character. }
///
/// The separator is always a match any input character. /// Check if the token is a match for the given character.
fn is_match(&self, ch: char) -> bool { ///
match self { /// The separator is always a match any input character.
MaskToken::Digit => ch.is_ascii_digit(), fn is_match(&self, ch: char) -> bool {
MaskToken::Letter => ch.is_ascii_alphabetic(), match self {
MaskToken::LetterOrDigit => ch.is_ascii_alphanumeric(), MaskToken::Digit => ch.is_ascii_digit(),
MaskToken::Any => true, MaskToken::Letter => ch.is_ascii_alphabetic(),
MaskToken::Sep(c) => *c == ch, MaskToken::LetterOrDigit => ch.is_ascii_alphanumeric(),
} MaskToken::Any => true,
} MaskToken::Sep(c) => *c == ch,
}
/// Is the token a separator (Can be ignored) }
fn is_sep(&self) -> bool {
matches!(self, MaskToken::Sep(_)) /// Is the token a separator (Can be ignored)
} fn is_sep(&self) -> bool {
matches!(self, MaskToken::Sep(_))
/// Check if the token is a number. }
pub fn is_number(&self) -> bool {
matches!(self, MaskToken::Digit) /// Check if the token is a number.
} pub fn is_number(&self) -> bool {
matches!(self, MaskToken::Digit)
pub fn placeholder(&self) -> char { }
match self {
MaskToken::Sep(c) => *c, pub fn placeholder(&self) -> char {
_ => '_', match self {
} MaskToken::Sep(c) => *c,
} _ => '_',
}
fn mask_char(&self, ch: char) -> char { }
match self {
MaskToken::Digit | MaskToken::LetterOrDigit | MaskToken::Letter => ch, fn mask_char(&self, ch: char) -> char {
MaskToken::Sep(c) => *c, match self {
MaskToken::Any => ch, MaskToken::Digit | MaskToken::LetterOrDigit | MaskToken::Letter => ch,
} MaskToken::Sep(c) => *c,
} MaskToken::Any => ch,
}
fn unmask_char(&self, ch: char) -> Option<char> { }
match self {
MaskToken::Digit => Some(ch), fn unmask_char(&self, ch: char) -> Option<char> {
MaskToken::Letter => Some(ch), match self {
MaskToken::LetterOrDigit => Some(ch), MaskToken::Digit => Some(ch),
MaskToken::Any => Some(ch), MaskToken::Letter => Some(ch),
_ => None, MaskToken::LetterOrDigit => Some(ch),
} MaskToken::Any => Some(ch),
} _ => None,
} }
}
#[derive(Clone, Default)] }
pub enum MaskPattern {
#[default] #[derive(Clone, Default)]
None, pub enum MaskPattern {
Pattern { #[default]
pattern: SharedString, None,
tokens: Vec<MaskToken>, Pattern {
}, pattern: SharedString,
Number { tokens: Vec<MaskToken>,
/// Group separator, e.g. "," or " " },
separator: Option<char>, Number {
/// Number of fraction digits, e.g. 2 for 123.45 /// Group separator, e.g. "," or " "
fraction: Option<usize>, separator: Option<char>,
}, /// Number of fraction digits, e.g. 2 for 123.45
} fraction: Option<usize>,
},
impl From<&str> for MaskPattern { }
fn from(pattern: &str) -> Self {
Self::new(pattern) impl From<&str> for MaskPattern {
} fn from(pattern: &str) -> Self {
} Self::new(pattern)
}
impl MaskPattern { }
/// Create a new mask pattern
/// impl MaskPattern {
/// - `9` - Digit /// Create a new mask pattern
/// - `A` - Letter ///
/// - `#` - Letter or Digit /// - `9` - Digit
/// - `*` - Any character /// - `A` - Letter
/// - other characters - Separator /// - `#` - Letter or Digit
/// /// - `*` - Any character
/// For example: /// - other characters - Separator
/// ///
/// - `(999)999-9999` - US phone number: (123)456-7890 /// For example:
/// - `99999-9999` - ZIP code: 12345-6789 ///
/// - `AAAA-99-####` - Custom pattern: ABCD-12-3AB4 /// - `(999)999-9999` - US phone number: (123)456-7890
/// - `*999*` - Custom pattern: (123) or [123] /// - `99999-9999` - ZIP code: 12345-6789
pub fn new(pattern: &str) -> Self { /// - `AAAA-99-####` - Custom pattern: ABCD-12-3AB4
let tokens = pattern /// - `*999*` - Custom pattern: (123) or [123]
.chars() pub fn new(pattern: &str) -> Self {
.map(|ch| match ch { let tokens = pattern
// '0' => MaskToken::Digit0, .chars()
'9' => MaskToken::Digit, .map(|ch| match ch {
'A' => MaskToken::Letter, // '0' => MaskToken::Digit0,
'#' => MaskToken::LetterOrDigit, '9' => MaskToken::Digit,
'*' => MaskToken::Any, 'A' => MaskToken::Letter,
_ => MaskToken::Sep(ch), '#' => MaskToken::LetterOrDigit,
}) '*' => MaskToken::Any,
.collect(); _ => MaskToken::Sep(ch),
})
Self::Pattern { .collect();
pattern: pattern.to_owned().into(),
tokens, Self::Pattern {
} pattern: pattern.to_owned().into(),
} tokens,
}
#[allow(unused)] }
fn tokens(&self) -> Option<&Vec<MaskToken>> {
match self { #[allow(unused)]
Self::Pattern { tokens, .. } => Some(tokens), fn tokens(&self) -> Option<&Vec<MaskToken>> {
Self::Number { .. } => None, match self {
Self::None => None, Self::Pattern { tokens, .. } => Some(tokens),
} Self::Number { .. } => None,
} Self::None => None,
}
/// Create a new mask pattern with group separator, e.g. "," or " " }
pub fn number(sep: Option<char>) -> Self {
Self::Number { /// Create a new mask pattern with group separator, e.g. "," or " "
separator: sep, pub fn number(sep: Option<char>) -> Self {
fraction: None, Self::Number {
} separator: sep,
} fraction: None,
}
pub fn placeholder(&self) -> Option<String> { }
match self {
Self::Pattern { tokens, .. } => { pub fn placeholder(&self) -> Option<String> {
Some(tokens.iter().map(|token| token.placeholder()).collect()) match self {
} Self::Pattern { tokens, .. } => {
Self::Number { .. } => None, Some(tokens.iter().map(|token| token.placeholder()).collect())
Self::None => None, }
} Self::Number { .. } => None,
} Self::None => None,
}
/// Return true if the mask pattern is None or no any pattern. }
pub fn is_none(&self) -> bool {
match self { /// Return true if the mask pattern is None or no any pattern.
Self::Pattern { tokens, .. } => tokens.is_empty(), pub fn is_none(&self) -> bool {
Self::Number { .. } => false, match self {
Self::None => true, Self::Pattern { tokens, .. } => tokens.is_empty(),
} Self::Number { .. } => false,
} Self::None => true,
}
/// Check is the mask text is valid. }
///
/// If the mask pattern is None, always return true. /// Check is the mask text is valid.
pub fn is_valid(&self, mask_text: &str) -> bool { ///
if self.is_none() { /// If the mask pattern is None, always return true.
return true; pub fn is_valid(&self, mask_text: &str) -> bool {
} if self.is_none() {
return true;
let mut text_index = 0; }
let mask_text_chars: Vec<char> = mask_text.chars().collect();
match self { let mut text_index = 0;
Self::Pattern { tokens, .. } => { let mask_text_chars: Vec<char> = mask_text.chars().collect();
for token in tokens { match self {
if text_index >= mask_text_chars.len() { Self::Pattern { tokens, .. } => {
break; for token in tokens {
} if text_index >= mask_text_chars.len() {
break;
let ch = mask_text_chars[text_index]; }
if token.is_match(ch) {
text_index += 1; let ch = mask_text_chars[text_index];
} if token.is_match(ch) {
} text_index += 1;
text_index == mask_text.len() }
} }
Self::Number { separator, .. } => { text_index == mask_text.len()
if mask_text.is_empty() { }
return true; Self::Number { separator, .. } => {
} if mask_text.is_empty() {
return true;
// check if the text is valid number }
let mut parts = mask_text.split('.');
let int_part = parts.next().unwrap_or(""); // check if the text is valid number
let frac_part = parts.next(); let mut parts = mask_text.split('.');
let int_part = parts.next().unwrap_or("");
if int_part.is_empty() { let frac_part = parts.next();
return false;
} if int_part.is_empty() {
return false;
// check if the integer part is valid }
if !int_part
.chars() let sign_positions: Vec<usize> = int_part
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator) .chars()
{ .enumerate()
return false; .filter_map(|(i, ch)| match is_sign(&ch) {
} true => Some(i),
false => None,
// check if the fraction part is valid })
if let Some(frac) = frac_part { .collect();
if !frac
.chars() // only one sign is valid
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator) // sign is only valid at the beginning of the string
{ if sign_positions.len() > 1 || sign_positions.first() > Some(&0) {
return false; return false;
} }
}
// check if the integer part is valid
true if !int_part.chars().enumerate().all(|(i, ch)| {
} ch.is_ascii_digit() || is_sign(&ch) && i == 0 || Some(ch) == *separator
Self::None => true, }) {
} return false;
} }
/// Check if valid input char at the given position. // check if the fraction part is valid
pub fn is_valid_at(&self, ch: char, pos: usize) -> bool { if let Some(frac) = frac_part {
if self.is_none() { if !frac
return true; .chars()
} .all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
{
match self { return false;
Self::Pattern { tokens, .. } => { }
if let Some(token) = tokens.get(pos) { }
if token.is_match(ch) {
return true; true
} }
Self::None => true,
if token.is_sep() { }
// If next token is match, it's valid }
if let Some(next_token) = tokens.get(pos + 1) {
if next_token.is_match(ch) { /// Check if valid input char at the given position.
return true; pub fn is_valid_at(&self, ch: char, pos: usize) -> bool {
} if self.is_none() {
} return true;
} }
}
match self {
false Self::Pattern { tokens, .. } => {
} if let Some(token) = tokens.get(pos) {
Self::Number { .. } => true, if token.is_match(ch) {
Self::None => true, return true;
} }
}
if token.is_sep() {
/// Format the text according to the mask pattern // If next token is match, it's valid
/// if let Some(next_token) = tokens.get(pos + 1) {
/// For example: if next_token.is_match(ch) {
/// return true;
/// - pattern: (999)999-999 }
/// - text: 123456789 }
/// - mask_text: (123)456-789 }
pub fn mask(&self, text: &str) -> SharedString { }
if self.is_none() {
return text.to_owned().into(); false
} }
Self::Number { .. } => true,
match self { Self::None => true,
Self::Number { }
separator, }
fraction,
} => { /// Format the text according to the mask pattern
if let Some(sep) = *separator { ///
// Remove the existing group separator /// For example:
let text = text.replace(sep, ""); ///
/// - pattern: (999)999-999
let mut parts = text.split('.'); /// - text: 123456789
let int_part = parts.next().unwrap_or(""); /// - mask_text: (123)456-789
pub fn mask(&self, text: &str) -> SharedString {
// Limit the fraction part to the given range, if not enough, pad with 0 if self.is_none() {
let frac_part = parts.next().map(|part| { return text.to_owned().into();
part.chars() }
.take(fraction.unwrap_or(usize::MAX))
.collect::<String>() match self {
}); Self::Number {
separator,
// Reverse the integer part for easier grouping fraction,
let chars: Vec<char> = int_part.chars().rev().collect(); } => {
let mut result = String::new(); if let Some(sep) = *separator {
for (i, ch) in chars.iter().enumerate() { // Remove the existing group separator
if i > 0 && i % 3 == 0 { let text = text.replace(sep, "");
result.push(sep);
} let mut parts = text.split('.');
result.push(*ch); let int_part = parts.next().unwrap_or("");
}
let int_with_sep: String = result.chars().rev().collect(); // Limit the fraction part to the given range, if not enough, pad with 0
let frac_part = parts.next().map(|part| {
let final_str = if let Some(frac) = frac_part { part.chars()
if fraction == &Some(0) { .take(fraction.unwrap_or(usize::MAX))
int_with_sep .collect::<String>()
} else { });
format!("{int_with_sep}.{frac}")
} // Reverse the integer part for easier grouping
} else { let mut chars: Vec<char> = int_part.chars().rev().collect();
int_with_sep
}; // Removing the sign from formatting to avoid cases such as: -,123
return final_str.into(); let maybe_signed = chars.iter().position(is_sign).map(|pos| chars.remove(pos));
}
let mut result = String::new();
text.to_owned().into() for (i, ch) in chars.iter().enumerate() {
} if i > 0 && i % 3 == 0 {
Self::Pattern { tokens, .. } => { result.push(sep);
let mut result = String::new(); }
let mut text_index = 0; result.push(*ch);
let text_chars: Vec<char> = text.chars().collect(); }
for (pos, token) in tokens.iter().enumerate() { let int_with_sep: String = result.chars().rev().collect();
if text_index >= text_chars.len() {
break; let final_str = if let Some(frac) = frac_part {
} if fraction == &Some(0) {
let ch = text_chars[text_index]; int_with_sep
// Break if expected char is not match } else {
if !token.is_sep() && !self.is_valid_at(ch, pos) { format!("{int_with_sep}.{frac}")
break; }
} } else {
let mask_ch = token.mask_char(ch); int_with_sep
result.push(mask_ch); };
if ch == mask_ch {
text_index += 1; let final_str = if let Some(sign) = maybe_signed {
continue; format!("{sign}{final_str}")
} } else {
} final_str
result.into() };
}
Self::None => text.to_owned().into(), return final_str.into();
} }
}
text.to_owned().into()
/// Extract original text from masked text }
pub fn unmask(&self, mask_text: &str) -> String { Self::Pattern { tokens, .. } => {
match self { let mut result = String::new();
Self::Number { separator, .. } => { let mut text_index = 0;
if let Some(sep) = *separator { let text_chars: Vec<char> = text.chars().collect();
let mut result = String::new(); for (pos, token) in tokens.iter().enumerate() {
for ch in mask_text.chars() { if text_index >= text_chars.len() {
if ch == sep { break;
continue; }
} let ch = text_chars[text_index];
result.push(ch); // Break if expected char is not match
} if !token.is_sep() && !self.is_valid_at(ch, pos) {
break;
if result.contains('.') { }
result = result.trim_end_matches('0').to_string(); let mask_ch = token.mask_char(ch);
} result.push(mask_ch);
return result; if ch == mask_ch {
} text_index += 1;
continue;
mask_text.to_owned() }
} }
Self::Pattern { tokens, .. } => { result.into()
let mut result = String::new(); }
let mask_text_chars: Vec<char> = mask_text.chars().collect(); Self::None => text.to_owned().into(),
for (text_index, token) in tokens.iter().enumerate() { }
if text_index >= mask_text_chars.len() { }
break;
} /// Extract original text from masked text
let ch = mask_text_chars[text_index]; pub fn unmask(&self, mask_text: &str) -> String {
let unmask_ch = token.unmask_char(ch); match self {
if let Some(ch) = unmask_ch { Self::Number { separator, .. } => {
result.push(ch); if let Some(sep) = *separator {
} let mut result = String::new();
} for ch in mask_text.chars() {
result if ch == sep {
} continue;
Self::None => mask_text.to_owned(), }
} result.push(ch);
} }
}
if result.contains('.') {
result = result.trim_end_matches('0').to_string();
}
return result;
}
mask_text.to_owned()
}
Self::Pattern { tokens, .. } => {
let mut result = String::new();
let mask_text_chars: Vec<char> = mask_text.chars().collect();
for (text_index, token) in tokens.iter().enumerate() {
if text_index >= mask_text_chars.len() {
break;
}
let ch = mask_text_chars[text_index];
let unmask_ch = token.unmask_char(ch);
if let Some(ch) = unmask_ch {
result.push(ch);
}
}
result
}
Self::None => mask_text.to_owned(),
}
}
}
#[inline]
fn is_sign(ch: &char) -> bool {
matches!(ch, '+' | '-')
}
+3 -1
View File
@@ -1,13 +1,15 @@
mod blink_cursor; mod blink_cursor;
mod change; mod change;
mod cursor;
mod element; mod element;
mod mask_pattern; mod mask_pattern;
mod mode;
mod rope_ext;
mod state; mod state;
mod text_input; mod text_input;
mod text_wrapper; mod text_wrapper;
pub(crate) mod clear_button; pub(crate) mod clear_button;
#[allow(ambiguous_glob_reexports)]
pub use state::*; pub use state::*;
pub use text_input::*; pub use text_input::*;
+129
View File
@@ -0,0 +1,129 @@
use gpui::SharedString;
use super::text_wrapper::TextWrapper;
#[derive(Debug, Copy, Clone)]
pub struct TabSize {
/// Default is 2
pub tab_size: usize,
/// Set true to use `\t` as tab indent, default is false
pub hard_tabs: bool,
}
impl Default for TabSize {
fn default() -> Self {
Self {
tab_size: 2,
hard_tabs: false,
}
}
}
impl TabSize {
pub(super) fn to_string(self) -> SharedString {
if self.hard_tabs {
"\t".into()
} else {
" ".repeat(self.tab_size).into()
}
}
}
#[derive(Default, Clone)]
pub enum InputMode {
#[default]
SingleLine,
MultiLine {
tab: TabSize,
rows: usize,
},
AutoGrow {
rows: usize,
min_rows: usize,
max_rows: usize,
},
}
#[allow(unused)]
impl InputMode {
#[inline]
pub(super) fn is_single_line(&self) -> bool {
matches!(self, InputMode::SingleLine)
}
#[inline]
pub(super) fn is_auto_grow(&self) -> bool {
matches!(self, InputMode::AutoGrow { .. })
}
#[inline]
pub(super) fn is_multi_line(&self) -> bool {
matches!(
self,
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. }
)
}
pub(super) fn set_rows(&mut self, new_rows: usize) {
match self {
InputMode::MultiLine { rows, .. } => {
*rows = new_rows;
}
InputMode::AutoGrow {
rows,
min_rows,
max_rows,
} => {
*rows = new_rows.clamp(*min_rows, *max_rows);
}
_ => {}
}
}
pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
if self.is_single_line() {
return;
}
let wrapped_lines = text_wrapper.len();
self.set_rows(wrapped_lines);
}
/// At least 1 row be return.
pub(super) fn rows(&self) -> usize {
match self {
InputMode::MultiLine { rows, .. } => *rows,
InputMode::AutoGrow { rows, .. } => *rows,
_ => 1,
}
.max(1)
}
/// At least 1 row be return.
#[allow(unused)]
pub(super) fn min_rows(&self) -> usize {
match self {
InputMode::MultiLine { .. } => 1,
InputMode::AutoGrow { min_rows, .. } => *min_rows,
_ => 1,
}
.max(1)
}
#[allow(unused)]
pub(super) fn max_rows(&self) -> usize {
match self {
InputMode::MultiLine { .. } => usize::MAX,
InputMode::AutoGrow { max_rows, .. } => *max_rows,
_ => 1,
}
}
#[inline]
pub(super) fn tab_size(&self) -> Option<&TabSize> {
match self {
InputMode::MultiLine { tab, .. } => Some(tab),
_ => None,
}
}
}
+207
View File
@@ -0,0 +1,207 @@
use std::ops::Range;
use rope::{Point, Rope};
use super::cursor::Position;
/// An extension trait for `Rope` to provide additional utility methods.
pub trait RopeExt {
/// Get the line at the given row (0-based) index, including the `\r` at the end, but not `\n`.
///
/// Return empty rope if the row (0-based) is out of bounds.
fn line(&self, row: usize) -> Rope;
/// Start offset of the line at the given row (0-based) index.
fn line_start_offset(&self, row: usize) -> usize;
/// Line the end offset (including `\n`) of the line at the given row (0-based) index.
///
/// Return the end of the rope if the row is out of bounds.
fn line_end_offset(&self, row: usize) -> usize;
/// Return the number of lines in the rope.
fn lines_len(&self) -> usize;
/// Return the lines iterator.
///
/// Each line is including the `\r` at the end, but not `\n`.
fn lines(&self) -> RopeLines;
/// Check is equal to another rope.
fn eq(&self, other: &Rope) -> bool;
/// Total number of characters in the rope.
fn chars_count(&self) -> usize;
/// Get char at the given offset (byte).
///
/// If the offset is in the middle of a multi-byte character will panic.
///
/// If the offset is out of bounds, return None.
fn char_at(&self, offset: usize) -> Option<char>;
/// Get the byte offset from the given line, column [`Position`] (0-based).
fn position_to_offset(&self, line_col: &Position) -> usize;
/// Get the line, column [`Position`] (0-based) from the given byte offset.
fn offset_to_position(&self, offset: usize) -> Position;
/// Get the word byte range at the given offset (byte).
fn word_range(&self, offset: usize) -> Option<Range<usize>>;
/// Get word at the given offset (byte).
#[allow(dead_code)]
fn word_at(&self, offset: usize) -> String;
}
/// An iterator over the lines of a `Rope`.
pub struct RopeLines {
row: usize,
end_row: usize,
rope: Rope,
}
impl RopeLines {
/// Create a new `RopeLines` iterator.
pub fn new(rope: Rope) -> Self {
let end_row = rope.lines_len();
Self {
row: 0,
end_row,
rope,
}
}
}
impl Iterator for RopeLines {
type Item = Rope;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
if self.row >= self.end_row {
return None;
}
let line = self.rope.line(self.row);
self.row += 1;
Some(line)
}
#[inline]
fn nth(&mut self, n: usize) -> Option<Self::Item> {
self.row = self.row.saturating_add(n);
self.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.end_row - self.row;
(len, Some(len))
}
}
impl std::iter::ExactSizeIterator for RopeLines {}
impl std::iter::FusedIterator for RopeLines {}
impl RopeExt for Rope {
fn line(&self, row: usize) -> Rope {
let start = self.line_start_offset(row);
let end = start + self.line_len(row as u32) as usize;
self.slice(start..end)
}
fn line_start_offset(&self, row: usize) -> usize {
let row = row as u32;
self.point_to_offset(Point::new(row, 0))
}
fn position_to_offset(&self, pos: &Position) -> usize {
let line = self.line(pos.line as usize);
self.line_start_offset(pos.line as usize)
+ line
.chars()
.take(pos.character as usize)
.map(|c| c.len_utf8())
.sum::<usize>()
}
fn offset_to_position(&self, offset: usize) -> Position {
let point = self.offset_to_point(offset);
let line = self.line(point.row as usize);
let column = line.clip_offset(point.column as usize, sum_tree::Bias::Left);
let character = line.slice(0..column).chars().count();
Position::new(point.row, character as u32)
}
fn line_end_offset(&self, row: usize) -> usize {
if row > self.max_point().row as usize {
return self.len();
}
self.line_start_offset(row) + self.line_len(row as u32) as usize
}
fn lines_len(&self) -> usize {
self.max_point().row as usize + 1
}
fn lines(&self) -> RopeLines {
RopeLines::new(self.clone())
}
fn eq(&self, other: &Rope) -> bool {
self.summary() == other.summary()
}
fn chars_count(&self) -> usize {
self.chars().count()
}
fn char_at(&self, offset: usize) -> Option<char> {
if offset > self.len() {
return None;
}
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
self.slice(offset..self.len()).chars().next()
}
fn word_range(&self, offset: usize) -> Option<Range<usize>> {
if offset >= self.len() {
return None;
}
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
let mut left = String::new();
for c in self.reversed_chars_at(offset) {
if c.is_alphanumeric() || c == '_' {
left.insert(0, c);
} else {
break;
}
}
let start = offset.saturating_sub(left.len());
let right = self
.chars_at(offset)
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect::<String>();
let end = offset + right.len();
if start == end {
None
} else {
Some(start..end)
}
}
fn word_at(&self, offset: usize) -> String {
if let Some(range) = self.word_range(offset) {
self.slice(range).to_string()
} else {
String::new()
}
}
}
File diff suppressed because it is too large Load Diff
+98 -94
View File
@@ -6,11 +6,10 @@ use gpui::{
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use super::InputState; use super::clear_button::clear_button;
use crate::button::{Button, ButtonVariants as _}; use super::state::{InputState, CONTEXT};
use crate::button::{Button, ButtonVariants};
use crate::indicator::Indicator; use crate::indicator::Indicator;
use crate::input::clear_button::clear_button;
use crate::scroll::{Scrollbar, ScrollbarAxis};
use crate::{h_flex, IconName, Sizable, Size, StyleSized, StyledExt}; use crate::{h_flex, IconName, Sizable, Size, StyleSized, StyledExt};
#[derive(IntoElement)] #[derive(IntoElement)]
@@ -18,7 +17,6 @@ pub struct TextInput {
state: Entity<InputState>, state: Entity<InputState>,
style: StyleRefinement, style: StyleRefinement,
size: Size, size: Size,
no_gap: bool,
prefix: Option<AnyElement>, prefix: Option<AnyElement>,
suffix: Option<AnyElement>, suffix: Option<AnyElement>,
height: Option<DefiniteLength>, height: Option<DefiniteLength>,
@@ -26,6 +24,8 @@ pub struct TextInput {
cleanable: bool, cleanable: bool,
mask_toggle: bool, mask_toggle: bool,
disabled: bool, disabled: bool,
bordered: bool,
focus_bordered: bool,
} }
impl Sizable for TextInput { impl Sizable for TextInput {
@@ -40,9 +40,8 @@ impl TextInput {
pub fn new(state: &Entity<InputState>) -> Self { pub fn new(state: &Entity<InputState>) -> Self {
Self { Self {
state: state.clone(), state: state.clone(),
style: StyleRefinement::default(),
size: Size::default(), size: Size::default(),
no_gap: false, style: StyleRefinement::default(),
prefix: None, prefix: None,
suffix: None, suffix: None,
height: None, height: None,
@@ -50,6 +49,8 @@ impl TextInput {
cleanable: false, cleanable: false,
mask_toggle: false, mask_toggle: false,
disabled: false, disabled: false,
bordered: true,
focus_bordered: true,
} }
} }
@@ -75,12 +76,24 @@ impl TextInput {
self self
} }
/// Set the appearance of the input field. /// Set the appearance of the input field, if false the input field will no border, background.
pub fn appearance(mut self, appearance: bool) -> Self { pub fn appearance(mut self, appearance: bool) -> Self {
self.appearance = appearance; self.appearance = appearance;
self 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 true to show the clear button when the input field is not empty. /// Set true to show the clear button when the input field is not empty.
pub fn cleanable(mut self) -> Self { pub fn cleanable(mut self) -> Self {
self.cleanable = true; self.cleanable = true;
@@ -99,15 +112,6 @@ impl TextInput {
self self
} }
/// Set true to not use gap between input and prefix, suffix, and clear button.
///
/// Default: false
#[allow(dead_code)]
pub(super) fn no_gap(mut self) -> Self {
self.no_gap = true;
self
}
fn render_toggle_mask_button(state: Entity<InputState>) -> impl IntoElement { fn render_toggle_mask_button(state: Entity<InputState>) -> impl IntoElement {
Button::new("toggle-mask") Button::new("toggle-mask")
.icon(IconName::Eye) .icon(IconName::Eye)
@@ -132,44 +136,51 @@ impl TextInput {
} }
} }
impl Styled for TextInput {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for TextInput { impl RenderOnce for TextInput {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
const LINE_HEIGHT: Rems = Rems(1.25); const LINE_HEIGHT: Rems = Rems(1.25);
let font = window.text_style().font();
let font_size = window.text_style().font_size.to_pixels(window.rem_size());
self.state.update(cx, |state, _| { self.state.update(cx, |state, cx| {
state.mode.set_height(self.height); state.text_wrapper.set_font(font, font_size, cx);
state.disabled = self.disabled; state.disabled = self.disabled;
}); });
let state = self.state.read(cx); let state = self.state.read(cx);
let focused = state.focus_handle.is_focused(window);
let mut gap_x = match self.size { let gap_x = match self.size {
Size::Small => px(4.), Size::Small => px(4.),
Size::Large => px(8.), Size::Large => px(8.),
_ => px(4.), _ => px(4.),
}; };
if self.no_gap {
gap_x = px(0.);
}
let prefix = self.prefix;
let suffix = self.suffix;
let show_clear_button =
self.cleanable && !state.loading && !state.text.is_empty() && state.is_single_line();
let bg = if state.disabled { let bg = if state.disabled {
cx.theme().surface_background cx.theme().surface_background
} else { } else {
cx.theme().elevated_surface_background cx.theme().elevated_surface_background
}; };
let prefix = self.prefix;
let suffix = self.suffix;
let show_clear_button = self.cleanable
&& !state.loading
&& !state.text.is_empty()
&& state.mode.is_single_line();
let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;
div() div()
.id(("input", self.state.entity_id())) .id(("input", self.state.entity_id()))
.flex() .flex()
.key_context(crate::input::CONTEXT) .key_context(CONTEXT)
.track_focus(&state.focus_handle) .track_focus(&state.focus_handle)
.when(!state.disabled, |this| { .when(!state.disabled, |this| {
this.on_action(window.listener_for(&self.state, InputState::backspace)) this.on_action(window.listener_for(&self.state, InputState::backspace))
@@ -182,17 +193,31 @@ impl RenderOnce for TextInput {
.on_action(window.listener_for(&self.state, InputState::delete_next_word)) .on_action(window.listener_for(&self.state, InputState::delete_next_word))
.on_action(window.listener_for(&self.state, InputState::enter)) .on_action(window.listener_for(&self.state, InputState::enter))
.on_action(window.listener_for(&self.state, InputState::escape)) .on_action(window.listener_for(&self.state, InputState::escape))
.on_action(window.listener_for(&self.state, InputState::paste))
.on_action(window.listener_for(&self.state, InputState::cut))
.on_action(window.listener_for(&self.state, InputState::undo))
.on_action(window.listener_for(&self.state, InputState::redo))
.when(state.mode.is_multi_line(), |this| {
this.on_action(window.listener_for(&self.state, InputState::indent_inline))
.on_action(window.listener_for(&self.state, InputState::outdent_inline))
.on_action(window.listener_for(&self.state, InputState::indent_block))
.on_action(window.listener_for(&self.state, InputState::outdent_block))
.on_action(
window.listener_for(&self.state, InputState::shift_to_new_line),
)
})
}) })
.on_action(window.listener_for(&self.state, InputState::left)) .on_action(window.listener_for(&self.state, InputState::left))
.on_action(window.listener_for(&self.state, InputState::right)) .on_action(window.listener_for(&self.state, InputState::right))
.on_action(window.listener_for(&self.state, InputState::select_left)) .on_action(window.listener_for(&self.state, InputState::select_left))
.on_action(window.listener_for(&self.state, InputState::select_right)) .on_action(window.listener_for(&self.state, InputState::select_right))
.when(state.is_multi_line(), |this| { .when(state.mode.is_multi_line(), |this| {
this.on_action(window.listener_for(&self.state, InputState::up)) this.on_action(window.listener_for(&self.state, InputState::up))
.on_action(window.listener_for(&self.state, InputState::down)) .on_action(window.listener_for(&self.state, InputState::down))
.on_action(window.listener_for(&self.state, InputState::select_up)) .on_action(window.listener_for(&self.state, InputState::select_up))
.on_action(window.listener_for(&self.state, InputState::select_down)) .on_action(window.listener_for(&self.state, InputState::select_down))
.on_action(window.listener_for(&self.state, InputState::shift_to_new_line)) .on_action(window.listener_for(&self.state, InputState::page_up))
.on_action(window.listener_for(&self.state, InputState::page_down))
}) })
.on_action(window.listener_for(&self.state, InputState::select_all)) .on_action(window.listener_for(&self.state, InputState::select_all))
.on_action(window.listener_for(&self.state, InputState::select_to_start_of_line)) .on_action(window.listener_for(&self.state, InputState::select_to_start_of_line))
@@ -209,90 +234,69 @@ impl RenderOnce for TextInput {
.on_action(window.listener_for(&self.state, InputState::select_to_end)) .on_action(window.listener_for(&self.state, InputState::select_to_end))
.on_action(window.listener_for(&self.state, InputState::show_character_palette)) .on_action(window.listener_for(&self.state, InputState::show_character_palette))
.on_action(window.listener_for(&self.state, InputState::copy)) .on_action(window.listener_for(&self.state, InputState::copy))
.on_action(window.listener_for(&self.state, InputState::paste))
.on_action(window.listener_for(&self.state, InputState::cut))
.on_action(window.listener_for(&self.state, InputState::undo))
.on_action(window.listener_for(&self.state, InputState::redo))
.on_key_down(window.listener_for(&self.state, InputState::on_key_down)) .on_key_down(window.listener_for(&self.state, InputState::on_key_down))
.on_mouse_down( .on_mouse_down(
MouseButton::Left, MouseButton::Left,
window.listener_for(&self.state, InputState::on_mouse_down), window.listener_for(&self.state, InputState::on_mouse_down),
) )
.on_mouse_down( .on_mouse_down(
MouseButton::Middle, MouseButton::Right,
window.listener_for(&self.state, InputState::on_mouse_down), window.listener_for(&self.state, InputState::on_mouse_down),
) )
.on_mouse_up( .on_mouse_up(
MouseButton::Left, MouseButton::Left,
window.listener_for(&self.state, InputState::on_mouse_up), window.listener_for(&self.state, InputState::on_mouse_up),
) )
.on_mouse_up(
MouseButton::Right,
window.listener_for(&self.state, InputState::on_mouse_up),
)
.on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel)) .on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
.size_full() .size_full()
.line_height(LINE_HEIGHT) .line_height(LINE_HEIGHT)
.cursor_text() .input_px(self.size)
.input_py(self.size) .input_py(self.size)
.input_h(self.size) .input_h(self.size)
.when(state.is_multi_line(), |this| { .cursor_text()
.text_size(font_size)
.items_center()
.when(state.mode.is_multi_line(), |this| {
this.h_auto() this.h_auto()
.when_some(self.height, |this, height| this.h(height)) .when_some(self.height, |this, height| this.h(height))
}) })
.when(self.appearance, |this| { .when(self.appearance, |this| {
this.bg(bg) this.bg(bg).rounded(cx.theme().radius)
.rounded(cx.theme().radius)
.when(focused, |this| this.border_color(cx.theme().ring))
}) })
.when(prefix.is_none(), |this| this.input_pl(self.size))
.input_pr(self.size)
.items_center() .items_center()
.gap(gap_x) .gap(gap_x)
.children(prefix)
// TODO: Define height here, and use it in the input element
.child(self.state.clone())
.child(
h_flex()
.id("suffix")
.absolute()
.gap(gap_x)
.when(self.appearance, |this| this.bg(bg))
.items_center()
.when(suffix.is_none(), |this| this.pr_1())
.right_0()
.when(state.loading, |this| {
this.child(Indicator::new().color(cx.theme().text_muted))
})
.when(self.mask_toggle, |this| {
this.child(Self::render_toggle_mask_button(self.state.clone()))
})
.when(show_clear_button, |this| {
this.child(clear_button(cx).on_click({
let state = self.state.clone();
move |_, window, cx| {
state.update(cx, |state, cx| {
state.clean(window, cx);
})
}
}))
})
.children(suffix),
)
.when(state.is_multi_line(), |this| {
if state.last_layout.is_some() {
this.relative().child(
div()
.absolute()
.top_0()
.left_0()
.right(px(1.))
.bottom_0()
.child(
Scrollbar::vertical(&state.scrollbar_state, &state.scroll_handle)
.axis(ScrollbarAxis::Vertical),
),
)
} else {
this
}
})
.refine_style(&self.style) .refine_style(&self.style)
.children(prefix)
.child(self.state.clone())
.when(has_suffix, |this| {
this.pr_2().child(
h_flex()
.id("suffix")
.gap(gap_x)
.when(self.appearance, |this| this.bg(bg))
.items_center()
.when(state.loading, |this| {
this.child(Indicator::new().color(cx.theme().text_muted))
})
.when(self.mask_toggle, |this| {
this.child(Self::render_toggle_mask_button(self.state.clone()))
})
.when(show_clear_button, |this| {
this.child(clear_button(cx).on_click({
let state = self.state.clone();
move |_, window, cx| {
state.update(cx, |state, cx| {
state.clean(window, cx);
})
}
}))
})
.children(suffix),
)
})
} }
} }
+215 -99
View File
@@ -1,99 +1,215 @@
use std::ops::Range; use std::ops::Range;
use gpui::{App, Font, LineFragment, Pixels, SharedString}; use gpui::{App, Font, LineFragment, Pixels};
use rope::Rope;
#[allow(unused)]
pub(super) struct LineWrap { use super::rope_ext::RopeExt;
/// The number of soft wrapped lines of this line (Not include first line.)
pub(super) wrap_lines: usize, /// A line with soft wrapped lines info.
/// The range of the line text in the entire text. #[derive(Clone)]
pub(super) range: Range<usize>, pub(super) struct LineItem {
} /// The original line text.
line: Rope,
impl LineWrap { /// The soft wrapped lines relative byte range (0..line.len) of this line (Include first line).
pub(super) fn height(&self, line_height: Pixels) -> Pixels { ///
line_height * (self.wrap_lines + 1) /// FIXME: Here in somecase, the `line_wrapper.wrap_line` has returned different
} /// like the `window.text_system().shape_text`. So, this value may not equal
} /// the actual rendered lines.
wrapped_lines: Vec<Range<usize>>,
/// Used to prepare the text with soft_wrap to be get lines to displayed in the TextArea }
///
/// After use lines to calculate the scroll size of the TextArea impl LineItem {
pub(super) struct TextWrapper { /// Get the bytes length of this line.
pub(super) text: SharedString, #[inline]
/// The wrapped lines, value is start and end index of the line (by split \n). pub(super) fn len(&self) -> usize {
pub(super) wrapped_lines: Vec<Range<usize>>, self.line.len()
/// The lines by split \n }
pub(super) lines: Vec<LineWrap>,
pub(super) font: Font, /// Get number of soft wrapped lines of this line (include the first line).
pub(super) font_size: Pixels, #[inline]
/// If is none, it means the text is not wrapped pub(super) fn lines_len(&self) -> usize {
pub(super) wrap_width: Option<Pixels>, self.wrapped_lines.len()
} }
#[allow(unused)] /// Get the height of this line item with given line height.
impl TextWrapper { pub(super) fn height(&self, line_height: Pixels) -> Pixels {
pub(super) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self { self.lines_len() as f32 * line_height
Self { }
text: SharedString::default(), }
font,
font_size, /// Used to prepare the text with soft wrap to be get lines to displayed in the Editor.
wrap_width, ///
wrapped_lines: Vec::new(), /// After use lines to calculate the scroll size of the Editor.
lines: Vec::new(), pub(super) struct TextWrapper {
} text: Rope,
} /// Total wrapped lines (Inlucde the first line), value is start and end index of the line.
soft_lines: usize,
pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) { font: Font,
self.wrap_width = wrap_width; font_size: Pixels,
self.update(&self.text.clone(), true, cx); /// If is none, it means the text is not wrapped
} wrap_width: Option<Pixels>,
/// The lines by split \n
pub(super) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) { pub(super) lines: Vec<LineItem>,
self.font = font; }
self.font_size = font_size;
self.update(&self.text.clone(), true, cx); #[allow(unused)]
} impl TextWrapper {
pub(super) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
pub(super) fn update(&mut self, text: &SharedString, force: bool, cx: &mut App) { Self {
if &self.text == text && !force { text: Rope::new(),
return; font,
} font_size,
wrap_width,
let mut wrapped_lines = vec![]; soft_lines: 0,
let mut lines = vec![]; lines: Vec::new(),
let wrap_width = self.wrap_width.unwrap_or(Pixels::MAX); }
let mut line_wrapper = cx }
.text_system()
.line_wrapper(self.font.clone(), self.font_size); #[inline]
pub(super) fn set_default_text(&mut self, text: &Rope) {
let mut prev_line_ix = 0; self.text = text.clone();
for line in text.split('\n') { }
let mut line_wraps = vec![];
let mut prev_boundary_ix = 0; /// Get the total number of lines including wrapped lines.
#[inline]
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty. pub(super) fn len(&self) -> usize {
for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) { self.soft_lines
line_wraps.push(prev_boundary_ix..boundary.ix); }
prev_boundary_ix = boundary.ix;
} /// Get the line item by row index.
#[inline]
lines.push(LineWrap { pub(super) fn line(&self, row: usize) -> Option<&LineItem> {
wrap_lines: line_wraps.len(), self.lines.get(row)
range: prev_line_ix..prev_line_ix + line.len(), }
});
pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
wrapped_lines.extend(line_wraps); if wrap_width == self.wrap_width {
// Reset of the line return;
if !line[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 { }
wrapped_lines.push(prev_line_ix + prev_boundary_ix..prev_line_ix + line.len());
} self.wrap_width = wrap_width;
self.update_all(&self.text.clone(), true, cx);
prev_line_ix += line.len() + 1; }
}
pub(super) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
self.text = text.clone(); if self.font.eq(&font) && self.font_size == font_size {
self.wrapped_lines = wrapped_lines; return;
self.lines = lines; }
}
} self.font = font;
self.font_size = font_size;
self.update_all(&self.text.clone(), true, cx);
}
/// Update the text wrapper and recalculate the wrapped lines.
///
/// If the `text` is the same as the current text, do nothing.
///
/// - `changed_text`: The text [`Rope`] that has changed.
/// - `range`: The `selected_range` before change.
/// - `new_text`: The inserted text.
/// - `force`: Whether to force the update, if false, the update will be skipped if the text is the same.
/// - `cx`: The application context.
pub(super) fn update(
&mut self,
changed_text: &Rope,
range: &Range<usize>,
new_text: &Rope,
force: bool,
cx: &mut App,
) {
let mut line_wrapper = cx
.text_system()
.line_wrapper(self.font.clone(), self.font_size);
self._update(
changed_text,
range,
new_text,
force,
&mut |line_str, wrap_width| {
line_wrapper
.wrap_line(&[LineFragment::text(line_str)], wrap_width)
.collect()
},
);
}
fn _update<F>(
&mut self,
changed_text: &Rope,
range: &Range<usize>,
new_text: &Rope,
force: bool,
wrap_line: &mut F,
) where
F: FnMut(&str, Pixels) -> Vec<gpui::Boundary>,
{
if self.text.eq(changed_text) && !force {
return;
}
// Remove the old changed lines.
let start_row = self.text.offset_to_point(range.start).row as usize;
let start_row = start_row.min(self.lines.len().saturating_sub(1));
let end_row = self.text.offset_to_point(range.end).row as usize;
let end_row = end_row.min(self.lines.len().saturating_sub(1));
let rows_range = start_row..=end_row;
// To add the new lines.
let new_start_row = changed_text.offset_to_point(range.start).row as usize;
let new_start_offset = changed_text.line_start_offset(new_start_row);
let new_end_row = changed_text
.offset_to_point(range.start + new_text.len())
.row as usize;
let new_end_offset = changed_text.line_end_offset(new_end_row);
let new_range = new_start_offset..new_end_offset;
let mut new_lines = vec![];
let wrap_width = self.wrap_width;
for line in changed_text.slice(new_range).lines() {
let line_str = line.to_string();
let mut wrapped_lines = vec![];
let mut prev_boundary_ix = 0;
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
if let Some(wrap_width) = wrap_width {
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
for boundary in wrap_line(&line_str, wrap_width) {
wrapped_lines.push(prev_boundary_ix..boundary.ix);
prev_boundary_ix = boundary.ix;
}
}
// Reset of the line
if !line_str[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
wrapped_lines.push(prev_boundary_ix..line.len());
}
new_lines.push(LineItem {
line: line.clone(),
wrapped_lines,
});
}
// dbg!(&new_lines.len());
// dbg!(self.lines.len());
if self.lines.is_empty() {
self.lines = new_lines;
} else {
self.lines.splice(rows_range, new_lines);
}
// dbg!(self.lines.len());
self.text = changed_text.clone();
self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
}
/// Update the text wrapper and recalculate the wrapped lines.
///
/// If the `text` is the same as the current text, do nothing.
pub(crate) fn update_all(&mut self, text: &Rope, force: bool, cx: &mut App) {
self.update(text, &(0..text.len()), text, force, cx);
}
}
+5 -3
View File
@@ -299,14 +299,16 @@ where
fn on_query_input_event( fn on_query_input_event(
&mut self, &mut self,
_: &Entity<InputState>, state: &Entity<InputState>,
event: &InputEvent, event: &InputEvent,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
match event { match event {
InputEvent::Change(text) => { InputEvent::Change => {
let text = state.read(cx).value();
let text = text.trim().to_string(); let text = text.trim().to_string();
if Some(&text) == self.last_query.as_ref() { if Some(&text) == self.last_query.as_ref() {
return; return;
} }
@@ -347,7 +349,7 @@ where
} }
} }
fn set_querying(&mut self, querying: bool, _: &mut Window, cx: &mut Context<Self>) { fn set_querying(&mut self, querying: bool, _window: &mut Window, cx: &mut Context<Self>) {
self.querying = querying; self.querying = querying;
if let Some(input) = &self.query_input { if let Some(input) = &self.query_input {
input.update(cx, |input, cx| input.set_loading(querying, cx)) input.update(cx, |input, cx| input.set_loading(querying, cx))
+2 -2
View File
@@ -13,7 +13,7 @@ use regex::Regex;
use registry::Registry; use registry::Registry;
use theme::ActiveTheme; use theme::ActiveTheme;
use crate::actions::OpenProfile; use crate::actions::OpenPublicKey;
static URL_REGEX: Lazy<Regex> = Lazy::new(|| { static URL_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^(?:[a-zA-Z]+://)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(/.*)?$").unwrap() Regex::new(r"^(?:[a-zA-Z]+://)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(/.*)?$").unwrap()
@@ -140,7 +140,7 @@ impl RenderedText {
log::error!("Failed to parse public key from: {clean_url}"); log::error!("Failed to parse public key from: {clean_url}");
return; return;
}; };
window.dispatch_action(Box::new(OpenProfile(public_key)), cx); window.dispatch_action(Box::new(OpenPublicKey(public_key)), cx);
} else if is_url(token) { } else if is_url(token) {
if !token.starts_with("http") { if !token.starts_with("http") {
cx.open_url(&format!("https://{token}")); cx.open_url(&format!("https://{token}"));
+3 -1
View File
@@ -272,9 +272,11 @@ profile:
unknown: unknown:
en: "Unknown contact" en: "Unknown contact"
njump: njump:
en: "Open in njump.me" en: "View on njump.me"
no_bio: no_bio:
en: "No bio." en: "No bio."
copy:
en: "Copy Public Key"
preferences: preferences:
account_header: account_header:
Executable
+79
View File
@@ -0,0 +1,79 @@
#!/bin/bash
# Script to release a new version of the application
# Usage: ./release <new_version>
set -e # Exit on any error
if [ $# -ne 1 ]; then
echo "Usage: $0 <new_version>"
echo "Example: $0 1.0.0"
exit 1
fi
NEW_VERSION="$1"
WORKSPACE_CARGO="Cargo.toml"
CRATE_CARGO="crates/coop/Cargo.toml"
# Check if both Cargo.toml files exist
if [ ! -f "$WORKSPACE_CARGO" ]; then
echo "Error: $WORKSPACE_CARGO not found in current directory"
exit 1
fi
if [ ! -f "$CRATE_CARGO" ]; then
echo "Error: $CRATE_CARGO not found"
exit 1
fi
# Function to update version in a Cargo.toml file
update_version() {
local file="$1"
local backup="${file}.bak"
# Backup the original file
cp "$file" "$backup"
# Replace the version in Cargo.toml
if sed -i.bak -E "s/^version = \"[0-9]+\.[0-9]+\.[0-9]+\"/version = \"$NEW_VERSION\"/" "$file"; then
echo "✓ Updated version to $NEW_VERSION in $file"
# Remove backup created by sed
if [ -f "${file}.bak" ]; then
rm "${file}.bak"
fi
else
echo "Error: Failed to update version in $file"
# Restore original backup
mv "$backup" "$file"
exit 1
fi
# Remove the initial backup file
rm -f "$backup"
}
# Update both Cargo.toml files
echo "Updating versions..."
update_version "$WORKSPACE_CARGO"
update_version "$CRATE_CARGO"
# Create git tag
TAG_NAME="v$NEW_VERSION"
COMMIT_MSG="Release version $NEW_VERSION"
if git tag -a "$TAG_NAME" -m "$COMMIT_MSG"; then
echo "✓ Created git tag: $TAG_NAME"
else
echo "Error: Failed to create git tag"
exit 1
fi
# Push to origin (both commits and tags)
echo "Pushing to origin..."
if git push origin master && git push origin "$TAG_NAME"; then
echo "✓ Successfully pushed to origin"
echo "✓ Release $NEW_VERSION completed successfully!"
else
echo "Error: Failed to push to origin"
exit 1
fi