chore: improve web support #36
+2
-2
@@ -19,8 +19,8 @@ dist/
|
|||||||
|
|
||||||
# Useless stuffs
|
# Useless stuffs
|
||||||
.DS_Store
|
.DS_Store
|
||||||
# Added by goreleaser init:
|
|
||||||
.intentionally-empty-file.o
|
|
||||||
|
|
||||||
.cargo/
|
.cargo/
|
||||||
vendor/
|
vendor/
|
||||||
|
wasm/
|
||||||
|
node_modules/
|
||||||
|
|||||||
Generated
+141
-115
@@ -316,9 +316,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-compression"
|
name = "async-compression"
|
||||||
version = "0.4.42"
|
version = "0.4.43"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac"
|
checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"compression-codecs",
|
"compression-codecs",
|
||||||
"compression-core",
|
"compression-core",
|
||||||
@@ -457,9 +457,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-utility"
|
name = "async-utility"
|
||||||
version = "0.3.1"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151"
|
checksum = "188f83b9a198af8c336e505611edb00d6d2ac5c694241c5a4f9a12316938cfe9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"gloo-timers",
|
"gloo-timers",
|
||||||
@@ -469,9 +469,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-wsocket"
|
name = "async-wsocket"
|
||||||
version = "0.15.0"
|
version = "0.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c69f24cf7a798027c6b0801de5bfd05d01fe343e87a8329f7c8b46a6d3bc4a79"
|
checksum = "efa9ef232c1731ddbe4a306b5b5dcf9e50a924d6bd086d4784939d14f8e78daa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -543,6 +543,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"common",
|
"common",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"instant",
|
||||||
"log",
|
"log",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1056,8 +1057,10 @@ dependencies = [
|
|||||||
"common",
|
"common",
|
||||||
"device",
|
"device",
|
||||||
"flume 0.11.1",
|
"flume 0.11.1",
|
||||||
|
"futures",
|
||||||
"fuzzy-matcher",
|
"fuzzy-matcher",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"instant",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
@@ -1076,6 +1079,7 @@ dependencies = [
|
|||||||
"chat",
|
"chat",
|
||||||
"common",
|
"common",
|
||||||
"flume 0.11.1",
|
"flume 0.11.1",
|
||||||
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"linkify",
|
"linkify",
|
||||||
@@ -1086,7 +1090,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
|
||||||
"state",
|
"state",
|
||||||
"theme",
|
"theme",
|
||||||
"ui",
|
"ui",
|
||||||
@@ -1130,9 +1133,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clang-sys"
|
name = "clang-sys"
|
||||||
version = "1.8.1"
|
version = "1.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glob",
|
"glob",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -1228,7 +1231,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui_util",
|
"gpui_util",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
@@ -1259,6 +1262,7 @@ dependencies = [
|
|||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
"futures",
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"instant",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -1384,21 +1388,21 @@ dependencies = [
|
|||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"console_log",
|
"console_log",
|
||||||
"device",
|
"device",
|
||||||
"errno",
|
|
||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui_platform",
|
"gpui_platform",
|
||||||
"gpui_web",
|
"gpui_web",
|
||||||
|
"instant",
|
||||||
"log",
|
"log",
|
||||||
"person",
|
"person",
|
||||||
"settings",
|
"settings",
|
||||||
"state",
|
"state",
|
||||||
"theme",
|
"theme",
|
||||||
"tokio",
|
|
||||||
"tracing-wasm",
|
"tracing-wasm",
|
||||||
"ui",
|
"ui",
|
||||||
|
"universal-time 0.3.1 (git+https://github.com/shadowylab/universal-time)",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
@@ -1650,9 +1654,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctor"
|
name = "ctor"
|
||||||
version = "1.0.10"
|
version = "1.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2e30e509674ef0ec91e21a7735766db37d163d46151b6a361d8b83dd79116bd"
|
checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"link-section",
|
"link-section",
|
||||||
"linktime-proc-macro",
|
"linktime-proc-macro",
|
||||||
@@ -1711,7 +1715,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#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1726,6 +1730,7 @@ dependencies = [
|
|||||||
"common",
|
"common",
|
||||||
"flume 0.11.1",
|
"flume 0.11.1",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"instant",
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"person",
|
"person",
|
||||||
@@ -1822,13 +1827,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.6"
|
version = "0.2.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
|
checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 3.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1903,7 +1908,7 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
"memchr",
|
"memchr",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"toml 1.1.3+spec-1.1.0",
|
"toml 1.1.4+spec-1.1.0",
|
||||||
"vswhom",
|
"vswhom",
|
||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
@@ -2023,11 +2028,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "event-listener"
|
name = "event-listener"
|
||||||
version = "5.4.1"
|
version = "5.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"concurrent-queue",
|
|
||||||
"parking",
|
"parking",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
@@ -2607,7 +2611,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -2689,7 +2693,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_linux"
|
name = "gpui_linux"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_unix",
|
"accesskit_unix",
|
||||||
@@ -2741,7 +2745,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_macos"
|
name = "gpui_macos"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_macos",
|
"accesskit_macos",
|
||||||
@@ -2790,7 +2794,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#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2801,7 +2805,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_platform"
|
name = "gpui_platform"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -2814,7 +2818,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_shared_string"
|
name = "gpui_shared_string"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2824,7 +2828,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#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -2835,7 +2839,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_util"
|
name = "gpui_util"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"log",
|
"log",
|
||||||
@@ -2845,7 +2849,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_web"
|
name = "gpui_web"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
@@ -2857,7 +2861,6 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"smallvec",
|
|
||||||
"uuid",
|
"uuid",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
@@ -2869,7 +2872,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_wgpu"
|
name = "gpui_wgpu"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -2887,6 +2890,7 @@ dependencies = [
|
|||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"swash",
|
"swash",
|
||||||
|
"unicode-bidi",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
@@ -2898,7 +2902,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_windows"
|
name = "gpui_windows"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_windows",
|
"accesskit_windows",
|
||||||
@@ -3154,9 +3158,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.4.2"
|
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 = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
|
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"itoa",
|
"itoa",
|
||||||
@@ -3188,7 +3192,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#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
@@ -3208,7 +3212,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#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -3493,6 +3497,18 @@ dependencies = [
|
|||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instant"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "interpolate_name"
|
name = "interpolate_name"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -3780,9 +3796,9 @@ checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "link-section"
|
name = "link-section"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8dc98458dfe90986c5e2f6ddcf68360c7e5c4252600153e06aa4ee8176c0f8d1"
|
checksum = "5ee1a0d6e252afe82e7bc2db42fba60e02ddf3b1accaf8cb21d96e34ba61f3d4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linkify"
|
name = "linkify"
|
||||||
@@ -3795,9 +3811,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linktime-proc-macro"
|
name = "linktime-proc-macro"
|
||||||
version = "0.2.0"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c7b0a3383c2a1002d11349c92c85a666a5fb679e96c79d782cf0dbe557fd6ee"
|
checksum = "348d0075b1fc163b26d72a7f75fc5141daf2fd1bdf128d873cbaf6785d495bdf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
@@ -4003,7 +4019,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen",
|
"bindgen",
|
||||||
@@ -4196,8 +4212,8 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.45.0-alpha.4"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841"
|
source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -4215,14 +4231,14 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
"universal-time",
|
"universal-time 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-blossom"
|
name = "nostr-blossom"
|
||||||
version = "0.45.0-alpha.4"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841"
|
source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4233,8 +4249,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-connect"
|
name = "nostr-connect"
|
||||||
version = "0.45.0-alpha.4"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841"
|
source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -4247,8 +4263,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.45.0-alpha.4"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841"
|
source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"btreecap",
|
"btreecap",
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
@@ -4258,8 +4274,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip"
|
name = "nostr-gossip"
|
||||||
version = "0.45.0-alpha.4"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841"
|
source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nostr",
|
"nostr",
|
||||||
"opaquerr",
|
"opaquerr",
|
||||||
@@ -4267,8 +4283,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip-memory"
|
name = "nostr-gossip-memory"
|
||||||
version = "0.45.0-alpha.4"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841"
|
source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -4279,8 +4295,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.45.0-alpha.4"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841"
|
source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"flume 0.12.0",
|
"flume 0.12.0",
|
||||||
@@ -4293,8 +4309,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-memory"
|
name = "nostr-memory"
|
||||||
version = "0.45.0-alpha.4"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841"
|
source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"btreecap",
|
"btreecap",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4304,8 +4320,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.45.0-alpha.4"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a48ab1cff0e1896537ac7bcdf16d828152f80841"
|
source = "git+https://github.com/rust-nostr/nostr#c15db19db55df326d56bbe11543583e0bd0276e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -4320,7 +4336,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"universal-time",
|
"universal-time 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4906,7 +4922,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#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -4921,6 +4937,7 @@ dependencies = [
|
|||||||
"common",
|
"common",
|
||||||
"flume 0.11.1",
|
"flume 0.11.1",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"instant",
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@@ -5717,7 +5734,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
]
|
]
|
||||||
@@ -5799,7 +5816,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#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -5969,9 +5986,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.42"
|
version = "0.23.43"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
|
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"log",
|
"log",
|
||||||
@@ -6114,7 +6131,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "scheduler"
|
name = "scheduler"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-task",
|
"async-task",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
@@ -6128,9 +6145,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "1.2.1"
|
version = "1.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
|
checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
@@ -6142,14 +6159,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars_derive"
|
name = "schemars_derive"
|
||||||
version = "1.2.1"
|
version = "1.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f"
|
checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde_derive_internals",
|
"serde_derive_internals",
|
||||||
"syn 2.0.119",
|
"syn 3.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6207,12 +6224,13 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "secp256k1"
|
name = "secp256k1"
|
||||||
version = "0.29.1"
|
version = "0.30.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
|
checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitcoin_hashes",
|
||||||
|
"rand 0.8.7",
|
||||||
"secp256k1-sys",
|
"secp256k1-sys",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6305,13 +6323,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive_internals"
|
name = "serde_derive_internals"
|
||||||
version = "0.29.1"
|
version = "0.30.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 3.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6666,6 +6684,7 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui_tokio",
|
"gpui_tokio",
|
||||||
|
"instant",
|
||||||
"log",
|
"log",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -6738,7 +6757,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#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heapless 0.9.3",
|
"heapless 0.9.3",
|
||||||
"log",
|
"log",
|
||||||
@@ -6749,34 +6768,35 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval"
|
name = "sval"
|
||||||
version = "2.20.0"
|
version = "2.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5fb9efbae90f97301f4d25f3be63dfd99d6b7af9d088228a52ec960d649b2e7d"
|
checksum = "ba3a370f3cd0422964fd9a19b6516f048527738e6ec50b1b0ff79b460b468390"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval_buffer"
|
name = "sval_buffer"
|
||||||
version = "2.20.0"
|
version = "2.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff5c0280ea0af40b3a1fd0b532680b5067482ffb81412ee66ec04b1d9952b49a"
|
checksum = "111e6e2dab25782acb41b281263f5f60d6f56a2ba0b3b076187ad23a1552544d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sval",
|
"sval",
|
||||||
"sval_ref",
|
"sval_ref",
|
||||||
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval_dynamic"
|
name = "sval_dynamic"
|
||||||
version = "2.20.0"
|
version = "2.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59b9067f2e68f58e110cf8019a268057e3791cf74b0fdb1b3c7c6e49104f44e6"
|
checksum = "590ac4ebd299740eac453162302a494e6d5b3be243feab8bf07d0d4331b6b2b3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sval",
|
"sval",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval_fmt"
|
name = "sval_fmt"
|
||||||
version = "2.20.0"
|
version = "2.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96ebdbf0e4b175884aa587fcf551c16dabe245ea04235abdd8cbbd160b27ed5a"
|
checksum = "4b964d6d917267355d7f343c515b908586e5b4aeeada328738f703452f9412d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
@@ -6785,9 +6805,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval_json"
|
name = "sval_json"
|
||||||
version = "2.20.0"
|
version = "2.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e448d9fa216a6c16670b28d624fbbcf5c04e41eb187bd7c52e01222ffa72a12"
|
checksum = "dea35e4d6b997acc7dc4786ab782f6a6c5000efe4ae93a2db89cf350775fe5fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
@@ -6796,9 +6816,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval_nested"
|
name = "sval_nested"
|
||||||
version = "2.20.0"
|
version = "2.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "021de5b5c26efd544c694cef9b8a9abe8633481bf3be1ea145a18a740818b291"
|
checksum = "a73195ebd4b3e5866db2e1b75f3f383657ad5abc600c646d69b4f064fee89154"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sval",
|
"sval",
|
||||||
"sval_buffer",
|
"sval_buffer",
|
||||||
@@ -6807,18 +6827,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval_ref"
|
name = "sval_ref"
|
||||||
version = "2.20.0"
|
version = "2.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "54ef5ffec8bc52ded04ee424ab8d959e25e64fd40a48a16d21f8991ce824c1bb"
|
checksum = "c0ef28df9d586bfd15115286651337cb5645f8c203160d22d2d1a20cf13c428c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sval",
|
"sval",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval_serde"
|
name = "sval_serde"
|
||||||
version = "2.20.0"
|
version = "2.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b983833a8a2390f89ebcf9b9acd06883017014b4ffd72ee28e0c9de6852039f5"
|
checksum = "d72fbe6537e88878f10237bde71ae4a1b65b2bf54bdf274abc566fc696334c92"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"sval",
|
"sval",
|
||||||
@@ -7186,13 +7206,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.7.1"
|
version = "2.7.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
|
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 3.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7273,9 +7293,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.1.3+spec-1.1.0"
|
version = "1.1.4+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c"
|
checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
@@ -7332,9 +7352,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.1.2+spec-1.1.0"
|
version = "1.1.3+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow 1.0.4",
|
"winnow 1.0.4",
|
||||||
]
|
]
|
||||||
@@ -7553,6 +7573,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"common",
|
"common",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"instant",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"lsp-types",
|
"lsp-types",
|
||||||
@@ -7661,9 +7682,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "universal-time"
|
name = "universal-time"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f942cacf37a673350e8f252dff4691fb8e525a5fff6e1f2c4b6c2f3b8349248d"
|
checksum = "47a939edecc3c5a7b83c02e5f6b3c31d2bc69eabcc9a87ab12c6d37ee6dbc856"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "universal-time"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "git+https://github.com/shadowylab/universal-time#394cd158556e1162806f6066fe89c413c06f9421"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
@@ -7732,7 +7758,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "util_macros"
|
name = "util_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"perf",
|
"perf",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -7942,8 +7968,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm_thread"
|
name = "wasm_thread"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/zed-industries/wasm_thread?rev=0cf96c7708dfb97ccf3da50347e25edcf75d6937#0cf96c7708dfb97ccf3da50347e25edcf75d6937"
|
||||||
checksum = "b7516db7f32decdadb1c3b8deb1b7d78b9df7606c5cc2f6241737c2ab3a0258e"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -8071,15 +8096,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webbrowser"
|
name = "webbrowser"
|
||||||
version = "1.2.1"
|
version = "1.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72"
|
checksum = "ef62a3d5f7b2411119a11b6f62570dbff91d7105e011a20fb83fbf8f5761c40f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation 0.10.1",
|
|
||||||
"jni 0.22.4",
|
"jni 0.22.4",
|
||||||
"log",
|
"log",
|
||||||
"ndk-context",
|
"ndk-context",
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
|
"objc2-app-kit 0.3.2",
|
||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
"url",
|
"url",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
@@ -8990,6 +9015,7 @@ dependencies = [
|
|||||||
"common",
|
"common",
|
||||||
"device",
|
"device",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"instant",
|
||||||
"log",
|
"log",
|
||||||
"nostr-connect",
|
"nostr-connect",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
@@ -9465,7 +9491,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "zlog"
|
name = "zlog"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -9482,7 +9508,7 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ztracing"
|
name = "ztracing"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -9493,7 +9519,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ztracing_macro"
|
name = "ztracing_macro"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#58df5a14ce946fa27b14194d9d2fce8f87b1c331"
|
source = "git+https://github.com/zed-industries/zed#f851d82e880c152d0f41e4a60d363aa82b4b8114"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zune-core"
|
name = "zune-core"
|
||||||
|
|||||||
+3
-2
@@ -24,13 +24,12 @@ nostr-memory = { git = "https://github.com/rust-nostr/nostr" }
|
|||||||
nostr-blossom = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-blossom = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
|
|
||||||
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44" ] }
|
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44" ] }
|
||||||
|
|
||||||
# Others
|
# Others
|
||||||
anyhow = "1.0.44"
|
anyhow = "1.0.44"
|
||||||
chrono = "0.4.38"
|
chrono = { version = "0.4.38", features = ["wasmbind"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
@@ -44,6 +43,8 @@ smallvec = "1.14.0"
|
|||||||
smol = "2"
|
smol = "2"
|
||||||
webbrowser = "1.0.4"
|
webbrowser = "1.0.4"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] }
|
tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] }
|
||||||
|
errno = { version = "0.3.14", default-features = false }
|
||||||
|
instant = "0.1"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Use stacker's psm version which may have better WASM support
|
# Use stacker's psm version which may have better WASM support
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ publish.workspace = true
|
|||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
instant.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||||
use gpui::http_client::{AsyncBody, HttpClient};
|
use gpui::http_client::{AsyncBody, HttpClient};
|
||||||
@@ -11,6 +10,7 @@ use gpui::{
|
|||||||
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task,
|
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task,
|
||||||
Window,
|
Window,
|
||||||
};
|
};
|
||||||
|
use instant::Duration;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ settings = { path = "../settings" }
|
|||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
instant.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
@@ -20,6 +21,7 @@ smallvec.workspace = true
|
|||||||
log.workspace = true
|
log.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
|
|
||||||
|
futures.workspace = true
|
||||||
fuzzy-matcher = "0.3.7"
|
fuzzy-matcher = "0.3.7"
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
|||||||
+133
-202
@@ -1,13 +1,9 @@
|
|||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
|
||||||
use std::sync::Arc;
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
use std::sync::RwLock;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::Duration;
|
use std::sync::{Arc, LazyLock, RwLock};
|
||||||
|
|
||||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
use common::EventExt;
|
use common::EventExt;
|
||||||
use fuzzy_matcher::FuzzyMatcher;
|
use fuzzy_matcher::FuzzyMatcher;
|
||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
@@ -15,10 +11,9 @@ use gpui::{
|
|||||||
App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task,
|
App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task,
|
||||||
WeakEntity, Window,
|
WeakEntity, Window,
|
||||||
};
|
};
|
||||||
|
use instant::Duration;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use smol::lock::RwLock;
|
|
||||||
use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP, UniversalSigner};
|
use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP, UniversalSigner};
|
||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
@@ -27,6 +22,9 @@ mod room;
|
|||||||
pub use message::*;
|
pub use message::*;
|
||||||
pub use room::*;
|
pub use room::*;
|
||||||
|
|
||||||
|
/// A static keypair used only for signing locally-cached rumor events.
|
||||||
|
static LOCAL_KEYS: LazyLock<Keys> = LazyLock::new(Keys::generate);
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) {
|
pub fn init(window: &mut Window, cx: &mut App) {
|
||||||
ChatRegistry::set_global(cx.new(|cx| ChatRegistry::new(window, cx)), cx);
|
ChatRegistry::set_global(cx.new(|cx| ChatRegistry::new(window, cx)), cx);
|
||||||
}
|
}
|
||||||
@@ -83,15 +81,15 @@ pub struct ChatRegistry {
|
|||||||
rooms: Vec<Entity<Room>>,
|
rooms: Vec<Entity<Room>>,
|
||||||
|
|
||||||
/// Events that failed to unwrap for any reason
|
/// Events that failed to unwrap for any reason
|
||||||
trashes: Entity<BTreeSet<FailedMessage>>,
|
trash: Entity<BTreeSet<FailedMessage>>,
|
||||||
|
|
||||||
/// Tracking events seen on which relays in the current session
|
/// Tracking events seen on which relays in the current session
|
||||||
seens: Arc<RwLock<HashMap<EventId, HashSet<RelayUrl>>>>,
|
seen: Arc<RwLock<HashMap<EventId, HashSet<RelayUrl>>>>,
|
||||||
|
|
||||||
/// Mapping of unwrapped event ids to their gift wrap event ids
|
/// Mapping of unwrapped event ids to their gift wrap event ids
|
||||||
event_map: Arc<RwLock<HashMap<EventId, EventId>>>,
|
event_map: Arc<RwLock<HashMap<EventId, EventId>>>,
|
||||||
|
|
||||||
/// Tracking the status of unwrapping gift wrap events.
|
/// True while the initial event backlog is still loading
|
||||||
tracking: Arc<AtomicBool>,
|
tracking: Arc<AtomicBool>,
|
||||||
|
|
||||||
/// Channel for sending signals to the UI.
|
/// Channel for sending signals to the UI.
|
||||||
@@ -103,10 +101,37 @@ pub struct ChatRegistry {
|
|||||||
/// Async tasks
|
/// Async tasks
|
||||||
tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
|
tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
|
||||||
|
|
||||||
|
/// Notification listener task (cancelled on signer change)
|
||||||
|
notification_listener: Option<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
|
/// Signal consumer task (cancelled on signer change)
|
||||||
|
signal_consumer: Option<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
|
/// Fuzzy matcher for room search (cached; intentionally excluded from Debug)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
matcher: CachedMatcher,
|
||||||
|
|
||||||
/// Subscriptions
|
/// Subscriptions
|
||||||
_subscriptions: SmallVec<[Subscription; 2]>,
|
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrapper to provide Debug for SkimMatcherV2
|
||||||
|
struct CachedMatcher(SkimMatcherV2);
|
||||||
|
|
||||||
|
impl std::fmt::Debug for CachedMatcher {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("CachedMatcher { .. }")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for CachedMatcher {
|
||||||
|
type Target = SkimMatcherV2;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl EventEmitter<ChatEvent> for ChatRegistry {}
|
impl EventEmitter<ChatEvent> for ChatRegistry {}
|
||||||
|
|
||||||
impl ChatRegistry {
|
impl ChatRegistry {
|
||||||
@@ -140,35 +165,39 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
// Run at the end of the current cycle
|
// Run at the end of the current cycle
|
||||||
cx.defer_in(window, |this, _window, cx| {
|
cx.defer_in(window, |this, _window, cx| {
|
||||||
this.tracking(cx);
|
|
||||||
this.get_rooms(cx);
|
this.get_rooms(cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
rooms: vec![],
|
rooms: vec![],
|
||||||
trashes: cx.new(|_| BTreeSet::default()),
|
trash: cx.new(|_| BTreeSet::default()),
|
||||||
seens: Arc::new(RwLock::new(HashMap::default())),
|
seen: Arc::new(RwLock::new(HashMap::default())),
|
||||||
event_map: Arc::new(RwLock::new(HashMap::default())),
|
event_map: Arc::new(RwLock::new(HashMap::default())),
|
||||||
tracking: Arc::new(AtomicBool::new(false)),
|
tracking: Arc::new(AtomicBool::new(true)),
|
||||||
|
matcher: CachedMatcher(SkimMatcherV2::default()),
|
||||||
signal_rx: rx,
|
signal_rx: rx,
|
||||||
signal_tx: tx,
|
signal_tx: tx,
|
||||||
tasks: smallvec![],
|
tasks: smallvec![],
|
||||||
|
notification_listener: None,
|
||||||
|
signal_consumer: None,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle nostr notifications
|
/// Handle nostr notifications
|
||||||
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
|
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
|
||||||
|
// Cancel previous notification tasks before spawning new ones
|
||||||
|
self.notification_listener = None;
|
||||||
|
self.signal_consumer = None;
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
let tracking = self.tracking.clone();
|
let seen = self.seen.clone();
|
||||||
let seens = self.seens.clone();
|
|
||||||
let event_map = self.event_map.clone();
|
let event_map = self.event_map.clone();
|
||||||
let trashes = self.trashes.downgrade();
|
let trash = self.trash.downgrade();
|
||||||
|
|
||||||
let initialized_at = Timestamp::now();
|
|
||||||
let sub_id1 = SubscriptionId::new(DEVICE_GIFTWRAP);
|
let sub_id1 = SubscriptionId::new(DEVICE_GIFTWRAP);
|
||||||
let sub_id2 = SubscriptionId::new(USER_GIFTWRAP);
|
let sub_id2 = SubscriptionId::new(USER_GIFTWRAP);
|
||||||
|
|
||||||
@@ -176,19 +205,22 @@ impl ChatRegistry {
|
|||||||
let tx = self.signal_tx.clone();
|
let tx = self.signal_tx.clone();
|
||||||
let rx = self.signal_rx.clone();
|
let rx = self.signal_rx.clone();
|
||||||
|
|
||||||
self.tasks.push(cx.background_spawn(async move {
|
self.notification_listener = Some(cx.background_spawn(async move {
|
||||||
let mut notifications = client.notifications();
|
let mut notifications = client.notifications();
|
||||||
let mut processed_events = HashSet::new();
|
let mut processed_events = HashSet::new();
|
||||||
|
const MAX_PROCESSED: usize = 10_000;
|
||||||
|
|
||||||
while let Some(notification) = notifications.next().await {
|
while let Some(notification) = notifications.next().await {
|
||||||
let ClientNotification::Message { message, relay_url } = notification else {
|
let ClientNotification::Message { message, relay_url } = notification else {
|
||||||
// Skip non-message notifications
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
match *message {
|
match *message {
|
||||||
RelayMessage::Event { event, .. } => {
|
RelayMessage::Event { event, .. } => {
|
||||||
// De-duplicate events by their ID
|
// Prune the dedup set before it grows unbounded
|
||||||
|
if processed_events.len() >= MAX_PROCESSED {
|
||||||
|
processed_events.clear();
|
||||||
|
}
|
||||||
if !processed_events.insert(event.id) {
|
if !processed_events.insert(event.id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -196,7 +228,6 @@ impl ChatRegistry {
|
|||||||
// Handle msg relays event to determine when the app is ready to subscribe
|
// Handle msg relays event to determine when the app is ready to subscribe
|
||||||
if event.kind == Kind::InboxRelays {
|
if event.kind == Kind::InboxRelays {
|
||||||
let current_user = signer.get_public_key_async().await?;
|
let current_user = signer.get_public_key_async().await?;
|
||||||
// Emit the inbox ready signal
|
|
||||||
if event.pubkey == current_user {
|
if event.pubkey == current_user {
|
||||||
tx.send_async(Signal::InboxReady).await?;
|
tx.send_async(Signal::InboxReady).await?;
|
||||||
}
|
}
|
||||||
@@ -209,33 +240,30 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
// Keep track of which relays have seen this event
|
// Keep track of which relays have seen this event
|
||||||
{
|
{
|
||||||
let mut seens = seens.write().await;
|
let mut seen = seen.write().unwrap();
|
||||||
seens.entry(event.id).or_default().insert(relay_url);
|
seen.entry(event.id).or_default().insert(relay_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the rumor from the gift wrap event
|
// Extract the rumor from the gift wrap event
|
||||||
match extract_rumor(&client, &signer, event.as_ref()).await {
|
match extract_rumor(&client, &signer, event.as_ref()).await {
|
||||||
Ok(rumor) => {
|
Ok(rumor) => {
|
||||||
// Map the rumor id to the gift wrap event id for later lookup
|
let Some(rumor_id) = rumor.id else {
|
||||||
|
log::error!("Rumor missing id after ensure_id");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
{
|
{
|
||||||
let mut event_map = event_map.write().await;
|
let mut event_map = event_map.write().unwrap();
|
||||||
event_map.insert(rumor.id.unwrap(), event.id);
|
event_map.insert(rumor_id, event.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the rumor has a recipient
|
|
||||||
if rumor.tags.is_empty() {
|
if rumor.tags.is_empty() {
|
||||||
let signal = Signal::error(&event, "Recipient is missing");
|
let signal = Signal::error(&event, "Recipient is missing");
|
||||||
tx.send_async(signal).await?;
|
tx.send_async(signal).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the rumor was created after the chat was initialized (for detecting new messages)
|
// Emit message for both new and backlog events
|
||||||
if rumor.created_at >= initialized_at {
|
|
||||||
let signal = Signal::message(event.id, rumor);
|
let signal = Signal::message(event.id, rumor);
|
||||||
tx.send_async(signal).await?;
|
tx.send_async(signal).await?;
|
||||||
} else {
|
|
||||||
// Mark the chat still processing new messages
|
|
||||||
tracking.store(true, Ordering::Release);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let reason = format!("Failed to extract rumor: {e}");
|
let reason = format!("Failed to extract rumor: {e}");
|
||||||
@@ -256,7 +284,7 @@ impl ChatRegistry {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.signal_consumer = Some(cx.spawn(async move |this, cx| {
|
||||||
while let Ok(message) = rx.recv_async().await {
|
while let Ok(message) = rx.recv_async().await {
|
||||||
match message {
|
match message {
|
||||||
Signal::Message(message) => {
|
Signal::Message(message) => {
|
||||||
@@ -270,13 +298,17 @@ impl ChatRegistry {
|
|||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
Signal::Eose => {
|
Signal::Eose => {
|
||||||
|
this.update(cx, |this, _cx| {
|
||||||
|
this.tracking.store(false, Ordering::Release);
|
||||||
|
})?;
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.get_rooms(cx);
|
this.get_rooms(cx);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
Signal::Error(trash) => {
|
Signal::Error(failed) => {
|
||||||
trashes.update(cx, |this, cx| {
|
trash.update(cx, |this, cx| {
|
||||||
this.insert(trash);
|
this.insert(failed);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
@@ -287,25 +319,6 @@ impl ChatRegistry {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tracking the status of unwrapping gift wrap events.
|
|
||||||
fn tracking(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let status = self.tracking.clone();
|
|
||||||
let tx = self.signal_tx.clone();
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |_, cx| {
|
|
||||||
let loop_duration = Duration::from_secs(15);
|
|
||||||
loop {
|
|
||||||
if status.load(Ordering::Acquire) {
|
|
||||||
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
|
|
||||||
_ = tx.send_async(Signal::Eose).await;
|
|
||||||
} else {
|
|
||||||
_ = tx.send_async(Signal::Eose).await;
|
|
||||||
}
|
|
||||||
cx.background_executor().timer(loop_duration).await;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all necessary metadata from relays for current user
|
/// Get all necessary metadata from relays for current user
|
||||||
pub fn get_metadata(&mut self, cx: &mut Context<Self>) {
|
pub fn get_metadata(&mut self, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
@@ -315,55 +328,43 @@ impl ChatRegistry {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.tasks.push(cx.background_spawn(async move {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
// Subscribe to metadata from relays
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
// Construct filter for msg relays
|
|
||||||
let msg_relays = Filter::new()
|
let msg_relays = Filter::new()
|
||||||
.kind(Kind::InboxRelays)
|
.kind(Kind::InboxRelays)
|
||||||
.author(public_key)
|
.author(public_key)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
// Construct filter for contact list
|
|
||||||
let contact_list = Filter::new()
|
let contact_list = Filter::new()
|
||||||
.kind(Kind::ContactList)
|
.kind(Kind::ContactList)
|
||||||
.author(public_key)
|
.author(public_key)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
// Subscribe
|
_ = client
|
||||||
client
|
|
||||||
.subscribe(vec![msg_relays, contact_list])
|
.subscribe(vec![msg_relays, contact_list])
|
||||||
.close_on(opts)
|
.close_on(opts)
|
||||||
.await?;
|
.await;
|
||||||
|
|
||||||
Ok(())
|
// Give relays time to respond
|
||||||
}));
|
|
||||||
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
|
|
||||||
// Spawn a task to verify user inbox relays after 5 seconds
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
cx.background_executor().timer(Duration::from_secs(5)).await;
|
cx.background_executor().timer(Duration::from_secs(5)).await;
|
||||||
|
|
||||||
if !cx
|
// Verify inbox relays were received
|
||||||
.background_spawn(async move {
|
|
||||||
// Construct inbox relays filter
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::InboxRelays)
|
.kind(Kind::InboxRelays)
|
||||||
.author(public_key)
|
.author(public_key)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
// Check the latest inbox relays event in database
|
let found = client
|
||||||
client
|
|
||||||
.database()
|
.database()
|
||||||
.query(filter)
|
.query(filter)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.first_owned()
|
.first_owned()
|
||||||
.is_some()
|
.is_some();
|
||||||
})
|
|
||||||
.await
|
if !found {
|
||||||
{
|
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(ChatEvent::InboxRelayNotFound);
|
cx.emit(ChatEvent::InboxRelayNotFound);
|
||||||
})?;
|
})?;
|
||||||
@@ -379,16 +380,15 @@ impl ChatRegistry {
|
|||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
let public_key = signer.get_public_key_async().await?;
|
let public_key = signer.get_public_key_async().await?;
|
||||||
|
|
||||||
// Construct inbox relays filter
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::InboxRelays)
|
.kind(Kind::InboxRelays)
|
||||||
.author(public_key)
|
.author(public_key)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
// Get the latest inbox relays event in database
|
|
||||||
let event = client
|
let event = client
|
||||||
.database()
|
.database()
|
||||||
.query(filter)
|
.query(filter)
|
||||||
@@ -396,19 +396,14 @@ impl ChatRegistry {
|
|||||||
.first_owned()
|
.first_owned()
|
||||||
.ok_or(anyhow::anyhow!("No inbox relays found"))?;
|
.ok_or(anyhow::anyhow!("No inbox relays found"))?;
|
||||||
|
|
||||||
// Extract relay list from event
|
|
||||||
let relays: Vec<RelayUrl> = nip17::extract_relay_list(&event).collect();
|
let relays: Vec<RelayUrl> = nip17::extract_relay_list(&event).collect();
|
||||||
|
|
||||||
// Ensure relay connections
|
|
||||||
for url in relays.iter() {
|
for url in relays.iter() {
|
||||||
client.add_relay(url).and_connect().await?;
|
client.add_relay(url).and_connect().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct gift wrap event filter
|
|
||||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||||
let id = SubscriptionId::new(format!("{}-msg", public_key.to_hex()));
|
let id = SubscriptionId::new(USER_GIFTWRAP);
|
||||||
|
|
||||||
// Construct target for subscription
|
|
||||||
let target: HashMap<RelayUrl, Filter> = relays
|
let target: HashMap<RelayUrl, Filter> = relays
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|relay| (relay, filter.clone()))
|
.map(|relay| (relay, filter.clone()))
|
||||||
@@ -419,18 +414,18 @@ impl ChatRegistry {
|
|||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
if let Err(e) = task.await {
|
if let Err(e) = task.await {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(ChatEvent::Error(e.to_string()));
|
cx.emit(ChatEvent::Error(e.to_string()));
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh the chat registry, fetching messages and contact list from relays.
|
/// Reload the chat registry, fetching messages and contact list from relays.
|
||||||
pub fn refresh(&mut self, cx: &mut Context<Self>) {
|
pub fn reload(&mut self, cx: &mut Context<Self>) {
|
||||||
self.reset(cx);
|
self.reset(cx);
|
||||||
self.get_metadata(cx);
|
self.get_metadata(cx);
|
||||||
self.get_rooms(cx);
|
self.get_rooms(cx);
|
||||||
@@ -468,35 +463,38 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
/// Count the number of messages seen by a given relay.
|
/// Count the number of messages seen by a given relay.
|
||||||
pub fn count_messages(&self, relay_url: &RelayUrl) -> usize {
|
pub fn count_messages(&self, relay_url: &RelayUrl) -> usize {
|
||||||
self.seens
|
self.seen
|
||||||
.read_blocking()
|
.read()
|
||||||
|
.unwrap()
|
||||||
.values()
|
.values()
|
||||||
.filter(|seen| seen.contains(relay_url))
|
.filter(|s| s.contains(relay_url))
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Count the number of trash messages.
|
/// Count the number of trash messages.
|
||||||
pub fn count_trash_messages(&self, cx: &App) -> usize {
|
pub fn count_trash_messages(&self, cx: &App) -> usize {
|
||||||
self.trashes.read(cx).len()
|
self.trash.read(cx).len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the trash messages entity.
|
/// Get the trash messages entity.
|
||||||
pub fn trashes(&self) -> Entity<BTreeSet<FailedMessage>> {
|
pub fn trash(&self) -> Entity<BTreeSet<FailedMessage>> {
|
||||||
self.trashes.clone()
|
self.trash.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the relays that have seen a given rumor id.
|
/// Get the relays that have seen a given rumor id.
|
||||||
pub fn rumor_seen_on(&self, id: &EventId) -> Option<HashSet<RelayUrl>> {
|
pub fn rumor_seen_on(&self, id: &EventId) -> Option<HashSet<RelayUrl>> {
|
||||||
self.event_map
|
self.event_map
|
||||||
.read_blocking()
|
.read()
|
||||||
|
.unwrap()
|
||||||
.get(id)
|
.get(id)
|
||||||
.map(|id| self.seen_on(id))
|
.map(|id| self.seen_on(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the relays that have seen a given gift wrap id.
|
/// Get the relays that have seen a given gift wrap id.
|
||||||
pub fn seen_on(&self, id: &EventId) -> HashSet<RelayUrl> {
|
pub fn seen_on(&self, id: &EventId) -> HashSet<RelayUrl> {
|
||||||
self.seens
|
self.seen
|
||||||
.read_blocking()
|
.read()
|
||||||
|
.unwrap()
|
||||||
.get(id)
|
.get(id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -505,31 +503,24 @@ impl ChatRegistry {
|
|||||||
/// Add a new room to the start of list.
|
/// Add a new room to the start of list.
|
||||||
pub fn add_room<I>(&mut self, room: I, cx: &mut Context<Self>)
|
pub fn add_room<I>(&mut self, room: I, cx: &mut Context<Self>)
|
||||||
where
|
where
|
||||||
I: Into<Room> + 'static,
|
I: Into<Room>,
|
||||||
{
|
{
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
let Some(public_key) = nostr.read(cx).current_user() else {
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
let room: Room = room.into().organize(&public_key);
|
let room: Room = room.into().organize(&public_key);
|
||||||
|
self.rooms.insert(0, cx.new(|_| room));
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.rooms.insert(0, cx.new(|_| room));
|
|
||||||
cx.emit(ChatEvent::Ping);
|
cx.emit(ChatEvent::Ping);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emit an open room event.
|
/// Emit an open room event.
|
||||||
///
|
///
|
||||||
/// If the room is new, add it to the registry.
|
/// If the room is new, add it to the registry.
|
||||||
pub fn emit_room(&mut self, room: &Entity<Room>, cx: &mut Context<Self>) {
|
pub fn emit_room(&mut self, room: &Entity<Room>, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
// Get the room's ID.
|
// Get the room's ID.
|
||||||
let id = room.read(cx).id;
|
let id = room.read(cx).id;
|
||||||
|
|
||||||
@@ -538,14 +529,18 @@ impl ChatRegistry {
|
|||||||
self.rooms.insert(0, room.to_owned());
|
self.rooms.insert(0, room.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit the open room event.
|
// Emit the open room event deferred to avoid re-entrant reads
|
||||||
|
cx.defer_in(window, move |_this, _window, cx| {
|
||||||
cx.emit(ChatEvent::OpenRoom(id));
|
cx.emit(ChatEvent::OpenRoom(id));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close a room.
|
/// Close a room.
|
||||||
pub fn close_room(&mut self, id: u64, cx: &mut Context<Self>) {
|
pub fn close_room(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if self.rooms.iter().any(|r| r.read(cx).id == id) {
|
if self.rooms.iter().any(|r| r.read(cx).id == id) {
|
||||||
|
cx.defer_in(window, move |_this, _window, cx| {
|
||||||
cx.emit(ChatEvent::CloseRoom(id));
|
cx.emit(ChatEvent::CloseRoom(id));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,8 +552,6 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
/// Finding rooms based on a query.
|
/// Finding rooms based on a query.
|
||||||
pub fn find(&self, query: &str, cx: &App) -> Vec<Entity<Room>> {
|
pub fn find(&self, query: &str, cx: &App) -> Vec<Entity<Room>> {
|
||||||
let matcher = SkimMatcherV2::default();
|
|
||||||
|
|
||||||
if let Ok(public_key) = PublicKey::parse(query) {
|
if let Ok(public_key) = PublicKey::parse(query) {
|
||||||
self.rooms
|
self.rooms
|
||||||
.iter()
|
.iter()
|
||||||
@@ -569,7 +562,7 @@ impl ChatRegistry {
|
|||||||
self.rooms
|
self.rooms
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|room| {
|
.filter(|room| {
|
||||||
matcher
|
self.matcher
|
||||||
.fuzzy_match(room.read(cx).display_name(cx).as_ref(), query)
|
.fuzzy_match(room.read(cx).display_name(cx).as_ref(), query)
|
||||||
.is_some()
|
.is_some()
|
||||||
})
|
})
|
||||||
@@ -581,7 +574,7 @@ impl ChatRegistry {
|
|||||||
/// Reset the registry.
|
/// Reset the registry.
|
||||||
pub fn reset(&mut self, cx: &mut Context<Self>) {
|
pub fn reset(&mut self, cx: &mut Context<Self>) {
|
||||||
self.rooms.clear();
|
self.rooms.clear();
|
||||||
self.trashes.update(cx, |this, cx| {
|
self.trash.update(cx, |this, cx| {
|
||||||
this.clear();
|
this.clear();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
@@ -635,7 +628,9 @@ impl ChatRegistry {
|
|||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to load rooms: {}", e);
|
this.update(cx, |_, cx| {
|
||||||
|
cx.emit(ChatEvent::Error(e.to_string()));
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -653,37 +648,21 @@ impl ChatRegistry {
|
|||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
// Get contacts
|
|
||||||
let contacts = client
|
let contacts = client
|
||||||
.database()
|
.database()
|
||||||
.contacts_public_keys(public_key)
|
.contacts_public_keys(public_key)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
// Construct authored filter
|
// Query all cached rumor events (works with both old and new cache formats)
|
||||||
let authored_filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::ApplicationSpecificData)
|
.kind(Kind::ApplicationSpecificData)
|
||||||
.custom_tag(SingleLetterTag::lowercase(Alphabet::A), public_key);
|
.custom_tag(SingleLetterTag::lowercase(Alphabet::K), "14");
|
||||||
|
let events = client.database().query(filter).await?;
|
||||||
|
|
||||||
// Get all authored events
|
|
||||||
let authored = client.database().query(authored_filter).await?;
|
|
||||||
|
|
||||||
// Construct addressed filter
|
|
||||||
let addressed_filter = Filter::new()
|
|
||||||
.kind(Kind::ApplicationSpecificData)
|
|
||||||
.custom_tag(SingleLetterTag::lowercase(Alphabet::P), public_key);
|
|
||||||
|
|
||||||
// Get all addressed events
|
|
||||||
let addressed = client.database().query(addressed_filter).await?;
|
|
||||||
|
|
||||||
// Merge authored and addressed events
|
|
||||||
let events = authored.merge(addressed);
|
|
||||||
|
|
||||||
// Collect results
|
|
||||||
let mut rooms: HashSet<Room> = HashSet::new();
|
let mut rooms: HashSet<Room> = HashSet::new();
|
||||||
let mut grouped: HashMap<u64, Vec<UnsignedEvent>> = HashMap::new();
|
let mut grouped: HashMap<u64, Vec<UnsignedEvent>> = HashMap::new();
|
||||||
|
|
||||||
// Process each event and group by room hash
|
|
||||||
for raw in events.into_iter() {
|
for raw in events.into_iter() {
|
||||||
if let Ok(rumor) = UnsignedEvent::from_json(&raw.content)
|
if let Ok(rumor) = UnsignedEvent::from_json(&raw.content)
|
||||||
&& rumor.tags.public_keys().peekable().peek().is_some()
|
&& rumor.tags.public_keys().peekable().peek().is_some()
|
||||||
@@ -775,9 +754,14 @@ async fn extract_rumor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to unwrap with the available signer
|
// Try to unwrap with the available signer
|
||||||
let unwrapped = try_unwrap(signer, gift_wrap).await?;
|
let unwrapped = try_unwrap_with(signer, gift_wrap).await?;
|
||||||
let mut rumor = unwrapped.rumor;
|
let mut rumor = unwrapped.rumor;
|
||||||
|
|
||||||
|
// Verify rumor author matches the seal sender (as per mobile implementation)
|
||||||
|
if rumor.pubkey != unwrapped.sender {
|
||||||
|
return Err(anyhow!("Rumor author does not match seal sender"));
|
||||||
|
}
|
||||||
|
|
||||||
// Generate event id for the rumor if it doesn't have one
|
// Generate event id for the rumor if it doesn't have one
|
||||||
rumor.ensure_id();
|
rumor.ensure_id();
|
||||||
|
|
||||||
@@ -789,25 +773,6 @@ async fn extract_rumor(
|
|||||||
Ok(rumor)
|
Ok(rumor)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper method to try unwrapping with different signers
|
|
||||||
async fn try_unwrap(signer: &UniversalSigner, gift_wrap: &Event) -> Result<UnwrappedGift, Error> {
|
|
||||||
/*
|
|
||||||
* // Try with the device signer first
|
|
||||||
if let Some(signer) = signer.get_encryption_signer().await {
|
|
||||||
log::info!("trying with encryption key");
|
|
||||||
if let Ok(unwrapped) = try_unwrap_with(gift_wrap, &signer).await {
|
|
||||||
return Ok(unwrapped);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to the user's signer
|
|
||||||
let user_signer = signer.get().await;
|
|
||||||
*/
|
|
||||||
let unwrapped = try_unwrap_with(signer, gift_wrap).await?;
|
|
||||||
|
|
||||||
Ok(unwrapped)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to unwrap a gift wrap event with a given signer.
|
/// Attempts to unwrap a gift wrap event with a given signer.
|
||||||
async fn try_unwrap_with(
|
async fn try_unwrap_with(
|
||||||
signer: &UniversalSigner,
|
signer: &UniversalSigner,
|
||||||
@@ -820,7 +785,7 @@ async fn try_unwrap_with(
|
|||||||
|
|
||||||
// Verify the sealed event
|
// Verify the sealed event
|
||||||
let seal: Event = Event::from_json(seal)?;
|
let seal: Event = Event::from_json(seal)?;
|
||||||
seal.verify_with_ctx(&SECP256K1)?;
|
seal.verify()?;
|
||||||
|
|
||||||
// Get the rumor event
|
// Get the rumor event
|
||||||
let rumor = signer
|
let rumor = signer
|
||||||
@@ -837,39 +802,20 @@ async fn try_unwrap_with(
|
|||||||
|
|
||||||
/// 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_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
|
async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
|
||||||
let rumor_id = rumor.id.context("Rumor is missing an event id")?;
|
let room_id = rumor.uniq_id().to_string();
|
||||||
let author = rumor.pubkey;
|
|
||||||
let conversation = conversation_id(rumor);
|
|
||||||
|
|
||||||
let mut tags = rumor.tags.clone().to_vec();
|
let tags = vec![
|
||||||
|
Tag::identifier(id),
|
||||||
|
Tag::public_key(rumor.pubkey),
|
||||||
|
Tag::custom("r", [room_id]),
|
||||||
|
Tag::custom("k", ["14"]),
|
||||||
|
];
|
||||||
|
|
||||||
// Add a unique identifier
|
let event = EventBuilder::new(Kind::ApplicationSpecificData, rumor.as_json())
|
||||||
tags.push(Tag::identifier(id));
|
|
||||||
|
|
||||||
// Add a reference to the rumor's author
|
|
||||||
tags.push(Tag::custom("a", [author]));
|
|
||||||
|
|
||||||
// Add a conversation id
|
|
||||||
tags.push(Tag::custom("c", [conversation.to_string()]));
|
|
||||||
|
|
||||||
// Add a reference to the rumor's id
|
|
||||||
tags.push(Tag::event(rumor_id));
|
|
||||||
|
|
||||||
// Add references to the rumor's participants
|
|
||||||
for receiver in rumor.tags.public_keys() {
|
|
||||||
tags.push(Tag::custom("P", [receiver]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert rumor to json
|
|
||||||
let content = rumor.as_json();
|
|
||||||
|
|
||||||
// Construct the event
|
|
||||||
let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
|
|
||||||
.tags(tags)
|
.tags(tags)
|
||||||
.finalize_async(&Keys::generate())
|
.finalize_async(&*LOCAL_KEYS)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Save the event to the database
|
|
||||||
client.database().save_event(&event).await?;
|
client.database().save_event(&event).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -877,10 +823,7 @@ async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Resul
|
|||||||
|
|
||||||
/// Retrieves a previously unwrapped event from local database
|
/// Retrieves a previously unwrapped event from local database
|
||||||
async fn get_rumor(client: &Client, gift_wrap: EventId) -> Result<UnsignedEvent, Error> {
|
async fn get_rumor(client: &Client, gift_wrap: EventId) -> Result<UnsignedEvent, Error> {
|
||||||
let filter = Filter::new()
|
let filter = Filter::new().identifier(gift_wrap).limit(1);
|
||||||
.kind(Kind::ApplicationSpecificData)
|
|
||||||
.identifier(gift_wrap)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||||
UnsignedEvent::from_json(event.content).map_err(|e| anyhow!(e))
|
UnsignedEvent::from_json(event.content).map_err(|e| anyhow!(e))
|
||||||
@@ -888,15 +831,3 @@ async fn get_rumor(client: &Client, gift_wrap: EventId) -> Result<UnsignedEvent,
|
|||||||
Err(anyhow!("Event is not cached yet."))
|
Err(anyhow!("Event is not cached yet."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the conversation ID for a given rumor (message).
|
|
||||||
fn conversation_id(rumor: &UnsignedEvent) -> u64 {
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
let mut pubkeys: Vec<PublicKey> = rumor.tags.public_keys().collect();
|
|
||||||
pubkeys.push(rumor.pubkey);
|
|
||||||
pubkeys.sort();
|
|
||||||
pubkeys.dedup();
|
|
||||||
pubkeys.hash(&mut hasher);
|
|
||||||
|
|
||||||
hasher.finish()
|
|
||||||
}
|
|
||||||
|
|||||||
+54
-54
@@ -1,6 +1,6 @@
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
use common::EventExt;
|
use common::EventExt;
|
||||||
@@ -363,7 +363,11 @@ impl Room {
|
|||||||
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
||||||
.timeout(Some(Duration::from_secs(TIMEOUT)));
|
.timeout(Some(Duration::from_secs(TIMEOUT)));
|
||||||
|
|
||||||
for public_key in members.into_iter() {
|
let tasks: Vec<_> = members
|
||||||
|
.into_iter()
|
||||||
|
.map(|public_key| {
|
||||||
|
let client = client.clone();
|
||||||
|
async move {
|
||||||
let inbox = Filter::new()
|
let inbox = Filter::new()
|
||||||
.author(public_key)
|
.author(public_key)
|
||||||
.kind(Kind::InboxRelays)
|
.kind(Kind::InboxRelays)
|
||||||
@@ -374,11 +378,16 @@ impl Room {
|
|||||||
.kind(Kind::Custom(10044))
|
.kind(Kind::Custom(10044))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
// Subscribe to the target
|
|
||||||
client
|
client
|
||||||
.subscribe(vec![inbox, announcement])
|
.subscribe(vec![inbox, announcement])
|
||||||
.close_on(opts)
|
.close_on(opts)
|
||||||
.await?;
|
.await
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for result in futures::future::join_all(tasks).await {
|
||||||
|
result?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -389,12 +398,12 @@ impl Room {
|
|||||||
pub fn get_messages(&self, cx: &App) -> Task<Result<Vec<UnsignedEvent>, Error>> {
|
pub fn get_messages(&self, cx: &App) -> Task<Result<Vec<UnsignedEvent>, Error>> {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let conversation_id = self.id.to_string();
|
let room_id = self.id.to_string();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::ApplicationSpecificData)
|
.kind(Kind::ApplicationSpecificData)
|
||||||
.custom_tag(SingleLetterTag::lowercase(Alphabet::C), conversation_id);
|
.custom_tag(SingleLetterTag::lowercase(Alphabet::R), room_id);
|
||||||
|
|
||||||
let messages = client
|
let messages = client
|
||||||
.database()
|
.database()
|
||||||
@@ -402,10 +411,6 @@ impl Room {
|
|||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|event| UnsignedEvent::from_json(&event.content).ok())
|
.filter_map(|event| UnsignedEvent::from_json(&event.content).ok())
|
||||||
.filter(|event| {
|
|
||||||
// Only process private direct messages and file messages
|
|
||||||
event.kind == Kind::PrivateDirectMessage || event.kind == Kind::Custom(15)
|
|
||||||
})
|
|
||||||
.sorted_by_key(|message| message.created_at)
|
.sorted_by_key(|message| message.created_at)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -429,14 +434,6 @@ impl Room {
|
|||||||
// Get current user's public key
|
// Get current user's public key
|
||||||
let sender = nostr.read(cx).current_user()?;
|
let sender = nostr.read(cx).current_user()?;
|
||||||
|
|
||||||
// Get all members, excluding the sender
|
|
||||||
let members: Vec<Person> = self
|
|
||||||
.members
|
|
||||||
.iter()
|
|
||||||
.filter(|public_key| public_key != &&sender)
|
|
||||||
.map(|member| persons.read(cx).get(member, cx))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Construct event's tags
|
// Construct event's tags
|
||||||
let mut tags = vec![];
|
let mut tags = vec![];
|
||||||
|
|
||||||
@@ -450,8 +447,9 @@ impl Room {
|
|||||||
tags.push(Tag::event(id))
|
tags.push(Tag::event(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all receiver tags
|
// Add all receiver tags (no intermediate allocation)
|
||||||
for member in members.into_iter() {
|
for public_key in self.members.iter().filter(|pk| *pk != &sender) {
|
||||||
|
let member = persons.read(cx).get(public_key, cx);
|
||||||
tags.push(
|
tags.push(
|
||||||
Nip01Tag::PublicKey {
|
Nip01Tag::PublicKey {
|
||||||
public_key: member.public_key(),
|
public_key: member.public_key(),
|
||||||
@@ -473,6 +471,30 @@ impl Room {
|
|||||||
Some(event)
|
Some(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Select the appropriate signer based on signer kind and available keys.
|
||||||
|
fn select_signer(
|
||||||
|
signer_kind: &SignerKind,
|
||||||
|
has_announcement: bool,
|
||||||
|
encryption_signer: &Option<UniversalSigner>,
|
||||||
|
user_signer: &UniversalSigner,
|
||||||
|
) -> UniversalSigner {
|
||||||
|
match signer_kind {
|
||||||
|
SignerKind::Auto => {
|
||||||
|
if has_announcement {
|
||||||
|
encryption_signer
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| user_signer.clone())
|
||||||
|
} else {
|
||||||
|
user_signer.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SignerKind::Encryption => encryption_signer
|
||||||
|
.clone()
|
||||||
|
.expect("encryption signer must be set"),
|
||||||
|
SignerKind::User => user_signer.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Send rumor event to all members's messaging relays
|
/// Send rumor event to all members's messaging relays
|
||||||
pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> {
|
pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> {
|
||||||
let config = self.config.clone();
|
let config = self.config.clone();
|
||||||
@@ -525,23 +547,12 @@ impl Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine the signer to use
|
// Determine the signer to use
|
||||||
let signer = match signer_kind {
|
let signer = Self::select_signer(
|
||||||
SignerKind::Auto => {
|
signer_kind,
|
||||||
if announcement.is_some()
|
announcement.is_some(),
|
||||||
&& let Some(encryption_signer) = encryption_signer.clone()
|
&encryption_signer,
|
||||||
{
|
&user_signer,
|
||||||
// Safe to unwrap due to earlier checks
|
);
|
||||||
encryption_signer
|
|
||||||
} else {
|
|
||||||
user_signer.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SignerKind::Encryption => {
|
|
||||||
// Safe to unwrap due to earlier checks
|
|
||||||
encryption_signer.as_ref().unwrap().clone()
|
|
||||||
}
|
|
||||||
SignerKind::User => user_signer.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send the gift wrap event and collect the report
|
// Send the gift wrap event and collect the report
|
||||||
match send_gift_wrap(&client, &signer, &member, &rumor, signer_kind).await {
|
match send_gift_wrap(&client, &signer, &member, &rumor, signer_kind).await {
|
||||||
@@ -561,23 +572,12 @@ impl Room {
|
|||||||
let public_key = sender.public_key();
|
let public_key = sender.public_key();
|
||||||
|
|
||||||
// Determine the signer to use
|
// Determine the signer to use
|
||||||
let signer = match signer_kind {
|
let signer = Self::select_signer(
|
||||||
SignerKind::Auto => {
|
signer_kind,
|
||||||
if sender.announcement().is_some()
|
sender.announcement().is_some(),
|
||||||
&& let Some(encryption_signer) = encryption_signer.clone()
|
&encryption_signer,
|
||||||
{
|
&user_signer,
|
||||||
// Safe to unwrap due to earlier checks
|
);
|
||||||
encryption_signer
|
|
||||||
} else {
|
|
||||||
user_signer.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SignerKind::Encryption => {
|
|
||||||
// Safe to unwrap due to earlier checks
|
|
||||||
encryption_signer.as_ref().unwrap().clone()
|
|
||||||
}
|
|
||||||
SignerKind::User => user_signer.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
match send_gift_wrap(&client, &signer, &sender, &rumor, signer_kind).await {
|
match send_gift_wrap(&client, &signer, &sender, &rumor, signer_kind).await {
|
||||||
Ok(report) => reports.push(report),
|
Ok(report) => reports.push(report),
|
||||||
|
|||||||
@@ -19,11 +19,10 @@ anyhow.workspace = true
|
|||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
||||||
linkify = "0.10.0"
|
linkify = "0.10.0"
|
||||||
pulldown-cmark = "0.13.1"
|
pulldown-cmark = "0.13.1"
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
|
||||||
smol.workspace = true
|
|
||||||
|
|||||||
+63
-87
@@ -1,12 +1,11 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, RwLock};
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
use std::sync::RwLock;
|
|
||||||
|
|
||||||
pub use actions::*;
|
pub use actions::*;
|
||||||
use anyhow::{Context as AnyhowContext, Error};
|
use anyhow::{Context as AnyhowContext, Error};
|
||||||
use chat::{ChatRegistry, Message, Room, RoomEvent, SendReport, SendStatus};
|
use chat::{ChatRegistry, Message, Room, RoomEvent, SendReport, SendStatus};
|
||||||
use common::{TimestampExt, coop_cache};
|
use common::{TimestampExt, coop_cache};
|
||||||
|
use futures::lock::Mutex;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||||
@@ -20,8 +19,6 @@ use nostr_sdk::prelude::*;
|
|||||||
use person::{Person, PersonRegistry};
|
use person::{Person, PersonRegistry};
|
||||||
use settings::{AppSettings, SignerKind};
|
use settings::{AppSettings, SignerKind};
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use smol::lock::RwLock;
|
|
||||||
use state::{NostrRegistry, upload};
|
use state::{NostrRegistry, upload};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
@@ -56,14 +53,14 @@ pub struct ChatPanel {
|
|||||||
/// Message list state
|
/// Message list state
|
||||||
list_state: ListState,
|
list_state: ListState,
|
||||||
|
|
||||||
/// All messages
|
/// All messages (sorted by created_at)
|
||||||
messages: BTreeSet<Message>,
|
messages: Vec<Message>,
|
||||||
|
|
||||||
/// Mapping message ids to their rendered texts
|
/// Mapping message ids to their rendered texts
|
||||||
rendered_texts_by_id: BTreeMap<EventId, RenderedText>,
|
rendered_texts_by_id: BTreeMap<EventId, RenderedText>,
|
||||||
|
|
||||||
/// Mapping message (rumor event) ids to their reports
|
/// Mapping message (rumor event) ids to their reports
|
||||||
reports_by_id: Entity<BTreeMap<EventId, Vec<SendReport>>>,
|
reports_by_id: Arc<RwLock<BTreeMap<EventId, Vec<SendReport>>>>,
|
||||||
|
|
||||||
/// Chat input state
|
/// Chat input state
|
||||||
input: Entity<InputState>,
|
input: Entity<InputState>,
|
||||||
@@ -75,7 +72,7 @@ pub struct ChatPanel {
|
|||||||
subject_bar: Entity<bool>,
|
subject_bar: Entity<bool>,
|
||||||
|
|
||||||
/// Sent message ids
|
/// Sent message ids
|
||||||
sent_ids: Arc<RwLock<Vec<EventId>>>,
|
sent_ids: Arc<Mutex<Vec<EventId>>>,
|
||||||
|
|
||||||
/// Replies to
|
/// Replies to
|
||||||
replies_to: Entity<HashSet<EventId>>,
|
replies_to: Entity<HashSet<EventId>>,
|
||||||
@@ -98,10 +95,10 @@ impl ChatPanel {
|
|||||||
// Define attachments and replies_to entities
|
// Define attachments and replies_to entities
|
||||||
let attachments = cx.new(|_| vec![]);
|
let attachments = cx.new(|_| vec![]);
|
||||||
let replies_to = cx.new(|_| HashSet::new());
|
let replies_to = cx.new(|_| HashSet::new());
|
||||||
let reports_by_id = cx.new(|_| BTreeMap::new());
|
let reports_by_id = Arc::new(RwLock::new(BTreeMap::new()));
|
||||||
|
|
||||||
// Define list of messages
|
// Define list of messages
|
||||||
let messages = BTreeSet::default();
|
let messages = Vec::new();
|
||||||
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
|
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
|
||||||
|
|
||||||
// Get room id and name
|
// Get room id and name
|
||||||
@@ -172,7 +169,7 @@ impl ChatPanel {
|
|||||||
attachments,
|
attachments,
|
||||||
rendered_texts_by_id: BTreeMap::new(),
|
rendered_texts_by_id: BTreeMap::new(),
|
||||||
reports_by_id,
|
reports_by_id,
|
||||||
sent_ids: Arc::new(RwLock::new(Vec::new())),
|
sent_ids: Arc::new(Mutex::new(Vec::new())),
|
||||||
uploading: false,
|
uploading: false,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
tasks: vec![],
|
tasks: vec![],
|
||||||
@@ -192,7 +189,7 @@ impl ChatPanel {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let sent_ids = self.sent_ids.clone();
|
let sent_ids = self.sent_ids.clone();
|
||||||
let reports = self.reports_by_id.downgrade();
|
let reports = self.reports_by_id.clone();
|
||||||
|
|
||||||
let (tx, rx) = flume::bounded::<Arc<SendStatus>>(256);
|
let (tx, rx) = flume::bounded::<Arc<SendStatus>>(256);
|
||||||
|
|
||||||
@@ -207,7 +204,7 @@ impl ChatPanel {
|
|||||||
message,
|
message,
|
||||||
} = *message
|
} = *message
|
||||||
{
|
{
|
||||||
let sent_ids = sent_ids.read().await;
|
let sent_ids = sent_ids.lock().await;
|
||||||
|
|
||||||
if sent_ids.contains(&event_id) {
|
if sent_ids.contains(&event_id) {
|
||||||
let status = if status {
|
let status = if status {
|
||||||
@@ -223,10 +220,11 @@ impl ChatPanel {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |_this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
while let Ok(status) = rx.recv_async().await {
|
while let Ok(status) = rx.recv_async().await {
|
||||||
reports.update(cx, |this, cx| {
|
{
|
||||||
for reports in this.values_mut() {
|
let mut map = reports.write().unwrap();
|
||||||
|
for reports in map.values_mut() {
|
||||||
for report in reports.iter_mut() {
|
for report in reports.iter_mut() {
|
||||||
let Some(output) = report.output.as_mut() else {
|
let Some(output) = report.output.as_mut() else {
|
||||||
continue;
|
continue;
|
||||||
@@ -243,10 +241,10 @@ impl ChatPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})?;
|
}
|
||||||
|
this.update(cx, |_, cx| cx.notify()).ok();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
@@ -264,7 +262,11 @@ impl ChatPanel {
|
|||||||
this.insert_message(message, false, cx);
|
this.insert_message(message, false, cx);
|
||||||
}
|
}
|
||||||
RoomEvent::Reload => {
|
RoomEvent::Reload => {
|
||||||
|
// Defer to avoid re-entrant read on Room while
|
||||||
|
// emit_refresh holds a write lock (via refresh_rooms).
|
||||||
|
cx.defer_in(window, |this, window, cx| {
|
||||||
this.get_messages(window, cx);
|
this.get_messages(window, cx);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -345,69 +347,47 @@ impl ChatPanel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get room entity
|
|
||||||
let room = self.room.clone();
|
let room = self.room.clone();
|
||||||
|
|
||||||
// Get content and replies
|
|
||||||
let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect();
|
let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect();
|
||||||
let content = value.to_string();
|
let content = value.to_string();
|
||||||
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let room = room.upgrade().context("Room is not available")?;
|
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
match room.read(cx).rumor(content, replies, cx) {
|
|
||||||
Some(rumor) => {
|
|
||||||
this.insert_message(&rumor, true, cx);
|
|
||||||
this.send_and_wait(rumor, window, cx);
|
|
||||||
this.clear(window, cx);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
window.push_notification("Failed to create message", cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send message in the background and wait for the response
|
|
||||||
fn send_and_wait(&mut self, rumor: UnsignedEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let sent_ids = self.sent_ids.clone();
|
let sent_ids = self.sent_ids.clone();
|
||||||
|
|
||||||
// This can't fail, because we already ensured that the ID is set
|
// Upgrade room and create rumor + send task in a single read lock
|
||||||
let id = rumor.id.unwrap();
|
let Some(room_entity) = room.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (rumor, send_task) = match room_entity.read_with(cx, |room, cx| {
|
||||||
|
let rumor = room.rumor(content.clone(), replies, cx)?;
|
||||||
|
let send_task = room.send(rumor.clone(), cx)?;
|
||||||
|
Some((rumor, send_task))
|
||||||
|
}) {
|
||||||
|
Some(pair) => pair,
|
||||||
|
None => {
|
||||||
|
window.push_notification("Failed to create message", cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Add empty reports
|
let id = rumor.id.expect("rumor must have an id");
|
||||||
|
|
||||||
|
// Insert optimistic message and clear input
|
||||||
|
self.insert_message(&rumor, true, cx);
|
||||||
self.insert_reports(id, vec![], cx);
|
self.insert_reports(id, vec![], cx);
|
||||||
|
self.clear(window, cx);
|
||||||
|
|
||||||
// Upgrade room reference
|
// Spawn a single task to await the send and update reports
|
||||||
let Some(room) = self.room.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the send message task
|
|
||||||
let Some(task) = room.read(cx).send(rumor, cx) else {
|
|
||||||
window.push_notification("Failed to send message", cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
// Send and get reports
|
let outputs = send_task.await;
|
||||||
let outputs = task.await;
|
|
||||||
|
|
||||||
// Add sent IDs to the list
|
let mut sent_ids = sent_ids.lock().await;
|
||||||
let mut sent_ids = sent_ids.write().await;
|
|
||||||
sent_ids.extend(outputs.iter().filter_map(|output| output.gift_wrap_id));
|
sent_ids.extend(outputs.iter().filter_map(|output| output.gift_wrap_id));
|
||||||
|
|
||||||
// Update the state
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.insert_reports(id, outputs, cx);
|
this.insert_reports(id, outputs, cx);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear the input field, attachments, and replies
|
/// Clear the input field, attachments, and replies
|
||||||
@@ -429,10 +409,13 @@ impl ChatPanel {
|
|||||||
|
|
||||||
/// Insert reports
|
/// Insert reports
|
||||||
fn insert_reports(&mut self, id: EventId, reports: Vec<SendReport>, cx: &mut Context<Self>) {
|
fn insert_reports(&mut self, id: EventId, reports: Vec<SendReport>, cx: &mut Context<Self>) {
|
||||||
self.reports_by_id.update(cx, |this, cx| {
|
self.reports_by_id
|
||||||
this.entry(id).or_default().extend(reports);
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.entry(id)
|
||||||
|
.or_default()
|
||||||
|
.extend(reports);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a message into the chat panel
|
/// Insert a message into the chat panel
|
||||||
@@ -441,9 +424,10 @@ impl ChatPanel {
|
|||||||
E: Into<Message>,
|
E: Into<Message>,
|
||||||
{
|
{
|
||||||
let old_len = self.messages.len();
|
let old_len = self.messages.len();
|
||||||
|
let msg: Message = m.into();
|
||||||
|
|
||||||
// Extend the messages list with the new events
|
if let Err(pos) = self.messages.binary_search(&msg) {
|
||||||
if self.messages.insert(m.into()) {
|
self.messages.insert(pos, msg);
|
||||||
self.list_state.splice(old_len..old_len, 1);
|
self.list_state.splice(old_len..old_len, 1);
|
||||||
|
|
||||||
if scroll {
|
if scroll {
|
||||||
@@ -466,13 +450,12 @@ impl ChatPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a message has any reports
|
/// Check if a message has any reports
|
||||||
fn has_reports(&self, id: &EventId, cx: &App) -> bool {
|
fn has_reports(&self, id: &EventId, _cx: &App) -> bool {
|
||||||
self.reports_by_id.read(cx).get(id).is_some()
|
self.reports_by_id.read().unwrap().get(id).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all sent reports for a message by its ID
|
fn sent_reports(&self, id: &EventId, _cx: &App) -> Option<Vec<SendReport>> {
|
||||||
fn sent_reports(&self, id: &EventId, cx: &App) -> Option<Vec<SendReport>> {
|
self.reports_by_id.read().unwrap().get(id).cloned()
|
||||||
self.reports_by_id.read(cx).get(id).cloned()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a message by its ID
|
/// Get a message by its ID
|
||||||
@@ -796,10 +779,8 @@ impl ChatPanel {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut iter = self.messages.iter();
|
if let Some(previous) = self.messages.get(ix - 1)
|
||||||
|
&& let Some(current) = self.messages.get(ix)
|
||||||
if let Some(previous) = iter.nth(ix - 1)
|
|
||||||
&& let Some(current) = iter.next()
|
|
||||||
{
|
{
|
||||||
if current.author != previous.author {
|
if current.author != previous.author {
|
||||||
return true;
|
return true;
|
||||||
@@ -822,7 +803,7 @@ impl ChatPanel {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
if let Some(message) = self.messages.iter().nth(ix) {
|
if let Some(message) = self.messages.get(ix) {
|
||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
let show_author = self.is_group_start(ix);
|
let show_author = self.is_group_start(ix);
|
||||||
let text = self
|
let text = self
|
||||||
@@ -1496,12 +1477,7 @@ impl Render for ChatPanel {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.border_b_1()
|
.border_b_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.child(
|
.child(Input::new(&self.subject_input).text_sm().small())
|
||||||
Input::new(&self.subject_input)
|
|
||||||
.text_sm()
|
|
||||||
.small()
|
|
||||||
.bordered(false),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
Button::new("change")
|
Button::new("change")
|
||||||
.icon(IconName::CheckCircle)
|
.icon(IconName::CheckCircle)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ publish.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr.workspace = true
|
nostr.workspace = true
|
||||||
|
instant.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ settings = { path = "../settings" }
|
|||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
instant.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|||||||
+34
-38
@@ -4,7 +4,7 @@ use std::path::PathBuf;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -92,7 +92,6 @@ impl DeviceRegistry {
|
|||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let settings = AppSettings::global(cx);
|
let settings = AppSettings::global(cx);
|
||||||
let nip4e_enabled = settings.read(cx).is_nip4e_enabled(cx);
|
|
||||||
|
|
||||||
let signer = cx.new(|_| None);
|
let signer = cx.new(|_| None);
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
@@ -109,7 +108,7 @@ impl DeviceRegistry {
|
|||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe the user signer
|
// Observe the user signer
|
||||||
cx.subscribe(&nostr, move |this, _nostr, event, cx| {
|
cx.subscribe(&nostr, move |this, _nostr, event, cx| {
|
||||||
if event.signer_changed() && nip4e_enabled {
|
if event.signer_changed() && settings.read(cx).is_nip4e_enabled(cx) {
|
||||||
this.get_announcement(cx);
|
this.get_announcement(cx);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -226,6 +225,11 @@ impl DeviceRegistry {
|
|||||||
let keys = get_keys(&client, &signer).await?;
|
let keys = get_keys(&client, &signer).await?;
|
||||||
let content = keys.secret_key().to_bech32()?;
|
let content = keys.secret_key().to_bech32()?;
|
||||||
|
|
||||||
|
if cfg!(target_arch = "wasm32") {
|
||||||
|
return Err(anyhow!("Not supported"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
smol::fs::write(path, &content).await?;
|
smol::fs::write(path, &content).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -258,27 +262,19 @@ impl DeviceRegistry {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
let announcement_existed = self.announcement_existed.clone();
|
let announcement_existed = self.announcement_existed.clone();
|
||||||
let executor = cx.background_executor().clone();
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
if !cx
|
|
||||||
.background_spawn(async move {
|
|
||||||
// Wait for 5 seconds
|
// Wait for 5 seconds
|
||||||
executor.timer(Duration::from_secs(5)).await;
|
cx.background_executor().timer(Duration::from_secs(5)).await;
|
||||||
|
|
||||||
// Then check if the msg relays have been found
|
// Then check if the msg relays have been found
|
||||||
if !announcement_existed.load(Ordering::Acquire) {
|
if announcement_existed.load(Ordering::Acquire) {
|
||||||
return true;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(DeviceEvent::NotSet);
|
cx.emit(DeviceEvent::NotSet);
|
||||||
})?;
|
})?;
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
@@ -404,11 +400,10 @@ impl DeviceRegistry {
|
|||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
let Ok(app_keys) = get_or_init_app_keys(cx) else {
|
let app_keys_task = get_or_init_app_keys(cx);
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let task: Task<Result<Option<Event>, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Option<Event>, Error>> = cx.background_spawn(async move {
|
||||||
|
let app_keys = app_keys_task.await?;
|
||||||
let app_pubkey = app_keys.public_key();
|
let app_pubkey = app_keys.public_key();
|
||||||
let public_key = signer.get_public_key_async().await?;
|
let public_key = signer.get_public_key_async().await?;
|
||||||
|
|
||||||
@@ -489,11 +484,10 @@ impl DeviceRegistry {
|
|||||||
|
|
||||||
/// Parse the approval event to get encryption key then set it
|
/// Parse the approval event to get encryption key then set it
|
||||||
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
|
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
|
||||||
let Ok(app_keys) = get_or_init_app_keys(cx) else {
|
let app_keys_task = get_or_init_app_keys(cx);
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
|
||||||
|
let app_keys = app_keys_task.await?;
|
||||||
let master = event
|
let master = event
|
||||||
.tags
|
.tags
|
||||||
.iter()
|
.iter()
|
||||||
@@ -573,7 +567,7 @@ impl DeviceRegistry {
|
|||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
self.tasks.push(cx.spawn_in(window, async move |_this, cx| {
|
||||||
match task.await {
|
match task.await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
@@ -591,8 +585,9 @@ impl DeviceRegistry {
|
|||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
|
||||||
.detach();
|
Ok(())
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle encryption request
|
/// Handle encryption request
|
||||||
@@ -715,33 +710,34 @@ impl DeviceRegistry {
|
|||||||
|
|
||||||
struct DeviceNotification;
|
struct DeviceNotification;
|
||||||
|
|
||||||
/// Get or create new app keys
|
/// Get or create new app keys (async, returns a task)
|
||||||
fn get_or_init_app_keys(cx: &App) -> Result<Keys, Error> {
|
fn get_or_init_app_keys(cx: &App) -> Task<Result<Keys, Error>> {
|
||||||
let read = cx.read_credentials(CLIENT_NAME);
|
let read = cx.read_credentials(CLIENT_NAME);
|
||||||
let stored_keys: Option<Keys> = cx.foreground_executor().block_on(async move {
|
|
||||||
if let Ok(Some((_, secret))) = read.await {
|
|
||||||
SecretKey::from_slice(&secret).map(Keys::new).ok()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(keys) = stored_keys {
|
cx.spawn(async move |cx| {
|
||||||
Ok(keys)
|
if let Ok(Some((_, secret))) = read.await
|
||||||
} else {
|
&& let Ok(keys) = SecretKey::from_slice(&secret).map(Keys::new)
|
||||||
|
{
|
||||||
|
return Ok(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No stored keys found or invalid — generate new ones
|
||||||
let keys = Keys::generate();
|
let keys = Keys::generate();
|
||||||
let user = keys.public_key().to_hex();
|
let user = keys.public_key().to_hex();
|
||||||
let secret = keys.secret_key().to_secret_bytes();
|
let secret = keys.secret_key().to_secret_bytes();
|
||||||
let write = cx.write_credentials(CLIENT_NAME, &user, &secret);
|
|
||||||
|
|
||||||
cx.foreground_executor().block_on(async move {
|
cx.update(|cx| {
|
||||||
|
let write = cx.write_credentials(CLIENT_NAME, &user, &secret);
|
||||||
|
cx.background_spawn(async move {
|
||||||
if let Err(e) = write.await {
|
if let Err(e) = write.await {
|
||||||
log::error!("Keyring not available or panic: {e}")
|
log::error!("Keyring not available or panic: {e}")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(keys)
|
Ok(keys)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt and store device keys in the local database.
|
/// Encrypt and store device keys in the local database.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ state = { path = "../state" }
|
|||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
instant.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
|
|||||||
+44
-56
@@ -1,7 +1,6 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::rc::Rc;
|
use std::sync::RwLock;
|
||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
use common::EventExt;
|
use common::EventExt;
|
||||||
@@ -24,9 +23,9 @@ impl Global for GlobalPersonRegistry {}
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Dispatch {
|
enum Dispatch {
|
||||||
Person(Box<Person>),
|
Person(Person),
|
||||||
Announcement(Box<Event>),
|
Announcement(Event),
|
||||||
Relays(Box<Event>),
|
Relays(Event),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Person Registry
|
/// Person Registry
|
||||||
@@ -36,7 +35,7 @@ pub struct PersonRegistry {
|
|||||||
persons: HashMap<PublicKey, Entity<Person>>,
|
persons: HashMap<PublicKey, Entity<Person>>,
|
||||||
|
|
||||||
/// Set of public keys that have been seen
|
/// Set of public keys that have been seen
|
||||||
seens: Rc<RefCell<HashSet<PublicKey>>>,
|
seen: RwLock<HashSet<PublicKey>>,
|
||||||
|
|
||||||
/// Sender for requesting metadata
|
/// Sender for requesting metadata
|
||||||
sender: flume::Sender<PublicKey>,
|
sender: flume::Sender<PublicKey>,
|
||||||
@@ -63,40 +62,26 @@ impl PersonRegistry {
|
|||||||
|
|
||||||
// Channel for communication between nostr and gpui
|
// Channel for communication between nostr and gpui
|
||||||
let (tx, rx) = flume::bounded::<Dispatch>(100);
|
let (tx, rx) = flume::bounded::<Dispatch>(100);
|
||||||
let (mta_tx, mta_rx) = flume::unbounded::<PublicKey>();
|
let (metadata_tx, metadata_rx) = flume::unbounded::<PublicKey>();
|
||||||
|
|
||||||
let mut tasks = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
tasks.push(
|
let client2 = client.clone();
|
||||||
// Handle nostr notifications
|
tasks.push(cx.background_spawn(async move {
|
||||||
cx.background_spawn({
|
Self::handle_notifications(&client2, &tx).await;
|
||||||
let client = client.clone();
|
}));
|
||||||
|
|
||||||
async move {
|
let client3 = client.clone();
|
||||||
Self::handle_notifications(&client, &tx).await;
|
tasks.push(cx.background_spawn(async move {
|
||||||
}
|
Self::handle_requests(&client3, &metadata_rx).await;
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(cx.spawn(async move |this, cx| {
|
||||||
// Handle metadata requests
|
|
||||||
cx.background_spawn({
|
|
||||||
let client = client.clone();
|
|
||||||
|
|
||||||
async move {
|
|
||||||
Self::handle_requests(&client, &mta_rx).await;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
tasks.push(
|
|
||||||
// Update GPUI state
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
while let Ok(event) = rx.recv_async().await {
|
while let Ok(event) = rx.recv_async().await {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
match event {
|
match event {
|
||||||
Dispatch::Person(person) => {
|
Dispatch::Person(person) => {
|
||||||
this.insert(*person, cx);
|
this.insert(person, cx);
|
||||||
}
|
}
|
||||||
Dispatch::Announcement(event) => {
|
Dispatch::Announcement(event) => {
|
||||||
this.set_announcement(&event, cx);
|
this.set_announcement(&event, cx);
|
||||||
@@ -108,8 +93,7 @@ impl PersonRegistry {
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
// Load all user profiles from the database
|
// Load all user profiles from the database
|
||||||
cx.defer_in(window, |this, _window, cx| {
|
cx.defer_in(window, |this, _window, cx| {
|
||||||
@@ -118,8 +102,8 @@ impl PersonRegistry {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
persons: HashMap::new(),
|
persons: HashMap::new(),
|
||||||
seens: Rc::new(RefCell::new(HashSet::new())),
|
seen: RwLock::new(HashSet::new()),
|
||||||
sender: mta_tx,
|
sender: metadata_tx,
|
||||||
tasks,
|
tasks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,24 +129,25 @@ impl PersonRegistry {
|
|||||||
Kind::Metadata => {
|
Kind::Metadata => {
|
||||||
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
||||||
let person = Person::new(event.pubkey, metadata);
|
let person = Person::new(event.pubkey, metadata);
|
||||||
let val = Box::new(person);
|
if tx.send_async(Dispatch::Person(person)).await.is_err() {
|
||||||
// Send
|
log::warn!("PersonRegistry channel closed, dropping metadata event");
|
||||||
tx.send_async(Dispatch::Person(val)).await.ok();
|
}
|
||||||
}
|
}
|
||||||
Kind::ContactList => {
|
Kind::ContactList => {
|
||||||
let public_keys = event.extract_public_keys();
|
let public_keys = event.extract_public_keys();
|
||||||
// Get metadata for all public keys
|
if let Err(e) = get_metadata(client, public_keys).await {
|
||||||
get_metadata(client, public_keys).await.ok();
|
log::warn!("Failed to get metadata for contact list: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Kind::InboxRelays => {
|
Kind::InboxRelays => {
|
||||||
let val = Box::new(event.into_owned());
|
tx.send_async(Dispatch::Relays(event.into_owned()))
|
||||||
// Send
|
.await
|
||||||
tx.send_async(Dispatch::Relays(val)).await.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
Kind::Custom(10044) => {
|
Kind::Custom(10044) => {
|
||||||
let val = Box::new(event.into_owned());
|
tx.send_async(Dispatch::Announcement(event.into_owned()))
|
||||||
// Send
|
.await
|
||||||
tx.send_async(Dispatch::Announcement(val)).await.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -182,13 +167,17 @@ impl PersonRegistry {
|
|||||||
Ok(Some(public_key)) => {
|
Ok(Some(public_key)) => {
|
||||||
batch.insert(public_key);
|
batch.insert(public_key);
|
||||||
// Process the batch if it's full
|
// Process the batch if it's full
|
||||||
if batch.len() >= 20 {
|
if batch.len() >= 20
|
||||||
get_metadata(client, std::mem::take(&mut batch)).await.ok();
|
&& let Err(e) = get_metadata(client, std::mem::take(&mut batch)).await
|
||||||
|
{
|
||||||
|
log::warn!("Failed to get metadata batch: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if !batch.is_empty() {
|
if !batch.is_empty()
|
||||||
get_metadata(client, std::mem::take(&mut batch)).await.ok();
|
&& let Err(e) = get_metadata(client, std::mem::take(&mut batch)).await
|
||||||
|
{
|
||||||
|
log::warn!("Failed to get metadata batch: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,7 +206,7 @@ impl PersonRegistry {
|
|||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
if let Ok(persons) = task.await {
|
if let Ok(persons) = task.await {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.bulk_inserts(persons, cx);
|
this.bulk_insert(persons, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -256,7 +245,7 @@ impl PersonRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Insert batch of persons
|
/// Insert batch of persons
|
||||||
fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
|
fn bulk_insert(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
|
||||||
for person in persons.into_iter() {
|
for person in persons.into_iter() {
|
||||||
let public_key = person.public_key();
|
let public_key = person.public_key();
|
||||||
self.persons
|
self.persons
|
||||||
@@ -290,15 +279,14 @@ impl PersonRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let public_key = *public_key;
|
let public_key = *public_key;
|
||||||
let mut seen = self.seens.borrow_mut();
|
|
||||||
|
|
||||||
if seen.insert(public_key) {
|
if self.seen.write().unwrap().insert(public_key) {
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
|
|
||||||
// Spawn background task to request metadata
|
// Spawn background task to request metadata
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
if let Err(e) = sender.send_async(public_key).await {
|
if let Err(e) = sender.send_async(public_key).await {
|
||||||
log::warn!("Failed to send public key for metadata request: {}", e);
|
log::warn!("Failed to send public key for metadata request: {e}");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use std::fmt::Display;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
@@ -42,28 +41,10 @@ setting_accessors! {
|
|||||||
pub hide_avatar: bool,
|
pub hide_avatar: bool,
|
||||||
pub screening: bool,
|
pub screening: bool,
|
||||||
pub nip4e: bool,
|
pub nip4e: bool,
|
||||||
pub auth_mode: AuthMode,
|
|
||||||
pub trusted_relays: Vec<String>,
|
pub trusted_relays: Vec<String>,
|
||||||
pub file_server: Url,
|
pub file_server: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authentication mode
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum AuthMode {
|
|
||||||
#[default]
|
|
||||||
Auto,
|
|
||||||
Manual,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for AuthMode {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
AuthMode::Auto => write!(f, "Auto"),
|
|
||||||
AuthMode::Manual => write!(f, "Ask every time"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Signer kind
|
/// Signer kind
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum SignerKind {
|
pub enum SignerKind {
|
||||||
@@ -141,9 +122,6 @@ pub struct Settings {
|
|||||||
/// Enable decoupling encryption key
|
/// Enable decoupling encryption key
|
||||||
pub nip4e: bool,
|
pub nip4e: bool,
|
||||||
|
|
||||||
/// Authentication mode
|
|
||||||
pub auth_mode: AuthMode,
|
|
||||||
|
|
||||||
/// Trusted relays; Coop will automatically authenticate with these relays
|
/// Trusted relays; Coop will automatically authenticate with these relays
|
||||||
pub trusted_relays: Vec<String>,
|
pub trusted_relays: Vec<String>,
|
||||||
|
|
||||||
@@ -159,7 +137,6 @@ impl Default for Settings {
|
|||||||
hide_avatar: false,
|
hide_avatar: false,
|
||||||
screening: true,
|
screening: true,
|
||||||
nip4e: false,
|
nip4e: false,
|
||||||
auth_mode: AuthMode::default(),
|
|
||||||
trusted_relays: vec![],
|
trusted_relays: vec![],
|
||||||
file_server: Url::parse("https://blossom.band/").unwrap(),
|
file_server: Url::parse("https://blossom.band/").unwrap(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ nostr-blossom.workspace = true
|
|||||||
nostr-connect.workspace = true
|
nostr-connect.workspace = true
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
instant.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|||||||
@@ -39,14 +39,15 @@ pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nip46.com";
|
|||||||
pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
|
pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
|
||||||
|
|
||||||
/// Default search relays
|
/// Default search relays
|
||||||
pub const INDEXER_RELAYS: [&str; 1] = ["wss://indexer.coracle.social"];
|
pub const INDEXER_RELAYS: [&str; 2] = ["wss://indexer.coracle.social", "wss://user.kindpag.es"];
|
||||||
|
|
||||||
/// Default search relays
|
/// Default search relays
|
||||||
pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"];
|
pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"];
|
||||||
|
|
||||||
/// Default bootstrap relays
|
/// Default bootstrap relays
|
||||||
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
|
pub const BOOTSTRAP_RELAYS: [&str; 4] = [
|
||||||
"wss://relay.ditto.pub",
|
"wss://relay.ditto.pub",
|
||||||
"wss://relay.primal.net",
|
"wss://relay.primal.net",
|
||||||
"wss://user.kindpag.es",
|
"wss://relay.nostr.net",
|
||||||
|
"wss://profiles.nostr1.com",
|
||||||
];
|
];
|
||||||
|
|||||||
+14
-5
@@ -1,9 +1,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
use common::config_dir;
|
use common::config_dir;
|
||||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, Window};
|
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, Window};
|
||||||
|
use instant::Duration;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use nostr_gossip_memory::prelude::*;
|
use nostr_gossip_memory::prelude::*;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@@ -130,7 +130,12 @@ impl NostrRegistry {
|
|||||||
// Connect to bootstrap relays after the window is ready
|
// Connect to bootstrap relays after the window is ready
|
||||||
cx.defer_in(window, |this, _window, cx| {
|
cx.defer_in(window, |this, _window, cx| {
|
||||||
this.connect_bootstrap_relays(cx);
|
this.connect_bootstrap_relays(cx);
|
||||||
|
|
||||||
|
if cfg!(target_arch = "wasm32") {
|
||||||
|
cx.emit(StateEvent::NoSigner);
|
||||||
|
} else {
|
||||||
this.get_user_credential(cx);
|
this.get_user_credential(cx);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -164,7 +169,7 @@ impl NostrRegistry {
|
|||||||
<T as AsyncSignEvent>::Error: std::error::Error + Send + Sync + 'static,
|
<T as AsyncSignEvent>::Error: std::error::Error + Send + Sync + 'static,
|
||||||
<T as AsyncNip44>::Error: std::error::Error + Send + Sync + 'static,
|
<T as AsyncNip44>::Error: std::error::Error + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
cx.spawn(async move |this, cx| {
|
let task = cx.spawn(async move |this, cx| {
|
||||||
match new_signer.get_public_key_async().await {
|
match new_signer.get_public_key_async().await {
|
||||||
Ok(public_key) => {
|
Ok(public_key) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
@@ -181,9 +186,9 @@ impl NostrRegistry {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok::<(), anyhow::Error>(())
|
Ok(())
|
||||||
})
|
});
|
||||||
.detach();
|
self.tasks.push(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect to the bootstrapping relays
|
/// Connect to the bootstrapping relays
|
||||||
@@ -268,6 +273,10 @@ impl NostrRegistry {
|
|||||||
|
|
||||||
/// Get the master key that used for Nostr Connect
|
/// Get the master key that used for Nostr Connect
|
||||||
pub fn get_master_key(&self, cx: &App) -> Task<Keys> {
|
pub fn get_master_key(&self, cx: &App) -> Task<Keys> {
|
||||||
|
if cfg!(target_arch = "wasm32") {
|
||||||
|
return cx.background_spawn(async move { Keys::generate() });
|
||||||
|
}
|
||||||
|
|
||||||
let task = cx.read_credentials(MASTER_KEYRING);
|
let task = cx.read_credentials(MASTER_KEYRING);
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
|
|||||||
+24
-14
@@ -1,5 +1,7 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use nostr_connect::client::AuthUrlHandler;
|
use nostr_connect::client::AuthUrlHandler;
|
||||||
@@ -60,21 +62,23 @@ impl UniversalSigner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trait InnerSigner: fmt::Debug + Send + Sync + 'static {
|
trait InnerSigner: fmt::Debug + Send + Sync + 'static {
|
||||||
fn get_public_key_async(&self) -> BoxedFuture<'_, Result<PublicKey, UniversalSignerError>>;
|
fn get_public_key_async(
|
||||||
|
&self,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<PublicKey, UniversalSignerError>> + Send + '_>>;
|
||||||
fn sign_event_async(
|
fn sign_event_async(
|
||||||
&self,
|
&self,
|
||||||
unsigned: UnsignedEvent,
|
unsigned: UnsignedEvent,
|
||||||
) -> BoxedFuture<'_, Result<Event, UniversalSignerError>>;
|
) -> Pin<Box<dyn Future<Output = Result<Event, UniversalSignerError>> + Send + '_>>;
|
||||||
fn nip44_encrypt_async<'a>(
|
fn nip44_encrypt_async<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
public_key: &'a PublicKey,
|
public_key: &'a PublicKey,
|
||||||
content: &'a str,
|
content: &'a str,
|
||||||
) -> BoxedFuture<'a, Result<String, UniversalSignerError>>;
|
) -> Pin<Box<dyn Future<Output = Result<String, UniversalSignerError>> + Send + 'a>>;
|
||||||
fn nip44_decrypt_async<'a>(
|
fn nip44_decrypt_async<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
public_key: &'a PublicKey,
|
public_key: &'a PublicKey,
|
||||||
payload: &'a str,
|
payload: &'a str,
|
||||||
) -> BoxedFuture<'a, Result<String, UniversalSignerError>>;
|
) -> Pin<Box<dyn Future<Output = Result<String, UniversalSignerError>> + Send + 'a>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -87,7 +91,9 @@ where
|
|||||||
<T as AsyncSignEvent>::Error: Error + Send + Sync + 'static,
|
<T as AsyncSignEvent>::Error: Error + Send + Sync + 'static,
|
||||||
<T as AsyncNip44>::Error: Error + Send + Sync + 'static,
|
<T as AsyncNip44>::Error: Error + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
fn get_public_key_async(&self) -> BoxedFuture<'_, Result<PublicKey, UniversalSignerError>> {
|
fn get_public_key_async(
|
||||||
|
&self,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<PublicKey, UniversalSignerError>> + Send + '_>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
AsyncGetPublicKey::get_public_key_async(&self.0)
|
AsyncGetPublicKey::get_public_key_async(&self.0)
|
||||||
.await
|
.await
|
||||||
@@ -98,7 +104,7 @@ where
|
|||||||
fn sign_event_async(
|
fn sign_event_async(
|
||||||
&self,
|
&self,
|
||||||
unsigned: UnsignedEvent,
|
unsigned: UnsignedEvent,
|
||||||
) -> BoxedFuture<'_, Result<Event, UniversalSignerError>> {
|
) -> Pin<Box<dyn Future<Output = Result<Event, UniversalSignerError>> + Send + '_>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
AsyncSignEvent::sign_event_async(&self.0, unsigned)
|
AsyncSignEvent::sign_event_async(&self.0, unsigned)
|
||||||
.await
|
.await
|
||||||
@@ -110,7 +116,7 @@ where
|
|||||||
&'a self,
|
&'a self,
|
||||||
public_key: &'a PublicKey,
|
public_key: &'a PublicKey,
|
||||||
content: &'a str,
|
content: &'a str,
|
||||||
) -> BoxedFuture<'a, Result<String, UniversalSignerError>> {
|
) -> Pin<Box<dyn Future<Output = Result<String, UniversalSignerError>> + Send + 'a>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
AsyncNip44::nip44_encrypt_async(&self.0, public_key, content)
|
AsyncNip44::nip44_encrypt_async(&self.0, public_key, content)
|
||||||
.await
|
.await
|
||||||
@@ -122,7 +128,7 @@ where
|
|||||||
&'a self,
|
&'a self,
|
||||||
public_key: &'a PublicKey,
|
public_key: &'a PublicKey,
|
||||||
payload: &'a str,
|
payload: &'a str,
|
||||||
) -> BoxedFuture<'a, Result<String, UniversalSignerError>> {
|
) -> Pin<Box<dyn Future<Output = Result<String, UniversalSignerError>> + Send + 'a>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
AsyncNip44::nip44_decrypt_async(&self.0, public_key, payload)
|
AsyncNip44::nip44_decrypt_async(&self.0, public_key, payload)
|
||||||
.await
|
.await
|
||||||
@@ -142,7 +148,9 @@ impl UniversalSigner {
|
|||||||
impl AsyncGetPublicKey for UniversalSigner {
|
impl AsyncGetPublicKey for UniversalSigner {
|
||||||
type Error = UniversalSignerError;
|
type Error = UniversalSignerError;
|
||||||
|
|
||||||
fn get_public_key_async(&self) -> BoxedFuture<'_, Result<PublicKey, Self::Error>> {
|
fn get_public_key_async(
|
||||||
|
&self,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<PublicKey, Self::Error>> + Send + '_>> {
|
||||||
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
||||||
Box::pin(async move { inner.get_public_key_async().await })
|
Box::pin(async move { inner.get_public_key_async().await })
|
||||||
}
|
}
|
||||||
@@ -154,7 +162,7 @@ impl AsyncSignEvent for UniversalSigner {
|
|||||||
fn sign_event_async(
|
fn sign_event_async(
|
||||||
&self,
|
&self,
|
||||||
unsigned: UnsignedEvent,
|
unsigned: UnsignedEvent,
|
||||||
) -> BoxedFuture<'_, Result<Event, Self::Error>> {
|
) -> Pin<Box<dyn Future<Output = Result<Event, Self::Error>> + Send + '_>> {
|
||||||
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
||||||
Box::pin(async move { inner.sign_event_async(unsigned).await })
|
Box::pin(async move { inner.sign_event_async(unsigned).await })
|
||||||
}
|
}
|
||||||
@@ -167,7 +175,7 @@ impl AsyncNip44 for UniversalSigner {
|
|||||||
&'a self,
|
&'a self,
|
||||||
public_key: &'a PublicKey,
|
public_key: &'a PublicKey,
|
||||||
content: &'a str,
|
content: &'a str,
|
||||||
) -> BoxedFuture<'a, Result<String, Self::Error>> {
|
) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
|
||||||
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
||||||
Box::pin(async move { inner.nip44_encrypt_async(public_key, content).await })
|
Box::pin(async move { inner.nip44_encrypt_async(public_key, content).await })
|
||||||
}
|
}
|
||||||
@@ -176,7 +184,7 @@ impl AsyncNip44 for UniversalSigner {
|
|||||||
&'a self,
|
&'a self,
|
||||||
public_key: &'a PublicKey,
|
public_key: &'a PublicKey,
|
||||||
payload: &'a str,
|
payload: &'a str,
|
||||||
) -> BoxedFuture<'a, Result<String, Self::Error>> {
|
) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
|
||||||
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
||||||
Box::pin(async move { inner.nip44_decrypt_async(public_key, payload).await })
|
Box::pin(async move { inner.nip44_decrypt_async(public_key, payload).await })
|
||||||
}
|
}
|
||||||
@@ -186,8 +194,10 @@ impl AsyncNip44 for UniversalSigner {
|
|||||||
pub struct CoopAuthUrlHandler;
|
pub struct CoopAuthUrlHandler;
|
||||||
|
|
||||||
impl AuthUrlHandler for CoopAuthUrlHandler {
|
impl AuthUrlHandler for CoopAuthUrlHandler {
|
||||||
#[allow(mismatched_lifetime_syntaxes)]
|
fn on_auth_url(
|
||||||
fn on_auth_url(&self, auth_url: Url) -> BoxedFuture<Result<(), nostr_connect::error::Error>> {
|
&self,
|
||||||
|
auth_url: Url,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<(), nostr_connect::error::Error>> + Send + '_>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
webbrowser::open(auth_url.as_str()).unwrap();
|
webbrowser::open(auth_url.as_str()).unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ impl ThemeColors {
|
|||||||
elevated_surface_background: neutral().light().step_3(),
|
elevated_surface_background: neutral().light().step_3(),
|
||||||
panel_background: neutral().light().step_1(),
|
panel_background: neutral().light().step_1(),
|
||||||
overlay: neutral().light_alpha().step_3(),
|
overlay: neutral().light_alpha().step_3(),
|
||||||
title_bar: neutral().light().step_3(),
|
title_bar: neutral().light().step_2(),
|
||||||
title_bar_inactive: neutral().light().step_1(),
|
title_bar_inactive: neutral().light().step_1(),
|
||||||
window_border: hsl(240.0, 5.9, 78.0),
|
window_border: hsl(240.0, 5.9, 78.0),
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ impl ThemeColors {
|
|||||||
elevated_surface_background: neutral().dark().step_3(),
|
elevated_surface_background: neutral().dark().step_3(),
|
||||||
panel_background: neutral().dark().step_1(),
|
panel_background: neutral().dark().step_1(),
|
||||||
overlay: neutral().dark_alpha().step_3(),
|
overlay: neutral().dark_alpha().step_3(),
|
||||||
title_bar: neutral().dark().step_3(),
|
title_bar: neutral().dark().step_2(),
|
||||||
title_bar_inactive: neutral().dark().step_1(),
|
title_bar_inactive: neutral().dark().step_1(),
|
||||||
window_border: hsl(240.0, 3.7, 28.0),
|
window_border: hsl(240.0, 3.7, 28.0),
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0);
|
|||||||
pub const TITLEBAR_HEIGHT: Pixels = px(36.0);
|
pub const TITLEBAR_HEIGHT: Pixels = px(36.0);
|
||||||
|
|
||||||
/// Defines workspace tabbar height
|
/// Defines workspace tabbar height
|
||||||
pub const TABBAR_HEIGHT: Pixels = px(28.0);
|
pub const TABBAR_HEIGHT: Pixels = px(44.0);
|
||||||
|
|
||||||
/// Defines default sidebar width
|
/// Defines default sidebar width
|
||||||
pub const SIDEBAR_WIDTH: Pixels = px(240.);
|
pub const SIDEBAR_WIDTH: Pixels = px(240.);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pub enum PlatformKind {
|
|||||||
Mac,
|
Mac,
|
||||||
Linux,
|
Linux,
|
||||||
Windows,
|
Windows,
|
||||||
|
Web,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformKind {
|
impl PlatformKind {
|
||||||
@@ -11,22 +12,21 @@ impl PlatformKind {
|
|||||||
Self::Linux
|
Self::Linux
|
||||||
} else if cfg!(target_os = "windows") {
|
} else if cfg!(target_os = "windows") {
|
||||||
Self::Windows
|
Self::Windows
|
||||||
} else {
|
} else if cfg!(target_os = "macos") {
|
||||||
Self::Mac
|
Self::Mac
|
||||||
|
} else {
|
||||||
|
Self::Web
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn is_linux(&self) -> bool {
|
pub fn is_linux(&self) -> bool {
|
||||||
matches!(self, Self::Linux)
|
matches!(self, Self::Linux)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn is_windows(&self) -> bool {
|
pub fn is_windows(&self) -> bool {
|
||||||
matches!(self, Self::Windows)
|
matches!(self, Self::Windows)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn is_mac(&self) -> bool {
|
pub fn is_mac(&self) -> bool {
|
||||||
matches!(self, Self::Mac)
|
matches!(self, Self::Mac)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ common = { path = "../common" }
|
|||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
instant.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
@@ -21,7 +22,7 @@ regex = "1"
|
|||||||
lsp-types = "0.97.0"
|
lsp-types = "0.97.0"
|
||||||
ropey = { version = "=2.0.0-beta.1", features = ["metric_lines_lf", "metric_utf16"] }
|
ropey = { version = "=2.0.0-beta.1", features = ["metric_lines_lf", "metric_utf16"] }
|
||||||
sum_tree = { git = "https://github.com/zed-industries/zed" }
|
sum_tree = { git = "https://github.com/zed-industries/zed" }
|
||||||
tree-sitter = "0.26"
|
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
tree-sitter = "0.26"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
|||||||
@@ -214,6 +214,8 @@ impl Dock {
|
|||||||
pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.open = open;
|
self.open = open;
|
||||||
let item = self.panel.clone();
|
let item = self.panel.clone();
|
||||||
|
// Use defer_in (not window.defer) so the callback is cancelled
|
||||||
|
// if this Dock entity is dropped before the deferred frame runs.
|
||||||
cx.defer_in(window, move |_, window, cx| {
|
cx.defer_in(window, move |_, window, cx| {
|
||||||
item.set_collapsed(!open, window, cx);
|
item.set_collapsed(!open, window, cx);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations, Edges, Entity,
|
AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges, Entity, EntityId,
|
||||||
EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _,
|
EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _, Pixels,
|
||||||
Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div, px,
|
Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div, px,
|
||||||
};
|
};
|
||||||
use theme::CLIENT_SIDE_DECORATION_ROUNDING;
|
|
||||||
|
|
||||||
use crate::ElementExt;
|
use crate::ElementExt;
|
||||||
|
|
||||||
@@ -110,15 +109,8 @@ impl DockItem {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut items = items;
|
|
||||||
|
|
||||||
let stack_panel = cx.new(|cx| {
|
let stack_panel = cx.new(|cx| {
|
||||||
let mut stack_panel = StackPanel::new(axis, window, cx);
|
let mut stack_panel = StackPanel::new(axis, window, cx);
|
||||||
for (i, item) in items.iter_mut().enumerate() {
|
|
||||||
let view = item.view();
|
|
||||||
let size = sizes.get(i).copied().flatten();
|
|
||||||
stack_panel.add_panel(view.clone(), size, dock_area.clone(), window, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i, item) in items.iter().enumerate() {
|
for (i, item) in items.iter().enumerate() {
|
||||||
let view = item.view();
|
let view = item.view();
|
||||||
@@ -745,34 +737,22 @@ impl EventEmitter<DockEvent> for DockArea {}
|
|||||||
impl Render for DockArea {
|
impl Render for DockArea {
|
||||||
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 view = cx.entity().clone();
|
let view = cx.entity().clone();
|
||||||
let decorations = window.window_decorations();
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id("dock-area")
|
.id("dock-area")
|
||||||
.relative()
|
.relative()
|
||||||
.size_full()
|
.size_full()
|
||||||
.overflow_hidden()
|
|
||||||
.on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds))
|
.on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds))
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if let Some(zoom_view) = self.zoom_view.clone() {
|
if let Some(zoom_view) = self.zoom_view.clone() {
|
||||||
this.map(|this| match decorations {
|
this.child(zoom_view)
|
||||||
Decorations::Server => this,
|
|
||||||
Decorations::Client { tiling } => this
|
|
||||||
.when(!(tiling.top || tiling.right), |div| {
|
|
||||||
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
||||||
})
|
|
||||||
.when(!(tiling.top || tiling.left), |div| {
|
|
||||||
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.child(zoom_view)
|
|
||||||
} else {
|
} else {
|
||||||
// render dock
|
// render dock
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_row()
|
.flex_row()
|
||||||
.h_full()
|
.size_full()
|
||||||
// Left dock
|
// Left dock
|
||||||
.when_some(self.left_dock.clone(), |this, dock| {
|
.when_some(self.left_dock.clone(), |this, dock| {
|
||||||
this.child(div().flex().flex_none().child(dock))
|
this.child(div().flex().flex_none().child(dock))
|
||||||
@@ -783,14 +763,8 @@ impl Render for DockArea {
|
|||||||
.flex()
|
.flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.overflow_hidden()
|
|
||||||
// Top center
|
// Top center
|
||||||
.child(
|
.child(div().flex_1().child(self.render_items(window, cx)))
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(self.render_items(window, cx)),
|
|
||||||
)
|
|
||||||
// Bottom Dock
|
// Bottom Dock
|
||||||
.when_some(self.bottom_dock.clone(), |this, dock| {
|
.when_some(self.bottom_dock.clone(), |this, dock| {
|
||||||
this.child(dock)
|
this.child(dock)
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
App, AppContext, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
IntoElement, ParentElement, Pixels, Render, SharedString, Styled, Subscription, WeakEntity,
|
IntoElement, ParentElement, Pixels, Render, SharedString, Styled, Subscription, WeakEntity,
|
||||||
Window,
|
Window,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use theme::{ActiveTheme, AxisExt as _, CLIENT_SIDE_DECORATION_ROUNDING, Placement};
|
use theme::{AxisExt as _, Placement};
|
||||||
|
|
||||||
use super::{DockArea, PanelEvent};
|
use super::{DockArea, PanelEvent};
|
||||||
use crate::dock::panel::{Panel, PanelView};
|
use crate::dock::panel::{Panel, PanelView};
|
||||||
@@ -369,18 +368,12 @@ impl Focusable for StackPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for StackPanel {}
|
impl EventEmitter<PanelEvent> for StackPanel {}
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for StackPanel {}
|
impl EventEmitter<DismissEvent> for StackPanel {}
|
||||||
|
|
||||||
impl Render for StackPanel {
|
impl Render for StackPanel {
|
||||||
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 {
|
||||||
h_flex()
|
h_flex().size_full().overflow_hidden().child(
|
||||||
.size_full()
|
|
||||||
.overflow_hidden()
|
|
||||||
.bg(cx.theme().panel_background)
|
|
||||||
.when(cx.theme().platform.is_linux(), |this| {
|
|
||||||
this.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
ResizablePanelGroup::new("stack-panel-group")
|
ResizablePanelGroup::new("stack-panel-group")
|
||||||
.with_state(&self.state)
|
.with_state(&self.state)
|
||||||
.axis(self.axis)
|
.axis(self.axis)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use gpui::{
|
|||||||
ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
|
ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
|
||||||
WeakEntity, Window, div, px, rems,
|
WeakEntity, Window, div, px, rems,
|
||||||
};
|
};
|
||||||
use theme::{ActiveTheme, AxisExt, CLIENT_SIDE_DECORATION_ROUNDING, Placement, TABBAR_HEIGHT};
|
use theme::{ActiveTheme, AxisExt, Placement, TABBAR_HEIGHT};
|
||||||
|
|
||||||
use crate::button::{Button, ButtonVariants as _};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
use crate::dock::dock::DockPlacement;
|
use crate::dock::dock::DockPlacement;
|
||||||
@@ -567,6 +567,7 @@ impl TabPanel {
|
|||||||
let left_dock_button = self.render_dock_toggle_button(DockPlacement::Left, window, cx);
|
let left_dock_button = self.render_dock_toggle_button(DockPlacement::Left, window, cx);
|
||||||
let bottom_dock_button = self.render_dock_toggle_button(DockPlacement::Bottom, window, cx);
|
let bottom_dock_button = self.render_dock_toggle_button(DockPlacement::Bottom, window, cx);
|
||||||
let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx);
|
let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx);
|
||||||
|
|
||||||
let has_extend_dock_button = left_dock_button.is_some() || bottom_dock_button.is_some();
|
let has_extend_dock_button = left_dock_button.is_some() || bottom_dock_button.is_some();
|
||||||
let tabs_count = self.panels.len();
|
let tabs_count = self.panels.len();
|
||||||
let is_bottom_dock = bottom_dock_button.is_some();
|
let is_bottom_dock = bottom_dock_button.is_some();
|
||||||
@@ -586,6 +587,7 @@ impl TabPanel {
|
|||||||
.py_2()
|
.py_2()
|
||||||
.pl_3()
|
.pl_3()
|
||||||
.pr_2()
|
.pr_2()
|
||||||
|
.rounded_t(cx.theme().radius_lg)
|
||||||
.bg(cx.theme().panel_background)
|
.bg(cx.theme().panel_background)
|
||||||
.when(left_dock_button.is_some(), |this| this.pl_2())
|
.when(left_dock_button.is_some(), |this| this.pl_2())
|
||||||
.when(right_dock_button.is_some(), |this| this.pr_2())
|
.when(right_dock_button.is_some(), |this| this.pr_2())
|
||||||
@@ -641,17 +643,14 @@ impl TabPanel {
|
|||||||
TabBar::new("tab-bar")
|
TabBar::new("tab-bar")
|
||||||
.track_scroll(&self.tab_bar_scroll_handle)
|
.track_scroll(&self.tab_bar_scroll_handle)
|
||||||
.h(TABBAR_HEIGHT)
|
.h(TABBAR_HEIGHT)
|
||||||
|
.bg(cx.theme().panel_background)
|
||||||
|
.rounded_t(cx.theme().radius_lg)
|
||||||
.when(has_extend_dock_button, |this| {
|
.when(has_extend_dock_button, |this| {
|
||||||
this.prefix(
|
this.prefix(
|
||||||
h_flex()
|
h_flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.top_0()
|
.top_0()
|
||||||
.right(-px(1.))
|
.right(-px(1.))
|
||||||
.border_r_1()
|
|
||||||
.border_b_1()
|
|
||||||
.h_full()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.bg(cx.theme().tab_background)
|
|
||||||
.pl_0p5()
|
.pl_0p5()
|
||||||
.pr_1()
|
.pr_1()
|
||||||
.children(left_dock_button)
|
.children(left_dock_button)
|
||||||
@@ -689,6 +688,7 @@ impl TabPanel {
|
|||||||
let panel = panel.clone();
|
let panel = panel.clone();
|
||||||
move |view, _ev, window, cx| {
|
move |view, _ev, window, cx| {
|
||||||
view.remove_panel(&panel, window, cx);
|
view.remove_panel(&panel, window, cx);
|
||||||
|
view.set_active_ix(ix, window, cx);
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
@@ -780,12 +780,8 @@ impl TabPanel {
|
|||||||
.top_0()
|
.top_0()
|
||||||
.right_0()
|
.right_0()
|
||||||
.h_full()
|
.h_full()
|
||||||
.border_l_1()
|
|
||||||
.border_b_1()
|
|
||||||
.px_0p5()
|
.px_0p5()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.bg(cx.theme().tab_background)
|
|
||||||
.child(self.render_toolbar(state, window, cx))
|
.child(self.render_toolbar(state, window, cx))
|
||||||
.when_some(right_dock_button, |this, btn| this.child(btn)),
|
.when_some(right_dock_button, |this, btn| this.child(btn)),
|
||||||
)
|
)
|
||||||
@@ -815,10 +811,8 @@ impl TabPanel {
|
|||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.size_full()
|
.size_full()
|
||||||
|
.rounded_b(cx.theme().radius_lg)
|
||||||
.bg(cx.theme().panel_background)
|
.bg(cx.theme().panel_background)
|
||||||
.when(cx.theme().platform.is_linux(), |this| {
|
|
||||||
this.rounded_b(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
||||||
})
|
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(
|
.child(
|
||||||
active_panel
|
active_panel
|
||||||
@@ -1140,17 +1134,24 @@ impl Render for TabPanel {
|
|||||||
state.closable = false;
|
state.closable = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
v_flex()
|
div()
|
||||||
.when(!self.collapsed, |this| {
|
.when(!self.collapsed, |this| {
|
||||||
this.on_action(cx.listener(Self::on_action_toggle_zoom))
|
this.on_action(cx.listener(Self::on_action_toggle_zoom))
|
||||||
.on_action(cx.listener(Self::on_action_close_panel))
|
.on_action(cx.listener(Self::on_action_close_panel))
|
||||||
})
|
})
|
||||||
.id("tab-panel")
|
.id("tab-panel")
|
||||||
.tab_group()
|
|
||||||
.track_focus(&focus_handle)
|
.track_focus(&focus_handle)
|
||||||
.size_full()
|
.size_full()
|
||||||
|
.p_1()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.rounded(cx.theme().radius_lg)
|
||||||
|
.when(cx.theme().shadow, |this| this.shadow_xs())
|
||||||
|
.size_full()
|
||||||
|
.tab_group()
|
||||||
.child(self.render_title_bar(&state, window, cx))
|
.child(self.render_title_bar(&state, window, cx))
|
||||||
.child(self.render_active_panel(&state, window, cx))
|
.child(self.render_active_panel(&state, window, cx)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::time::{Duration, Instant};
|
use instant::{Duration, Instant};
|
||||||
|
|
||||||
/// A HistoryItem represents a single change in the history.
|
/// A HistoryItem represents a single change in the history.
|
||||||
/// It must implement Clone and PartialEq to be used in the History.
|
/// It must implement Clone and PartialEq to be used in the History.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use gpui::{Context, Pixels, Task, px};
|
use gpui::{Context, Pixels, Task, px};
|
||||||
|
|
||||||
|
|||||||
@@ -1,202 +1,61 @@
|
|||||||
/// DisplayMap: Public facade for Editor/Input display mapping.
|
|
||||||
///
|
|
||||||
/// This combines WrapMap and FoldMap to provide a unified API:
|
|
||||||
/// - BufferPoint ↔ DisplayPoint conversion
|
|
||||||
/// - Fold management (candidates, toggle, query)
|
|
||||||
/// - Automatic projection updates on text/layout changes
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use gpui::{App, Font, Pixels};
|
use gpui::{App, Font, Pixels};
|
||||||
use ropey::Rope;
|
use ropey::Rope;
|
||||||
|
|
||||||
use super::fold_map::FoldMap;
|
|
||||||
use super::folding::FoldRange;
|
|
||||||
use super::text_wrapper::{LineItem, WrapDisplayPoint};
|
use super::text_wrapper::{LineItem, WrapDisplayPoint};
|
||||||
use super::wrap_map::WrapMap;
|
use super::wrap_map::WrapMap;
|
||||||
use super::{BufferPoint, DisplayPoint};
|
|
||||||
use crate::input::Point as TreeSitterPoint;
|
use crate::input::Point as TreeSitterPoint;
|
||||||
use crate::input::display_map::WrapPoint;
|
|
||||||
use crate::input::rope_ext::RopeExt as _;
|
|
||||||
|
|
||||||
/// DisplayMap is the main interface for Editor/Input coordinate mapping.
|
/// DisplayMap is the main interface for Input coordinate mapping.
|
||||||
///
|
|
||||||
/// It manages the two-layer projection:
|
|
||||||
/// 1. Buffer → Wrap (soft-wrapping)
|
|
||||||
/// 2. Wrap → Display (folding)
|
|
||||||
///
|
|
||||||
/// Editor/Input only needs to work with BufferPoint and DisplayPoint.
|
|
||||||
pub struct DisplayMap {
|
pub struct DisplayMap {
|
||||||
wrap_map: WrapMap,
|
wrap_map: WrapMap,
|
||||||
fold_map: FoldMap,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayMap {
|
impl DisplayMap {
|
||||||
pub fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
pub fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
wrap_map: WrapMap::new(font, font_size, wrap_width),
|
wrap_map: WrapMap::new(font, font_size, wrap_width),
|
||||||
fold_map: FoldMap::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Core Coordinate Mapping ====================
|
/// Get total number of display rows (same as wrap rows without folding)
|
||||||
|
|
||||||
/// Convert buffer position to display position
|
|
||||||
pub fn buffer_pos_to_display_pos(&self, pos: BufferPoint) -> DisplayPoint {
|
|
||||||
// Buffer → Wrap
|
|
||||||
let wrap_pos = self.wrap_map.buffer_pos_to_wrap_pos(pos);
|
|
||||||
|
|
||||||
// Wrap → Display
|
|
||||||
if let Some(display_row) = self.fold_map.wrap_row_to_display_row(wrap_pos.row) {
|
|
||||||
DisplayPoint::new(display_row, wrap_pos.col)
|
|
||||||
} else {
|
|
||||||
// Cursor is in a folded region, find nearest visible row
|
|
||||||
let display_row = self.fold_map.nearest_visible_display_row(wrap_pos.row);
|
|
||||||
DisplayPoint::new(display_row, 0) // Column 0 at fold boundary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert display position to buffer position
|
|
||||||
pub fn display_pos_to_buffer_pos(&self, pos: DisplayPoint) -> BufferPoint {
|
|
||||||
// Display → Wrap
|
|
||||||
let wrap_row = self.fold_map.display_row_to_wrap_row(pos.row).unwrap_or(0);
|
|
||||||
|
|
||||||
// Wrap → Buffer
|
|
||||||
let wrap_pos = WrapPoint::new(wrap_row, pos.col);
|
|
||||||
self.wrap_map.wrap_pos_to_buffer_pos(wrap_pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get total number of visible display rows
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn display_row_count(&self) -> usize {
|
pub fn display_row_count(&self) -> usize {
|
||||||
self.fold_map.display_row_count()
|
self.wrap_map.wrap_row_count()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the buffer line for a given display row
|
/// Get the buffer line for a given display row
|
||||||
pub fn display_row_to_buffer_line(&self, display_row: usize) -> usize {
|
pub fn display_row_to_buffer_line(&self, display_row: usize) -> usize {
|
||||||
// Display → Wrap
|
self.wrap_map.wrap_row_to_buffer_line(display_row)
|
||||||
let wrap_row = self
|
|
||||||
.fold_map
|
|
||||||
.display_row_to_wrap_row(display_row)
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
// Wrap → Buffer line
|
|
||||||
self.wrap_map.wrap_row_to_buffer_line(wrap_row)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the display row range for a buffer line: [start, end)
|
/// Get the display row range for a buffer line: [start, end)
|
||||||
/// Returns None if the buffer line is completely hidden
|
|
||||||
pub fn buffer_line_to_display_row_range(&self, line: usize) -> Option<Range<usize>> {
|
pub fn buffer_line_to_display_row_range(&self, line: usize) -> Option<Range<usize>> {
|
||||||
// Buffer line → Wrap row range
|
let range = self.wrap_map.buffer_line_to_wrap_row_range(line);
|
||||||
let wrap_row_range = self.wrap_map.buffer_line_to_wrap_row_range(line);
|
if range.is_empty() { None } else { Some(range) }
|
||||||
|
|
||||||
// Find first and last visible display rows in this range
|
|
||||||
let mut first_display_row = None;
|
|
||||||
let mut last_display_row = None;
|
|
||||||
|
|
||||||
for wrap_row in wrap_row_range {
|
|
||||||
if let Some(display_row) = self.fold_map.wrap_row_to_display_row(wrap_row) {
|
|
||||||
if first_display_row.is_none() {
|
|
||||||
first_display_row = Some(display_row);
|
|
||||||
}
|
|
||||||
last_display_row = Some(display_row);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (Some(start), Some(end)) = (first_display_row, last_display_row) {
|
/// Check if a buffer line is completely hidden (never true without folding)
|
||||||
Some(start..end + 1)
|
|
||||||
} else {
|
|
||||||
None // Completely folded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a buffer line is completely hidden
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_buffer_line_hidden(&self, line: usize) -> bool {
|
pub fn is_buffer_line_hidden(&self, _line: usize) -> bool {
|
||||||
self.buffer_line_to_display_row_range(line).is_none()
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set fold candidates (from tree-sitter/LSP)
|
/// All wrap rows are visible since there's no folding.
|
||||||
pub fn set_fold_candidates(&mut self, candidates: Vec<FoldRange>) {
|
|
||||||
self.fold_map.set_candidates(candidates);
|
|
||||||
self.rebuild_fold_projection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a fold at the given start_line (must be in candidates)
|
|
||||||
pub fn set_folded(&mut self, start_line: usize, folded: bool) {
|
|
||||||
self.fold_map.set_folded(start_line, folded);
|
|
||||||
self.rebuild_fold_projection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggle fold at the given start_line
|
|
||||||
pub fn toggle_fold(&mut self, start_line: usize) {
|
|
||||||
self.fold_map.toggle_fold(start_line);
|
|
||||||
self.rebuild_fold_projection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a line is currently folded
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_folded_at(&self, start_line: usize) -> bool {
|
pub fn folded_ranges(&self) -> &[()] {
|
||||||
self.fold_map.is_folded_at(start_line)
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a line is a fold candidate
|
/// Adjust folds for edit (no-op without folding)
|
||||||
#[inline]
|
pub fn adjust_folds_for_edit(
|
||||||
pub fn is_fold_candidate(&self, start_line: usize) -> bool {
|
|
||||||
self.fold_map.is_fold_candidate(start_line)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all currently folded ranges
|
|
||||||
#[inline]
|
|
||||||
pub fn folded_ranges(&self) -> &[FoldRange] {
|
|
||||||
self.fold_map.folded_ranges()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear all folds
|
|
||||||
pub fn clear_folds(&mut self) {
|
|
||||||
self.fold_map.clear_folds();
|
|
||||||
self.rebuild_fold_projection();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Text and Layout Updates ====================
|
|
||||||
|
|
||||||
/// Adjust folds and candidates for a text edit before updating the wrap map.
|
|
||||||
///
|
|
||||||
/// Must be called with the OLD text (before replacement) and the edit range/new_text
|
|
||||||
/// so we can compute which old lines were affected.
|
|
||||||
pub fn adjust_folds_for_edit(&mut self, old_text: &Rope, range: &Range<usize>, new_text: &str) {
|
|
||||||
if self.fold_map.folded_ranges().is_empty() && self.fold_map.fold_candidates().is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let edit_start_line = old_text.offset_to_point(range.start).row;
|
|
||||||
let edit_end_line = old_text.offset_to_point(range.end.min(old_text.len())).row;
|
|
||||||
|
|
||||||
let old_lines_in_range = edit_end_line.saturating_sub(edit_start_line);
|
|
||||||
let new_lines_in_range = new_text.chars().filter(|c| *c == '\n').count();
|
|
||||||
let line_delta = new_lines_in_range as isize - old_lines_in_range as isize;
|
|
||||||
|
|
||||||
self.fold_map
|
|
||||||
.adjust_folds_for_edit(edit_start_line, edit_end_line, line_delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Incrementally update fold candidates after a text edit.
|
|
||||||
///
|
|
||||||
/// Extracts new fold candidates only within the edited byte range
|
|
||||||
/// and merges them with existing (already adjusted) candidates.
|
|
||||||
pub fn update_fold_candidates_for_edit(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
tree: &super::folding::Tree,
|
_old_text: &Rope,
|
||||||
edit_byte_range: Range<usize>,
|
_range: &Range<usize>,
|
||||||
new_text: &Rope,
|
_new_text: &str,
|
||||||
) {
|
) {
|
||||||
let new_start_line = new_text.offset_to_point(edit_byte_range.start).row;
|
// No-op: no folding
|
||||||
let new_end_line = new_text
|
|
||||||
.offset_to_point(edit_byte_range.end.min(new_text.len()))
|
|
||||||
.row;
|
|
||||||
|
|
||||||
let new_candidates = super::folding::extract_fold_ranges_in_range(tree, edit_byte_range);
|
|
||||||
self.fold_map
|
|
||||||
.merge_candidates_for_edit(new_start_line, new_end_line, new_candidates);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update text (incremental or full)
|
/// Update text (incremental or full)
|
||||||
@@ -209,52 +68,28 @@ impl DisplayMap {
|
|||||||
) {
|
) {
|
||||||
self.wrap_map
|
self.wrap_map
|
||||||
.on_text_changed(changed_text, range, new_text, cx);
|
.on_text_changed(changed_text, range, new_text, cx);
|
||||||
self.rebuild_fold_projection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update layout parameters (wrap width or font)
|
/// Update layout parameters (wrap width or font)
|
||||||
pub fn on_layout_changed(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
pub fn on_layout_changed(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||||
self.wrap_map.on_layout_changed(wrap_width, cx);
|
self.wrap_map.on_layout_changed(wrap_width, cx);
|
||||||
self.rebuild_fold_projection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set font parameters
|
/// Set font parameters
|
||||||
pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
||||||
self.wrap_map.set_font(font, font_size, cx);
|
self.wrap_map.set_font(font, font_size, cx);
|
||||||
self.rebuild_fold_projection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure text is prepared (initializes wrapper if needed)
|
/// Ensure text is prepared (initializes wrapper if needed)
|
||||||
pub fn ensure_text_prepared(&mut self, text: &Rope, cx: &mut App) {
|
pub fn ensure_text_prepared(&mut self, text: &Rope, cx: &mut App) {
|
||||||
let did_initialize = self.wrap_map.ensure_text_prepared(text, cx);
|
self.wrap_map.ensure_text_prepared(text, cx);
|
||||||
if did_initialize {
|
|
||||||
self.rebuild_fold_projection();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize with text
|
/// Initialize with text
|
||||||
pub fn set_text(&mut self, text: &Rope, cx: &mut App) {
|
pub fn set_text(&mut self, text: &Rope, cx: &mut App) {
|
||||||
self.wrap_map.set_text(text, cx);
|
self.wrap_map.set_text(text, cx);
|
||||||
self.rebuild_fold_projection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Internal Helpers ====================
|
|
||||||
|
|
||||||
/// Rebuild fold projection after wrap_map or fold state changes
|
|
||||||
/// Only rebuilds if there are actually folded ranges
|
|
||||||
fn rebuild_fold_projection(&mut self) {
|
|
||||||
if !self.fold_map.folded_ranges().is_empty() {
|
|
||||||
self.fold_map.rebuild(&self.wrap_map);
|
|
||||||
} else {
|
|
||||||
// No active folds: identity mapping (wrap_row == display_row).
|
|
||||||
// Just update cached count so query methods work without Vec allocation.
|
|
||||||
self.fold_map
|
|
||||||
.mark_dirty_with_wrap_count(self.wrap_map.wrap_row_count());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Wrap Display Point Operations ====================
|
|
||||||
|
|
||||||
/// Convert byte offset to wrap display point (with soft wrap info).
|
/// Convert byte offset to wrap display point (with soft wrap info).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn offset_to_wrap_display_point(&self, offset: usize) -> WrapDisplayPoint {
|
pub(crate) fn offset_to_wrap_display_point(&self, offset: usize) -> WrapDisplayPoint {
|
||||||
@@ -273,23 +108,30 @@ impl DisplayMap {
|
|||||||
self.wrap_map.wrapper().display_point_to_point(point)
|
self.wrap_map.wrapper().display_point_to_point(point)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a wrap row to a display row (skipping folded rows).
|
/// Since there's no folding, wrap row == display row.
|
||||||
/// Returns None if the wrap row is folded.
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option<usize> {
|
pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option<usize> {
|
||||||
self.fold_map.wrap_row_to_display_row(wrap_row)
|
if wrap_row < self.wrap_row_count() {
|
||||||
|
Some(wrap_row)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the nearest visible display row for a given wrap row.
|
/// Since there's no folding, nearest visible row is the row itself.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn nearest_visible_display_row(&self, wrap_row: usize) -> usize {
|
pub fn nearest_visible_display_row(&self, wrap_row: usize) -> usize {
|
||||||
self.fold_map.nearest_visible_display_row(wrap_row)
|
wrap_row.min(self.wrap_row_count().saturating_sub(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a display row to a wrap row.
|
/// Since there's no folding, display row == wrap row.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option<usize> {
|
pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option<usize> {
|
||||||
self.fold_map.display_row_to_wrap_row(display_row)
|
if display_row < self.wrap_row_count() {
|
||||||
|
Some(display_row)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the longest row index (by byte length).
|
/// Get the longest row index (by byte length).
|
||||||
@@ -298,8 +140,6 @@ impl DisplayMap {
|
|||||||
self.wrap_map.wrapper().longest_row.row
|
self.wrap_map.wrapper().longest_row.row
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Access Methods ====================
|
|
||||||
|
|
||||||
/// Get access to line items (for rendering)
|
/// Get access to line items (for rendering)
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn lines(&self) -> &[LineItem] {
|
pub(crate) fn lines(&self) -> &[LineItem] {
|
||||||
@@ -312,14 +152,13 @@ impl DisplayMap {
|
|||||||
self.wrap_map.text()
|
self.wrap_map.text()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate how many wrap rows of a buffer line are visible (not folded)
|
/// Calculate how many wrap rows of a buffer line are visible
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize {
|
pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize {
|
||||||
self.wrap_map
|
self.wrap_map.visible_wrap_row_count_for_buffer_line(line)
|
||||||
.visible_wrap_row_count_for_line(line, &self.fold_map)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the wrap row count (before folding)
|
/// Get the wrap row count
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn wrap_row_count(&self) -> usize {
|
pub fn wrap_row_count(&self) -> usize {
|
||||||
self.wrap_map.wrap_row_count()
|
self.wrap_map.wrap_row_count()
|
||||||
|
|||||||
@@ -1,343 +0,0 @@
|
|||||||
/// FoldMap: Folding projection layer (Wrap rows → Display rows).
|
|
||||||
///
|
|
||||||
/// This module manages code folding by:
|
|
||||||
/// - Filtering out wrap rows that belong to folded regions
|
|
||||||
/// - Maintaining bidirectional mapping: wrap_row ↔ display_row
|
|
||||||
/// - Handling fold state changes and rebuilding the projection
|
|
||||||
use super::folding::FoldRange;
|
|
||||||
use super::wrap_map::WrapMap;
|
|
||||||
|
|
||||||
/// FoldMap projects wrap rows to display rows by hiding folded regions.
|
|
||||||
pub struct FoldMap {
|
|
||||||
/// Mapping: display_row → wrap_row
|
|
||||||
/// index = display_row, value = actual wrap_row
|
|
||||||
visible_wrap_rows: Vec<usize>,
|
|
||||||
|
|
||||||
/// Reverse mapping: wrap_row → display_row
|
|
||||||
/// index = wrap_row, value = Some(display_row) if visible, None if folded
|
|
||||||
wrap_row_to_display_row: Vec<Option<usize>>,
|
|
||||||
|
|
||||||
/// Candidate fold ranges (from tree-sitter/LSP)
|
|
||||||
/// Sorted by start_line, unique start_line
|
|
||||||
candidates: Vec<FoldRange>,
|
|
||||||
|
|
||||||
/// Currently folded ranges
|
|
||||||
/// Subset of candidates, sorted by start_line
|
|
||||||
folded: Vec<FoldRange>,
|
|
||||||
|
|
||||||
/// Flag indicating if the fold projection needs rebuilding
|
|
||||||
/// Used for lazy evaluation to avoid expensive rebuilds on every text change
|
|
||||||
needs_rebuild: bool,
|
|
||||||
|
|
||||||
/// Cached wrap_row_count from last rebuild
|
|
||||||
/// Used to detect if WrapMap changed and rebuild is needed
|
|
||||||
cached_wrap_row_count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FoldMap {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
visible_wrap_rows: Vec::new(),
|
|
||||||
wrap_row_to_display_row: Vec::new(),
|
|
||||||
candidates: Vec::new(),
|
|
||||||
folded: Vec::new(),
|
|
||||||
needs_rebuild: true,
|
|
||||||
cached_wrap_row_count: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update cached wrap_row_count without full rebuild.
|
|
||||||
/// Used when no folds are active (identity mapping assumed).
|
|
||||||
pub(super) fn mark_dirty_with_wrap_count(&mut self, wrap_row_count: usize) {
|
|
||||||
self.needs_rebuild = true;
|
|
||||||
self.cached_wrap_row_count = wrap_row_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get total number of visible display rows
|
|
||||||
pub fn display_row_count(&self) -> usize {
|
|
||||||
if self.folded.is_empty() {
|
|
||||||
return self.cached_wrap_row_count;
|
|
||||||
}
|
|
||||||
self.visible_wrap_rows.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert wrap_row to display_row
|
|
||||||
/// Returns None if the wrap_row is hidden by folding
|
|
||||||
pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option<usize> {
|
|
||||||
if self.folded.is_empty() {
|
|
||||||
return if wrap_row < self.cached_wrap_row_count {
|
|
||||||
Some(wrap_row)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
}
|
|
||||||
self.wrap_row_to_display_row
|
|
||||||
.get(wrap_row)
|
|
||||||
.copied()
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert display_row to wrap_row
|
|
||||||
pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option<usize> {
|
|
||||||
if self.folded.is_empty() {
|
|
||||||
return if display_row < self.cached_wrap_row_count {
|
|
||||||
Some(display_row)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
}
|
|
||||||
self.visible_wrap_rows.get(display_row).copied()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find the nearest visible display_row for a given wrap_row
|
|
||||||
pub fn nearest_visible_display_row(&self, wrap_row: usize) -> usize {
|
|
||||||
if self.folded.is_empty() {
|
|
||||||
return wrap_row.min(self.cached_wrap_row_count.saturating_sub(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(dr) = self.wrap_row_to_display_row(wrap_row) {
|
|
||||||
return dr;
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.visible_wrap_rows.binary_search(&wrap_row) {
|
|
||||||
Ok(idx) => idx,
|
|
||||||
Err(insert_pos) => insert_pos.saturating_sub(1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set fold candidates (from tree-sitter/LSP), full replacement.
|
|
||||||
pub fn set_candidates(&mut self, mut candidates: Vec<FoldRange>) {
|
|
||||||
// Sort and deduplicate by start_line
|
|
||||||
candidates.sort_by_key(|r| r.start_line);
|
|
||||||
candidates.dedup_by_key(|r| r.start_line);
|
|
||||||
self.candidates = candidates;
|
|
||||||
|
|
||||||
// Remove any folded ranges that are no longer in candidates
|
|
||||||
self.folded.retain(|fold| {
|
|
||||||
self.candidates
|
|
||||||
.iter()
|
|
||||||
.any(|c| c.start_line == fold.start_line)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Merge new candidates extracted from an edited region into existing candidates.
|
|
||||||
///
|
|
||||||
/// Replaces candidates within [edit_start_line, edit_end_line] with `new_candidates`,
|
|
||||||
/// keeping candidates outside the edit range intact.
|
|
||||||
pub fn merge_candidates_for_edit(
|
|
||||||
&mut self,
|
|
||||||
edit_start_line: usize,
|
|
||||||
edit_end_line: usize,
|
|
||||||
new_candidates: Vec<FoldRange>,
|
|
||||||
) {
|
|
||||||
// Remove old candidates within the edit range (already done by adjust_folds_for_edit)
|
|
||||||
// But do it again in case adjust wasn't called or range differs
|
|
||||||
self.candidates
|
|
||||||
.retain(|c| c.start_line < edit_start_line || c.start_line > edit_end_line);
|
|
||||||
|
|
||||||
// Add new candidates
|
|
||||||
self.candidates.extend(new_candidates);
|
|
||||||
self.candidates.sort_by_key(|r| r.start_line);
|
|
||||||
self.candidates.dedup_by_key(|r| r.start_line);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a fold at the given start_line (must be in candidates)
|
|
||||||
pub fn set_folded(&mut self, start_line: usize, folded: bool) {
|
|
||||||
if folded {
|
|
||||||
// Find the candidate range for this start_line
|
|
||||||
if let Some(candidate) = self.candidates.iter().find(|c| c.start_line == start_line) {
|
|
||||||
// Add to folded if not already present
|
|
||||||
if !self.folded.iter().any(|f| f.start_line == start_line) {
|
|
||||||
self.folded.push(*candidate);
|
|
||||||
self.folded.sort_by_key(|r| r.start_line);
|
|
||||||
self.needs_rebuild = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Remove from folded
|
|
||||||
self.folded.retain(|f| f.start_line != start_line);
|
|
||||||
self.needs_rebuild = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggle fold at the given start_line
|
|
||||||
pub fn toggle_fold(&mut self, start_line: usize) {
|
|
||||||
let is_folded = self.is_folded_at(start_line);
|
|
||||||
self.set_folded(start_line, !is_folded);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a line is currently folded
|
|
||||||
pub fn is_folded_at(&self, start_line: usize) -> bool {
|
|
||||||
self.folded.iter().any(|f| f.start_line == start_line)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a line is a fold candidate
|
|
||||||
pub fn is_fold_candidate(&self, start_line: usize) -> bool {
|
|
||||||
self.candidates.iter().any(|c| c.start_line == start_line)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all fold candidates
|
|
||||||
#[inline]
|
|
||||||
pub fn fold_candidates(&self) -> &[FoldRange] {
|
|
||||||
&self.candidates
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all currently folded ranges
|
|
||||||
#[inline]
|
|
||||||
pub fn folded_ranges(&self) -> &[FoldRange] {
|
|
||||||
&self.folded
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear all folds
|
|
||||||
#[inline]
|
|
||||||
pub fn clear_folds(&mut self) {
|
|
||||||
self.folded.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adjust folds and candidates after a text edit.
|
|
||||||
///
|
|
||||||
/// - Folds/candidates overlapping the edited line range are removed
|
|
||||||
/// - Folds/candidates after the edit are shifted by line_delta
|
|
||||||
///
|
|
||||||
/// This avoids expensive full tree traversal on every keystroke.
|
|
||||||
pub fn adjust_folds_for_edit(
|
|
||||||
&mut self,
|
|
||||||
edit_start_line: usize,
|
|
||||||
edit_end_line: usize,
|
|
||||||
line_delta: isize,
|
|
||||||
) {
|
|
||||||
// Adjust folded ranges
|
|
||||||
if !self.folded.is_empty() {
|
|
||||||
self.folded.retain(|fold| {
|
|
||||||
!(fold.start_line <= edit_end_line && fold.end_line >= edit_start_line)
|
|
||||||
});
|
|
||||||
|
|
||||||
if line_delta != 0 {
|
|
||||||
for fold in &mut self.folded {
|
|
||||||
if fold.start_line > edit_end_line {
|
|
||||||
fold.start_line = (fold.start_line as isize + line_delta).max(0) as usize;
|
|
||||||
fold.end_line = (fold.end_line as isize + line_delta).max(0) as usize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust candidates the same way
|
|
||||||
if !self.candidates.is_empty() {
|
|
||||||
self.candidates
|
|
||||||
.retain(|c| !(c.start_line <= edit_end_line && c.end_line >= edit_start_line));
|
|
||||||
|
|
||||||
if line_delta != 0 {
|
|
||||||
for c in &mut self.candidates {
|
|
||||||
if c.start_line > edit_end_line {
|
|
||||||
c.start_line = (c.start_line as isize + line_delta).max(0) as usize;
|
|
||||||
c.end_line = (c.end_line as isize + line_delta).max(0) as usize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.needs_rebuild = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rebuild the fold mapping after wrap_map or fold state changes
|
|
||||||
///
|
|
||||||
/// This is the core algorithm that projects wrap rows to display rows.
|
|
||||||
pub fn rebuild(&mut self, wrap_map: &WrapMap) {
|
|
||||||
let wrap_row_count = wrap_map.wrap_row_count();
|
|
||||||
|
|
||||||
// Performance optimization: skip rebuild if nothing changed
|
|
||||||
if !self.needs_rebuild && wrap_row_count == self.cached_wrap_row_count {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cached_wrap_row_count = wrap_row_count;
|
|
||||||
|
|
||||||
self.visible_wrap_rows.clear();
|
|
||||||
self.wrap_row_to_display_row = vec![None; wrap_row_count];
|
|
||||||
|
|
||||||
if self.folded.is_empty() {
|
|
||||||
// Fast path: no folds, all wrap rows are visible
|
|
||||||
self.visible_wrap_rows = (0..wrap_row_count).collect();
|
|
||||||
for (display_row, &wrap_row) in self.visible_wrap_rows.iter().enumerate() {
|
|
||||||
self.wrap_row_to_display_row[wrap_row] = Some(display_row);
|
|
||||||
}
|
|
||||||
self.needs_rebuild = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build set of hidden wrap_row ranges from folded buffer lines
|
|
||||||
let mut hidden_ranges = Vec::new();
|
|
||||||
for fold in &self.folded {
|
|
||||||
// Hide wrap rows from (start_line + 1) to (end_line - 1) (inclusive)
|
|
||||||
// Both the first line and last line of the fold remain visible
|
|
||||||
let hide_start_line = fold.start_line + 1;
|
|
||||||
let hide_end_line = fold.end_line.saturating_sub(1);
|
|
||||||
|
|
||||||
if hide_start_line > hide_end_line {
|
|
||||||
continue; // No middle lines to hide (0 or 1 lines between start and end)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get wrap_row ranges for the hidden buffer lines
|
|
||||||
let start_wrap_row = wrap_map.buffer_line_to_first_wrap_row(hide_start_line);
|
|
||||||
let end_wrap_row = if hide_end_line + 1 < wrap_map.buffer_line_count() {
|
|
||||||
wrap_map.buffer_line_to_first_wrap_row(hide_end_line + 1)
|
|
||||||
} else {
|
|
||||||
wrap_row_count
|
|
||||||
};
|
|
||||||
|
|
||||||
if start_wrap_row < end_wrap_row {
|
|
||||||
hidden_ranges.push(start_wrap_row..end_wrap_row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge overlapping hidden ranges
|
|
||||||
hidden_ranges.sort_by_key(|r| r.start);
|
|
||||||
let mut merged_hidden = Vec::new();
|
|
||||||
for range in hidden_ranges {
|
|
||||||
if let Some(last) = merged_hidden.last_mut() {
|
|
||||||
if range.start <= *last {
|
|
||||||
// Overlapping or adjacent, merge
|
|
||||||
*last = (*last).max(range.end);
|
|
||||||
} else {
|
|
||||||
merged_hidden.push(range.start);
|
|
||||||
merged_hidden.push(range.end);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
merged_hidden.push(range.start);
|
|
||||||
merged_hidden.push(range.end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan all wrap rows and filter out hidden ones
|
|
||||||
let mut display_row = 0;
|
|
||||||
let mut hidden_iter = merged_hidden.chunks_exact(2);
|
|
||||||
let mut current_hidden = hidden_iter.next();
|
|
||||||
|
|
||||||
for wrap_row in 0..wrap_row_count {
|
|
||||||
// Check if wrap_row is in current hidden range
|
|
||||||
let is_hidden = if let Some(&[start, end]) = current_hidden {
|
|
||||||
if wrap_row >= end {
|
|
||||||
current_hidden = hidden_iter.next();
|
|
||||||
if let Some(&[new_start, new_end]) = current_hidden {
|
|
||||||
wrap_row >= new_start && wrap_row < new_end
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
wrap_row >= start && wrap_row < end
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_hidden {
|
|
||||||
self.visible_wrap_rows.push(wrap_row);
|
|
||||||
self.wrap_row_to_display_row[wrap_row] = Some(display_row);
|
|
||||||
display_row += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.needs_rebuild = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
use tree_sitter::Node;
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
pub use tree_sitter::Tree;
|
|
||||||
|
|
||||||
#[cfg(target_family = "wasm")]
|
|
||||||
/// Stub type for tree-sitter Tree on WASM (tree-sitter not available).
|
|
||||||
pub struct Tree;
|
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
/// Minimum line span for a node to be considered foldable.
|
|
||||||
const MIN_FOLD_LINES: usize = 2;
|
|
||||||
|
|
||||||
/// A fold range representing a foldable code region.
|
|
||||||
///
|
|
||||||
/// The fold range spans from start_line to end_line (inclusive).
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct FoldRange {
|
|
||||||
/// Start line (inclusive)
|
|
||||||
pub start_line: usize,
|
|
||||||
/// End line (inclusive)
|
|
||||||
pub end_line: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FoldRange {
|
|
||||||
pub fn new(start_line: usize, end_line: usize) -> Self {
|
|
||||||
assert!(
|
|
||||||
start_line <= end_line,
|
|
||||||
"fold start_line must be <= end_line"
|
|
||||||
);
|
|
||||||
Self {
|
|
||||||
start_line,
|
|
||||||
end_line,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
/// Check if a named node qualifies as a fold candidate.
|
|
||||||
///
|
|
||||||
/// Uses a structural heuristic: any **named** node spanning ≥ MIN_FOLD_LINES
|
|
||||||
/// is foldable. tree-sitter already parses code into semantic units (functions,
|
|
||||||
/// classes, blocks, etc.), so named nodes naturally correspond to meaningful
|
|
||||||
/// foldable regions across all languages without a per-language node-type list.
|
|
||||||
fn is_foldable_node(node: &Node) -> bool {
|
|
||||||
let start = node.start_position().row;
|
|
||||||
let end = node.end_position().row;
|
|
||||||
end.saturating_sub(start) >= MIN_FOLD_LINES
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
/// Extract fold ranges only within a byte range (for incremental updates after edits).
|
|
||||||
///
|
|
||||||
/// Skips subtrees entirely outside the range, making it O(nodes in range)
|
|
||||||
/// instead of O(all nodes in tree).
|
|
||||||
pub fn extract_fold_ranges_in_range(tree: &Tree, byte_range: Range<usize>) -> Vec<FoldRange> {
|
|
||||||
let mut ranges = Vec::new();
|
|
||||||
let root = tree.root_node();
|
|
||||||
let mut cursor = root.walk();
|
|
||||||
// Skip the root, it's not foldable. Use named_children to skip literal tokens.
|
|
||||||
for child in root.named_children(&mut cursor) {
|
|
||||||
collect_foldable_nodes_in_range(child, &byte_range, &mut ranges);
|
|
||||||
}
|
|
||||||
|
|
||||||
ranges.sort_by_key(|r| r.start_line);
|
|
||||||
ranges.dedup_by_key(|r| r.start_line);
|
|
||||||
ranges
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
/// Recursively collect foldable nodes, skipping subtrees outside byte_range.
|
|
||||||
fn collect_foldable_nodes_in_range(
|
|
||||||
node: Node,
|
|
||||||
byte_range: &Range<usize>,
|
|
||||||
ranges: &mut Vec<FoldRange>,
|
|
||||||
) {
|
|
||||||
if node.end_byte() <= byte_range.start || node.start_byte() >= byte_range.end {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !is_foldable_node(&node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ranges.push(FoldRange {
|
|
||||||
start_line: node.start_position().row,
|
|
||||||
end_line: node.end_position().row,
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut cursor = node.walk();
|
|
||||||
for child in node.named_children(&mut cursor) {
|
|
||||||
collect_foldable_nodes_in_range(child, byte_range, ranges);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +1,7 @@
|
|||||||
#[allow(clippy::module_inception)]
|
#[allow(clippy::module_inception)]
|
||||||
mod display_map;
|
mod display_map;
|
||||||
mod fold_map;
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
mod folding;
|
|
||||||
#[cfg(target_family = "wasm")]
|
|
||||||
pub mod folding;
|
|
||||||
mod text_wrapper;
|
mod text_wrapper;
|
||||||
mod wrap_map;
|
mod wrap_map;
|
||||||
|
|
||||||
// Re-export public API
|
|
||||||
// Re-export FoldRange and extract_fold_ranges
|
|
||||||
pub use folding::FoldRange;
|
|
||||||
|
|
||||||
pub use self::display_map::DisplayMap;
|
pub use self::display_map::DisplayMap;
|
||||||
pub(crate) use self::text_wrapper::LineLayout;
|
pub(crate) use self::text_wrapper::LineLayout;
|
||||||
|
|
||||||
/// Position in the buffer (logical text).
|
|
||||||
///
|
|
||||||
/// - `line`: 0-based logical line number (split by `\n`)
|
|
||||||
/// - `col`: 0-based column offset (byte offset)
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct BufferPoint {
|
|
||||||
pub line: usize,
|
|
||||||
pub col: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BufferPoint {
|
|
||||||
pub fn new(line: usize, col: usize) -> Self {
|
|
||||||
Self { line, col }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Position after soft-wrapping but before folding (internal).
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub(super) struct WrapPoint {
|
|
||||||
pub row: usize,
|
|
||||||
pub col: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WrapPoint {
|
|
||||||
pub fn new(row: usize, col: usize) -> Self {
|
|
||||||
Self { row, col }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Final display position (after soft-wrapping and folding).
|
|
||||||
///
|
|
||||||
/// - `row`: 0-based display row (final visible row)
|
|
||||||
/// - `col`: 0-based display column
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct DisplayPoint {
|
|
||||||
pub row: usize,
|
|
||||||
pub col: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DisplayPoint {
|
|
||||||
pub fn new(row: usize, col: usize) -> Self {
|
|
||||||
Self { row, col }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ use std::ops::Range;
|
|||||||
use gpui::{App, Font, Pixels};
|
use gpui::{App, Font, Pixels};
|
||||||
use ropey::Rope;
|
use ropey::Rope;
|
||||||
|
|
||||||
use super::fold_map::FoldMap;
|
use super::text_wrapper::{LineItem, TextWrapper};
|
||||||
use super::text_wrapper::{LineItem, TextWrapper, WrapDisplayPoint};
|
|
||||||
use super::{BufferPoint, WrapPoint};
|
|
||||||
use crate::input::rope_ext::RopeExt;
|
|
||||||
|
|
||||||
/// WrapMap manages soft-wrapping and provides buffer ↔ wrap coordinate mapping.
|
/// WrapMap manages soft-wrapping and provides buffer ↔ wrap coordinate mapping.
|
||||||
pub struct WrapMap {
|
pub struct WrapMap {
|
||||||
@@ -55,49 +52,6 @@ impl WrapMap {
|
|||||||
self.wrapper.lines.len()
|
self.wrapper.lines.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert buffer position to wrap position
|
|
||||||
pub(super) fn buffer_pos_to_wrap_pos(&self, pos: BufferPoint) -> WrapPoint {
|
|
||||||
let BufferPoint { line, col } = pos;
|
|
||||||
|
|
||||||
// Clamp to valid range
|
|
||||||
let line = line.min(self.buffer_line_count().saturating_sub(1));
|
|
||||||
let line_item = self.wrapper.lines.get(line);
|
|
||||||
|
|
||||||
let col = if let Some(line_item) = line_item {
|
|
||||||
col.min(line_item.len())
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate offset in rope
|
|
||||||
let line_start_offset = self.wrapper.text().line_start_offset(line);
|
|
||||||
let offset = line_start_offset + col;
|
|
||||||
|
|
||||||
// Use TextWrapper's existing conversion
|
|
||||||
let display_point = self.wrapper.offset_to_display_point(offset);
|
|
||||||
|
|
||||||
WrapPoint::new(display_point.row, display_point.column)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert wrap position to buffer position
|
|
||||||
pub(super) fn wrap_pos_to_buffer_pos(&self, pos: WrapPoint) -> BufferPoint {
|
|
||||||
let WrapPoint { row, col } = pos;
|
|
||||||
|
|
||||||
// Clamp wrap_row to valid range
|
|
||||||
let row = row.min(self.wrap_row_count().saturating_sub(1));
|
|
||||||
|
|
||||||
// Use TextWrapper's existing conversion
|
|
||||||
let display_point = WrapDisplayPoint::new(row, 0, col);
|
|
||||||
let offset = self.wrapper.display_point_to_offset(display_point);
|
|
||||||
|
|
||||||
// Convert offset to buffer position
|
|
||||||
let point = self.wrapper.text().offset_to_point(offset);
|
|
||||||
let line_start = self.wrapper.text().line_start_offset(point.row);
|
|
||||||
let col = offset.saturating_sub(line_start);
|
|
||||||
|
|
||||||
BufferPoint::new(point.row, col)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the buffer line for a given wrap row
|
/// Get the buffer line for a given wrap row
|
||||||
pub fn wrap_row_to_buffer_line(&self, wrap_row: usize) -> usize {
|
pub fn wrap_row_to_buffer_line(&self, wrap_row: usize) -> usize {
|
||||||
if wrap_row >= self.wrap_row_count() {
|
if wrap_row >= self.wrap_row_count() {
|
||||||
@@ -176,8 +130,6 @@ impl WrapMap {
|
|||||||
let wrap_row_count = self.wrapper.len();
|
let wrap_row_count = self.wrapper.len();
|
||||||
|
|
||||||
// Skip if nothing changed: both buffer line count and total wrap row count must match.
|
// Skip if nothing changed: both buffer line count and total wrap row count must match.
|
||||||
// Checking wrap_row_count is essential because soft-wrap can change the number of
|
|
||||||
// wrap rows per line without changing the buffer line count.
|
|
||||||
if line_count == self.cached_line_count
|
if line_count == self.cached_line_count
|
||||||
&& wrap_row_count == self.cached_wrap_row_count
|
&& wrap_row_count == self.cached_wrap_row_count
|
||||||
&& !self.buffer_line_starts.is_empty()
|
&& !self.buffer_line_starts.is_empty()
|
||||||
@@ -212,11 +164,9 @@ impl WrapMap {
|
|||||||
self.wrapper.text()
|
self.wrapper.text()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate how many wrap rows of a buffer line are visible (not folded)
|
/// Calculate how many wrap rows of a buffer line are visible.
|
||||||
pub fn visible_wrap_row_count_for_line(&self, line: usize, fold_map: &FoldMap) -> usize {
|
/// Without folding, all wrap rows are visible.
|
||||||
let wrap_range = self.buffer_line_to_wrap_row_range(line);
|
pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize {
|
||||||
wrap_range
|
self.buffer_line_to_wrap_row_range(line).len()
|
||||||
.filter(|&wr| fold_map.wrap_row_to_display_row(wr).is_some())
|
|
||||||
.count()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-679
@@ -3,9 +3,8 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Bounds, Corners, Edges, Element, ElementId, ElementInputHandler, Entity,
|
AnyElement, App, Bounds, Corners, Edges, Element, ElementId, ElementInputHandler, Entity,
|
||||||
GlobalElementId, Half, HighlightStyle, Hitbox, HitboxBehavior, Hsla, InteractiveElement,
|
GlobalElementId, Half, Hsla, IntoElement, LayoutId, MouseButton, MouseMoveEvent, MouseUpEvent,
|
||||||
IntoElement, LayoutId, MouseButton, MouseMoveEvent, MouseUpEvent, Path, Pixels, Point,
|
Path, Pixels, Point, Position, SharedString, Size, Style, TextAlign, TextRun, TextStyle,
|
||||||
Position, ShapedLine, SharedString, Size, Style, Styled as _, TextAlign, TextRun, TextStyle,
|
|
||||||
UnderlineStyle, Window, fill, point, px, relative, size,
|
UnderlineStyle, Window, fill, point, px, relative, size,
|
||||||
};
|
};
|
||||||
use ropey::Rope;
|
use ropey::Rope;
|
||||||
@@ -14,19 +13,14 @@ use theme::ActiveTheme;
|
|||||||
|
|
||||||
use super::mode::InputMode;
|
use super::mode::InputMode;
|
||||||
use super::{InputState, LastLayout, WhitespaceIndicators};
|
use super::{InputState, LastLayout, WhitespaceIndicators};
|
||||||
use crate::button::{Button, ButtonVariants as _};
|
use crate::Root;
|
||||||
use crate::input::RopeExt as _;
|
use crate::input::RopeExt as _;
|
||||||
use crate::input::blink_cursor::CURSOR_WIDTH;
|
use crate::input::blink_cursor::CURSOR_WIDTH;
|
||||||
use crate::input::display_map::LineLayout;
|
use crate::input::display_map::LineLayout;
|
||||||
use crate::scroll::Scrollbar;
|
use crate::scroll::Scrollbar;
|
||||||
use crate::{IconName, Root, Selectable, Sizable as _};
|
|
||||||
|
|
||||||
const BOTTOM_MARGIN_ROWS: usize = 3;
|
const BOTTOM_MARGIN_ROWS: usize = 3;
|
||||||
pub(super) const RIGHT_MARGIN: Pixels = px(10.);
|
pub(super) const RIGHT_MARGIN: Pixels = px(10.);
|
||||||
pub(super) const LINE_NUMBER_RIGHT_MARGIN: Pixels = px(10.);
|
|
||||||
const FOLD_ICON_WIDTH: Pixels = px(14.);
|
|
||||||
const FOLD_ICON_HITBOX_WIDTH: Pixels = px(18.);
|
|
||||||
const MAX_HIGHLIGHT_LINE_LENGTH: usize = 10_000;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
struct EditorScrollbarLayout {
|
struct EditorScrollbarLayout {
|
||||||
@@ -72,7 +66,7 @@ impl EditorScrollbarLayout {
|
|||||||
let left = if line_number_width == px(0.) {
|
let left = if line_number_width == px(0.) {
|
||||||
px(0.)
|
px(0.)
|
||||||
} else {
|
} else {
|
||||||
paddings.left + line_number_width - LINE_NUMBER_RIGHT_MARGIN
|
paddings.left + line_number_width
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -216,14 +210,6 @@ fn masked_display_offset(text: &Rope, original_offset: usize) -> usize {
|
|||||||
text.offset_to_char_index(original_offset) * MASK_CHAR.len_utf8()
|
text.offset_to_char_index(original_offset) * MASK_CHAR.len_utf8()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layout information for fold icons.
|
|
||||||
struct FoldIconLayout {
|
|
||||||
/// Hitbox for the line number area (used for hover detection)
|
|
||||||
line_number_hitbox: Hitbox,
|
|
||||||
/// List of (display_row, is_folded, icon_element) pairs for each fold candidate
|
|
||||||
icons: Vec<(usize, bool, gpui::AnyElement)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) struct TextElement {
|
pub(super) struct TextElement {
|
||||||
pub(crate) state: Entity<InputState>,
|
pub(crate) state: Entity<InputState>,
|
||||||
placeholder: SharedString,
|
placeholder: SharedString,
|
||||||
@@ -285,7 +271,7 @@ impl TextElement {
|
|||||||
scroll_size: Size<Pixels>,
|
scroll_size: Size<Pixels>,
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> (Option<Bounds<Pixels>>, Point<Pixels>, Option<usize>) {
|
) -> (Option<Bounds<Pixels>>, Point<Pixels>) {
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
|
|
||||||
let line_height = last_layout.line_height;
|
let line_height = last_layout.line_height;
|
||||||
@@ -307,7 +293,6 @@ impl TextElement {
|
|||||||
cursor = masked_display_offset(&state.text, cursor);
|
cursor = masked_display_offset(&state.text, cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut current_row = None;
|
|
||||||
let mut scroll_offset = state.scroll_handle.offset();
|
let mut scroll_offset = state.scroll_handle.offset();
|
||||||
let mut cursor_bounds = None;
|
let mut cursor_bounds = None;
|
||||||
|
|
||||||
@@ -330,7 +315,6 @@ impl TextElement {
|
|||||||
let visible_buffer_lines = &last_layout.visible_buffer_lines;
|
let visible_buffer_lines = &last_layout.visible_buffer_lines;
|
||||||
let mut vi = 0; // index into visible_buffer_lines / lines
|
let mut vi = 0; // index into visible_buffer_lines / lines
|
||||||
for (ix, wrap_line) in buffer_lines.iter().enumerate() {
|
for (ix, wrap_line) in buffer_lines.iter().enumerate() {
|
||||||
let row = ix;
|
|
||||||
let line_origin = point(px(0.), offset_y);
|
let line_origin = point(px(0.), offset_y);
|
||||||
|
|
||||||
// break loop if all cursor positions are found
|
// break loop if all cursor positions are found
|
||||||
@@ -353,7 +337,6 @@ impl TextElement {
|
|||||||
if let Some(pos) =
|
if let Some(pos) =
|
||||||
line.position_for_index(offset, last_layout, state.cursor_line_end_affinity)
|
line.position_for_index(offset, last_layout, state.cursor_line_end_affinity)
|
||||||
{
|
{
|
||||||
current_row = Some(row);
|
|
||||||
cursor_pos = Some(line_origin + pos);
|
cursor_pos = Some(line_origin + pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,7 +360,6 @@ impl TextElement {
|
|||||||
// Not visible (before visible range or hidden/folded).
|
// Not visible (before visible range or hidden/folded).
|
||||||
// Just increase the offset_y and prev_lines_offset for scroll tracking.
|
// Just increase the offset_y and prev_lines_offset for scroll tracking.
|
||||||
if prev_lines_offset >= cursor && cursor_pos.is_none() {
|
if prev_lines_offset >= cursor && cursor_pos.is_none() {
|
||||||
current_row = Some(row);
|
|
||||||
cursor_pos = Some(line_origin);
|
cursor_pos = Some(line_origin);
|
||||||
}
|
}
|
||||||
if prev_lines_offset >= selected_range.start && cursor_start.is_none() {
|
if prev_lines_offset >= selected_range.start && cursor_start.is_none() {
|
||||||
@@ -494,7 +476,7 @@ impl TextElement {
|
|||||||
|
|
||||||
bounds.origin += scroll_offset;
|
bounds.origin += scroll_offset;
|
||||||
|
|
||||||
(cursor_bounds, scroll_offset, current_row)
|
(cursor_bounds, scroll_offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layout the match range to a Path.
|
/// Layout the match range to a Path.
|
||||||
@@ -642,41 +624,6 @@ impl TextElement {
|
|||||||
builder.build().ok()
|
builder.build().ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout_search_matches(
|
|
||||||
&self,
|
|
||||||
_last_layout: &LastLayout,
|
|
||||||
_bounds: &Bounds<Pixels>,
|
|
||||||
_cx: &mut App,
|
|
||||||
) -> Vec<(Path<Pixels>, bool)> {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout_hover_highlight(
|
|
||||||
&self,
|
|
||||||
_last_layout: &LastLayout,
|
|
||||||
_bounds: &Bounds<Pixels>,
|
|
||||||
_cx: &mut App,
|
|
||||||
) -> Option<Path<Pixels>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout_document_colors(
|
|
||||||
&self,
|
|
||||||
document_colors: &[(Range<usize>, Hsla)],
|
|
||||||
last_layout: &LastLayout,
|
|
||||||
bounds: &Bounds<Pixels>,
|
|
||||||
_cx: &mut App,
|
|
||||||
) -> Vec<(Path<Pixels>, Hsla)> {
|
|
||||||
let mut paths = vec![];
|
|
||||||
for (range, color) in document_colors.iter() {
|
|
||||||
if let Some(path) = Self::layout_match_range(range.clone(), last_layout, bounds) {
|
|
||||||
paths.push((path, *color));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
paths
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout_selections(
|
fn layout_selections(
|
||||||
&self,
|
&self,
|
||||||
last_layout: &LastLayout,
|
last_layout: &LastLayout,
|
||||||
@@ -784,267 +731,20 @@ impl TextElement {
|
|||||||
(visible_range, visible_buffer_lines, visible_top)
|
(visible_range, visible_buffer_lines, visible_top)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return (line_number_width, line_number_len)
|
|
||||||
fn layout_line_numbers(
|
|
||||||
state: &InputState,
|
|
||||||
text: &Rope,
|
|
||||||
font_size: Pixels,
|
|
||||||
style: &TextStyle,
|
|
||||||
window: &mut Window,
|
|
||||||
) -> (Pixels, usize) {
|
|
||||||
let total_lines = text.lines_len();
|
|
||||||
let line_number_len = match total_lines {
|
|
||||||
0..=9999 => 5,
|
|
||||||
10000..=99999 => 6,
|
|
||||||
100000..=999999 => 7,
|
|
||||||
_ => 8,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut line_number_width = if state.mode.line_number() {
|
|
||||||
let empty_line_number = window.text_system().shape_line(
|
|
||||||
"+".repeat(line_number_len).into(),
|
|
||||||
font_size,
|
|
||||||
&[TextRun {
|
|
||||||
len: line_number_len,
|
|
||||||
font: style.font(),
|
|
||||||
color: gpui::black(),
|
|
||||||
background_color: None,
|
|
||||||
underline: None,
|
|
||||||
strikethrough: None,
|
|
||||||
}],
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
empty_line_number.width + LINE_NUMBER_RIGHT_MARGIN
|
|
||||||
} else if state.mode.is_code_editor() && state.mode.is_multi_line() {
|
|
||||||
LINE_NUMBER_RIGHT_MARGIN
|
|
||||||
} else {
|
|
||||||
px(0.)
|
|
||||||
};
|
|
||||||
|
|
||||||
if state.mode.is_folding() {
|
|
||||||
// Add extra space for fold icons
|
|
||||||
line_number_width += FOLD_ICON_HITBOX_WIDTH
|
|
||||||
}
|
|
||||||
|
|
||||||
(line_number_width, line_number_len)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Layout shaped lines for whitespace indicators (space and tab).
|
/// Layout shaped lines for whitespace indicators (space and tab).
|
||||||
///
|
///
|
||||||
/// Returns `WhitespaceIndicators` with shaped lines for space and tab characters.
|
/// Returns `WhitespaceIndicators` with shaped lines for space and tab characters.
|
||||||
fn layout_whitespace_indicators(
|
fn layout_whitespace_indicators(
|
||||||
state: &InputState,
|
_state: &InputState,
|
||||||
text_size: Pixels,
|
text_size: Pixels,
|
||||||
style: &TextStyle,
|
style: &TextStyle,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Option<WhitespaceIndicators> {
|
) -> Option<WhitespaceIndicators> {
|
||||||
if !state.show_whitespaces {
|
// Whitespace indicators are not currently enabled.
|
||||||
return None;
|
// When re-enabled, check `state.show_whitespaces` to conditionally enable.
|
||||||
}
|
let _ = (text_size, style, window, cx);
|
||||||
|
None
|
||||||
let invisible_color = cx.theme().text_muted;
|
|
||||||
let space_font_size = text_size.half();
|
|
||||||
let tab_font_size = text_size;
|
|
||||||
|
|
||||||
let space_text = SharedString::new_static("•");
|
|
||||||
let space = window.text_system().shape_line(
|
|
||||||
space_text.clone(),
|
|
||||||
space_font_size,
|
|
||||||
&[TextRun {
|
|
||||||
len: space_text.len(),
|
|
||||||
font: style.font(),
|
|
||||||
color: invisible_color,
|
|
||||||
background_color: None,
|
|
||||||
underline: None,
|
|
||||||
strikethrough: None,
|
|
||||||
}],
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
let tab_text = SharedString::new_static("→");
|
|
||||||
let tab = window.text_system().shape_line(
|
|
||||||
tab_text.clone(),
|
|
||||||
tab_font_size,
|
|
||||||
&[TextRun {
|
|
||||||
len: tab_text.len(),
|
|
||||||
font: style.font(),
|
|
||||||
color: invisible_color,
|
|
||||||
background_color: None,
|
|
||||||
underline: None,
|
|
||||||
strikethrough: None,
|
|
||||||
}],
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
Some(WhitespaceIndicators { space, tab })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute inline completion ghost lines for rendering.
|
|
||||||
///
|
|
||||||
/// Returns (first_line, ghost_lines) where:
|
|
||||||
/// - first_line: Shaped text for the first line (goes after cursor on same line)
|
|
||||||
/// - ghost_lines: Shaped lines for subsequent lines (shift content down)
|
|
||||||
fn layout_inline_completion(
|
|
||||||
_state: &InputState,
|
|
||||||
_visible_range: &Range<usize>,
|
|
||||||
_font_size: Pixels,
|
|
||||||
_window: &mut Window,
|
|
||||||
_cx: &App,
|
|
||||||
) -> (Option<ShapedLine>, Vec<ShapedLine>) {
|
|
||||||
(None, vec![])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return (line_number_width, line_number_len)
|
|
||||||
/// Layout fold icon hitboxes during prepaint phase.
|
|
||||||
///
|
|
||||||
/// This creates hitboxes for the fold icon area, positioned to the right of line numbers.
|
|
||||||
/// Icons are created and prepainted here to avoid panics.
|
|
||||||
fn layout_fold_icons(
|
|
||||||
&self,
|
|
||||||
origin_x: Pixels,
|
|
||||||
bounds: &Bounds<Pixels>,
|
|
||||||
last_layout: &LastLayout,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> FoldIconLayout {
|
|
||||||
// First pass: collect fold information from state
|
|
||||||
struct FoldInfo {
|
|
||||||
buffer_line: usize,
|
|
||||||
is_folded: bool,
|
|
||||||
display_row: usize,
|
|
||||||
offset_y: Pixels,
|
|
||||||
}
|
|
||||||
|
|
||||||
let line_number_hitbox = window.insert_hitbox(
|
|
||||||
Bounds::new(
|
|
||||||
point(origin_x, bounds.origin.y + last_layout.visible_top),
|
|
||||||
size(last_layout.line_number_width, bounds.size.height),
|
|
||||||
),
|
|
||||||
HitboxBehavior::Normal,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut icon_layout = FoldIconLayout {
|
|
||||||
line_number_hitbox,
|
|
||||||
icons: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
let fold_infos: Vec<FoldInfo> = {
|
|
||||||
let state = self.state.read(cx);
|
|
||||||
if !state.mode.is_folding() {
|
|
||||||
return icon_layout;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut infos = Vec::with_capacity(last_layout.visible_buffer_lines.len());
|
|
||||||
let mut offset_y = last_layout.visible_top;
|
|
||||||
|
|
||||||
for (line, &buffer_line) in last_layout
|
|
||||||
.lines
|
|
||||||
.iter()
|
|
||||||
.zip(last_layout.visible_buffer_lines.iter())
|
|
||||||
{
|
|
||||||
if state.display_map.is_fold_candidate(buffer_line) {
|
|
||||||
let is_folded = state.display_map.is_folded_at(buffer_line);
|
|
||||||
infos.push(FoldInfo {
|
|
||||||
buffer_line,
|
|
||||||
is_folded,
|
|
||||||
display_row: buffer_line,
|
|
||||||
offset_y,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
offset_y += line.wrapped_lines.len() * last_layout.line_height;
|
|
||||||
}
|
|
||||||
|
|
||||||
infos
|
|
||||||
}; // state is dropped here
|
|
||||||
|
|
||||||
// Second pass: create and prepaint icons
|
|
||||||
let line_height = last_layout.line_height;
|
|
||||||
let line_number_width =
|
|
||||||
last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN - FOLD_ICON_HITBOX_WIDTH;
|
|
||||||
let icon_relative_pos = point(
|
|
||||||
(FOLD_ICON_HITBOX_WIDTH - FOLD_ICON_WIDTH).half(),
|
|
||||||
(line_height - FOLD_ICON_WIDTH).half(),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (ix, info) in fold_infos.iter().enumerate() {
|
|
||||||
// Position fold icon to the right of line numbers.
|
|
||||||
// Use origin_x (unscrolled) so icons stay fixed in the gutter during horizontal scroll.
|
|
||||||
let fold_icon_bounds = Bounds::new(
|
|
||||||
point(
|
|
||||||
origin_x + icon_relative_pos.x + line_number_width,
|
|
||||||
bounds.origin.y + icon_relative_pos.y + info.offset_y,
|
|
||||||
),
|
|
||||||
size(FOLD_ICON_HITBOX_WIDTH, line_height),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create and prepaint icon
|
|
||||||
let mut icon = Button::new(("fold", ix))
|
|
||||||
.ghost()
|
|
||||||
.icon(if info.is_folded {
|
|
||||||
IconName::CaretRight
|
|
||||||
} else {
|
|
||||||
IconName::CaretDown
|
|
||||||
})
|
|
||||||
.xsmall()
|
|
||||||
.rounded_xs()
|
|
||||||
.size(FOLD_ICON_WIDTH)
|
|
||||||
.selected(info.is_folded)
|
|
||||||
.on_mouse_down(MouseButton::Left, {
|
|
||||||
let state = self.state.clone();
|
|
||||||
let buffer_line = info.buffer_line;
|
|
||||||
move |_, _: &mut Window, cx: &mut App| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
|
|
||||||
state.update(cx, |state, cx| {
|
|
||||||
state.display_map.toggle_fold(buffer_line);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into_any_element();
|
|
||||||
|
|
||||||
icon.prepaint_as_root(
|
|
||||||
fold_icon_bounds.origin,
|
|
||||||
fold_icon_bounds.size.into(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
icon_layout
|
|
||||||
.icons
|
|
||||||
.push((info.display_row, info.is_folded, icon));
|
|
||||||
}
|
|
||||||
|
|
||||||
icon_layout
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Paint fold icons using prepaint hitboxes.
|
|
||||||
///
|
|
||||||
/// This handles:
|
|
||||||
/// - Rendering fold icons (chevron-right for folded, chevron-down for expanded)
|
|
||||||
/// - Mouse click handling to toggle fold state
|
|
||||||
/// - Cursor style changes on hover
|
|
||||||
/// - Only show icon on hover or for current line
|
|
||||||
fn paint_fold_icons(
|
|
||||||
&mut self,
|
|
||||||
fold_icon_layout: &mut FoldIconLayout,
|
|
||||||
current_row: Option<usize>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
let is_hovered = fold_icon_layout.line_number_hitbox.is_hovered(window);
|
|
||||||
for (display_row, is_folded, icon) in fold_icon_layout.icons.iter_mut() {
|
|
||||||
let is_current_line = current_row == Some(*display_row);
|
|
||||||
|
|
||||||
if !is_hovered && !is_current_line && !*is_folded {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
icon.paint(window, cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -1098,10 +798,6 @@ impl TextElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut lines = Vec::with_capacity(last_layout.visible_buffer_lines.len());
|
let mut lines = Vec::with_capacity(last_layout.visible_buffer_lines.len());
|
||||||
// run_offset tracks position in the runs vec coordinate space (only visible line bytes).
|
|
||||||
// This is separate from the visible_text offset because runs from highlight_lines
|
|
||||||
// only cover visible (non-folded) lines.
|
|
||||||
let mut run_offset = 0;
|
|
||||||
|
|
||||||
for (vi, &buffer_line) in last_layout.visible_buffer_lines.iter().enumerate() {
|
for (vi, &buffer_line) in last_layout.visible_buffer_lines.iter().enumerate() {
|
||||||
let line_text: String = display_text.slice_line(buffer_line).into();
|
let line_text: String = display_text.slice_line(buffer_line).into();
|
||||||
@@ -1112,9 +808,10 @@ impl TextElement {
|
|||||||
debug_assert_eq!(line_item.len(), line_text.len());
|
debug_assert_eq!(line_item.len(), line_text.len());
|
||||||
|
|
||||||
let mut wrapped_lines = SmallVec::with_capacity(1);
|
let mut wrapped_lines = SmallVec::with_capacity(1);
|
||||||
|
let line_offset = display_text.line_start_offset(buffer_line);
|
||||||
|
|
||||||
for range in &line_item.wrapped_lines {
|
for range in &line_item.wrapped_lines {
|
||||||
let line_runs = runs_for_range(runs, run_offset, range);
|
let line_runs = runs_for_range(runs, line_offset, range);
|
||||||
let line_runs = if bg_segments.is_empty() {
|
let line_runs = if bg_segments.is_empty() {
|
||||||
line_runs
|
line_runs
|
||||||
} else {
|
} else {
|
||||||
@@ -1137,107 +834,21 @@ impl TextElement {
|
|||||||
.lines(wrapped_lines)
|
.lines(wrapped_lines)
|
||||||
.with_whitespaces(whitespace_indicators.clone());
|
.with_whitespaces(whitespace_indicators.clone());
|
||||||
lines.push(line_layout);
|
lines.push(line_layout);
|
||||||
|
|
||||||
// +1 for the `\n`
|
|
||||||
run_offset += line_text.len() + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
|
|
||||||
/// First usize is the offset of skipped.
|
|
||||||
fn highlight_lines(
|
|
||||||
&mut self,
|
|
||||||
visible_buffer_lines: &[usize],
|
|
||||||
_visible_top: Pixels,
|
|
||||||
_visible_byte_range: Range<usize>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
|
|
||||||
let state = self.state.read(cx);
|
|
||||||
let text = &state.text;
|
|
||||||
let is_multi_line = state.mode.is_multi_line();
|
|
||||||
|
|
||||||
let mut styles = Vec::with_capacity(visible_buffer_lines.len());
|
|
||||||
|
|
||||||
// Helper to flush a contiguous range of lines. These ranges are disjoint,
|
|
||||||
// so appending avoids repeatedly cloning and recombining prior styles.
|
|
||||||
let flush_range = |start_line: usize, end_line: usize, _skip: bool, styles: &mut Vec<_>| {
|
|
||||||
let byte_start = text.line_start_offset(start_line);
|
|
||||||
let byte_end = if is_multi_line {
|
|
||||||
// +1 for `\n`
|
|
||||||
text.line_start_offset(end_line + 1)
|
|
||||||
} else {
|
|
||||||
text.line_end_offset(end_line)
|
|
||||||
};
|
|
||||||
let range_styles = vec![(byte_start..byte_end, HighlightStyle::default())];
|
|
||||||
styles.extend(range_styles);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Group contiguous visible lines into ranges and call styles() once per range
|
|
||||||
let mut visible_iter = visible_buffer_lines.iter().peekable();
|
|
||||||
let mut range_start: Option<usize> = None;
|
|
||||||
|
|
||||||
while let Some(&line) = visible_iter.next() {
|
|
||||||
// Check if this line is too long for highlighting
|
|
||||||
let line_len = text.slice_line(line).len();
|
|
||||||
if line_len > MAX_HIGHLIGHT_LINE_LENGTH {
|
|
||||||
// Flush any accumulated range first
|
|
||||||
if let Some(start) = range_start.take() {
|
|
||||||
flush_range(start, line - 1, false, &mut styles);
|
|
||||||
}
|
|
||||||
|
|
||||||
flush_range(line, line, true, &mut styles);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
range_start.get_or_insert(line);
|
|
||||||
|
|
||||||
// Check if next line is contiguous, if so keep accumulating
|
|
||||||
if visible_iter
|
|
||||||
.peek()
|
|
||||||
.map(|&&next| next == line + 1)
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush the contiguous range
|
|
||||||
let start_line = range_start.take().unwrap();
|
|
||||||
flush_range(start_line, line, false, &mut styles);
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(styles)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct PrepaintState {
|
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 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>,
|
||||||
/// row index (zero based), no wrap, same line as the cursor.
|
|
||||||
current_row: Option<usize>,
|
|
||||||
selection_path: Option<Path<Pixels>>,
|
selection_path: Option<Path<Pixels>>,
|
||||||
hover_highlight_path: Option<Path<Pixels>>,
|
|
||||||
search_match_paths: Vec<(Path<Pixels>, bool)>,
|
|
||||||
document_color_paths: Vec<(Path<Pixels>, Hsla)>,
|
|
||||||
hover_definition_hitbox: Option<Hitbox>,
|
|
||||||
indent_guides_path: Option<Path<Pixels>>,
|
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
/// Fold icon layout data
|
|
||||||
fold_icon_layout: FoldIconLayout,
|
|
||||||
// Inline completion rendering data
|
|
||||||
/// Shaped ghost lines to paint after cursor row (completion lines 2+)
|
|
||||||
ghost_lines: Vec<ShapedLine>,
|
|
||||||
/// First line of inline completion (painted after cursor on same line)
|
|
||||||
ghost_first_line: Option<ShapedLine>,
|
|
||||||
ghost_lines_height: Pixels,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrepaintState {
|
impl PrepaintState {
|
||||||
@@ -1356,13 +967,6 @@ impl Element for TextElement {
|
|||||||
.text
|
.text
|
||||||
.line_end_offset(visible_range.end.saturating_sub(1));
|
.line_end_offset(visible_range.end.saturating_sub(1));
|
||||||
|
|
||||||
let highlight_styles = self.highlight_lines(
|
|
||||||
&visible_buffer_lines,
|
|
||||||
visible_top,
|
|
||||||
visible_start_offset..visible_end_offset,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let multi_line = state.mode.is_multi_line();
|
let multi_line = state.mode.is_multi_line();
|
||||||
let text = state.text.clone();
|
let text = state.text.clone();
|
||||||
@@ -1382,9 +986,8 @@ impl Element for TextElement {
|
|||||||
(&text, fg)
|
(&text, fg)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate the width of the line numbers
|
// Line numbers are not used (code editor mode removed)
|
||||||
let (line_number_width, line_number_len) =
|
let line_number_width = px(0.);
|
||||||
Self::layout_line_numbers(state, &text, text_size, &text_style, window);
|
|
||||||
|
|
||||||
let mut bounds = bounds;
|
let mut bounds = bounds;
|
||||||
let wrap_width = if multi_line && state.soft_wrap {
|
let wrap_width = if multi_line && state.soft_wrap {
|
||||||
@@ -1452,28 +1055,7 @@ impl Element for TextElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let runs = if !is_empty {
|
let runs = if !is_empty {
|
||||||
if let Some(highlight_styles) = highlight_styles {
|
|
||||||
let mut runs = Vec::with_capacity(highlight_styles.len());
|
|
||||||
|
|
||||||
runs.extend(highlight_styles.iter().map(|(range, style)| {
|
|
||||||
let mut run = text_style.clone().highlight(*style).to_run(range.len());
|
|
||||||
|
|
||||||
if let Some(ime_marked_range) = &state.ime_marked_range
|
|
||||||
&& range.start >= ime_marked_range.start
|
|
||||||
&& range.end <= ime_marked_range.end
|
|
||||||
{
|
|
||||||
run.color = marked_run.color;
|
|
||||||
run.strikethrough = marked_run.strikethrough;
|
|
||||||
run.underline = marked_run.underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
run
|
|
||||||
}));
|
|
||||||
|
|
||||||
runs.into_iter().filter(|run| run.len > 0).collect()
|
|
||||||
} else {
|
|
||||||
vec![run]
|
vec![run]
|
||||||
}
|
|
||||||
} else if let Some(ime_marked_range) = &state.ime_marked_range {
|
} else if let Some(ime_marked_range) = &state.ime_marked_range {
|
||||||
// IME marked text
|
// IME marked text
|
||||||
vec![
|
vec![
|
||||||
@@ -1498,8 +1080,6 @@ impl Element for TextElement {
|
|||||||
vec![run]
|
vec![run]
|
||||||
};
|
};
|
||||||
|
|
||||||
let document_colors = [];
|
|
||||||
|
|
||||||
// Create shaped lines for whitespace indicators before layout
|
// Create shaped lines for whitespace indicators before layout
|
||||||
let whitespace_indicators =
|
let whitespace_indicators =
|
||||||
Self::layout_whitespace_indicators(state, text_size, &text_style, window, cx);
|
Self::layout_whitespace_indicators(state, text_size, &text_style, window, cx);
|
||||||
@@ -1510,7 +1090,7 @@ impl Element for TextElement {
|
|||||||
&last_layout,
|
&last_layout,
|
||||||
text_size,
|
text_size,
|
||||||
&runs,
|
&runs,
|
||||||
&document_colors,
|
&[],
|
||||||
whitespace_indicators,
|
whitespace_indicators,
|
||||||
window,
|
window,
|
||||||
);
|
);
|
||||||
@@ -1540,26 +1120,8 @@ impl Element for TextElement {
|
|||||||
}
|
}
|
||||||
last_layout.lines = Rc::new(lines);
|
last_layout.lines = Rc::new(lines);
|
||||||
|
|
||||||
let (ghost_first_line, ghost_lines) = Self::layout_inline_completion(
|
|
||||||
state,
|
|
||||||
&last_layout.visible_range,
|
|
||||||
text_size,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
let ghost_line_count = ghost_lines.len();
|
|
||||||
let ghost_lines_height = ghost_line_count as f32 * line_height;
|
|
||||||
|
|
||||||
let total_wrapped_lines = state.display_map.wrap_row_count();
|
let total_wrapped_lines = state.display_map.wrap_row_count();
|
||||||
let empty_bottom_height = if state.mode.is_code_editor() {
|
let empty_bottom_height = px(0.);
|
||||||
bounds
|
|
||||||
.size
|
|
||||||
.height
|
|
||||||
.half()
|
|
||||||
.max(BOTTOM_MARGIN_ROWS * line_height)
|
|
||||||
} else {
|
|
||||||
px(0.)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut scroll_size = size(
|
let mut scroll_size = size(
|
||||||
if longest_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width {
|
if longest_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width {
|
||||||
@@ -1567,7 +1129,7 @@ impl Element for TextElement {
|
|||||||
} else {
|
} else {
|
||||||
longest_line_width
|
longest_line_width
|
||||||
},
|
},
|
||||||
(total_wrapped_lines as f32 * line_height + empty_bottom_height + ghost_lines_height)
|
(total_wrapped_lines as f32 * line_height + empty_bottom_height)
|
||||||
.max(bounds.size.height),
|
.max(bounds.size.height),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1608,75 +1170,16 @@ impl Element for TextElement {
|
|||||||
|
|
||||||
// Calculate the scroll offset to keep the cursor in view
|
// Calculate the scroll offset to keep the cursor in view
|
||||||
|
|
||||||
// Save the unscrolled x before layout_cursor modifies bounds.origin with scroll_offset.
|
// Save the bounds before layout_cursor modifies bounds.origin with scroll_offset.
|
||||||
// Fold icons and their hitboxes must use this value so they stay fixed in the gutter
|
|
||||||
// regardless of horizontal scroll position.
|
|
||||||
let input_bounds = bounds;
|
let input_bounds = bounds;
|
||||||
let original_x = bounds.origin.x;
|
|
||||||
|
|
||||||
let (cursor_bounds, cursor_scroll_offset, current_row) =
|
let (cursor_bounds, cursor_scroll_offset) =
|
||||||
self.layout_cursor(&last_layout, &mut bounds, scroll_size, window, cx);
|
self.layout_cursor(&last_layout, &mut bounds, scroll_size, window, cx);
|
||||||
last_layout.cursor_bounds = cursor_bounds;
|
last_layout.cursor_bounds = cursor_bounds;
|
||||||
|
|
||||||
let search_match_paths = self.layout_search_matches(&last_layout, &bounds, cx);
|
|
||||||
let selection_path = self.layout_selections(&last_layout, &mut bounds, window, cx);
|
let selection_path = self.layout_selections(&last_layout, &mut bounds, window, cx);
|
||||||
let hover_highlight_path = self.layout_hover_highlight(&last_layout, &bounds, cx);
|
|
||||||
let document_color_paths =
|
|
||||||
self.layout_document_colors(&document_colors, &last_layout, &bounds, cx);
|
|
||||||
|
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let line_numbers = if state.mode.line_number() {
|
|
||||||
let mut line_numbers = Vec::with_capacity(last_layout.visible_buffer_lines.len());
|
|
||||||
let other_line_runs = vec![TextRun {
|
|
||||||
len: line_number_len,
|
|
||||||
font: style.font(),
|
|
||||||
color: cx.theme().text_muted,
|
|
||||||
background_color: None,
|
|
||||||
underline: None,
|
|
||||||
strikethrough: None,
|
|
||||||
}];
|
|
||||||
let current_line_runs = vec![TextRun {
|
|
||||||
len: line_number_len,
|
|
||||||
font: style.font(),
|
|
||||||
color: cx.theme().text,
|
|
||||||
background_color: None,
|
|
||||||
underline: None,
|
|
||||||
strikethrough: None,
|
|
||||||
}];
|
|
||||||
|
|
||||||
// build line numbers
|
|
||||||
for (line, &buffer_line) in last_layout
|
|
||||||
.lines
|
|
||||||
.iter()
|
|
||||||
.zip(last_layout.visible_buffer_lines.iter())
|
|
||||||
{
|
|
||||||
let line_no: SharedString =
|
|
||||||
format!("{:>width$}", buffer_line + 1, width = line_number_len).into();
|
|
||||||
|
|
||||||
let runs = if current_row == Some(buffer_line) {
|
|
||||||
¤t_line_runs
|
|
||||||
} else {
|
|
||||||
&other_line_runs
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut sub_lines: SmallVec<[ShapedLine; 1]> = SmallVec::new();
|
|
||||||
sub_lines.push(
|
|
||||||
window
|
|
||||||
.text_system()
|
|
||||||
.shape_line(line_no, text_size, runs, None),
|
|
||||||
);
|
|
||||||
for _ in 0..line.wrapped_lines.len().saturating_sub(1) {
|
|
||||||
sub_lines.push(ShapedLine::default());
|
|
||||||
}
|
|
||||||
line_numbers.push(sub_lines);
|
|
||||||
}
|
|
||||||
Some(line_numbers)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let indent_guides_path =
|
|
||||||
self.layout_indent_guides(state, &bounds, &last_layout, &text_style, window);
|
|
||||||
|
|
||||||
state
|
state
|
||||||
.editor_scrollbar_snapshot
|
.editor_scrollbar_snapshot
|
||||||
@@ -1688,27 +1191,13 @@ impl Element for TextElement {
|
|||||||
state,
|
state,
|
||||||
)));
|
)));
|
||||||
|
|
||||||
let fold_icon_layout =
|
|
||||||
self.layout_fold_icons(original_x, &bounds, &last_layout, window, cx);
|
|
||||||
|
|
||||||
PrepaintState {
|
PrepaintState {
|
||||||
bounds,
|
bounds,
|
||||||
last_layout,
|
last_layout,
|
||||||
scroll_size,
|
scroll_size,
|
||||||
line_numbers,
|
|
||||||
cursor_bounds,
|
cursor_bounds,
|
||||||
cursor_scroll_offset,
|
cursor_scroll_offset,
|
||||||
current_row,
|
|
||||||
selection_path,
|
selection_path,
|
||||||
search_match_paths,
|
|
||||||
hover_highlight_path,
|
|
||||||
hover_definition_hitbox: None,
|
|
||||||
document_color_paths,
|
|
||||||
indent_guides_path,
|
|
||||||
fold_icon_layout,
|
|
||||||
ghost_first_line,
|
|
||||||
ghost_lines,
|
|
||||||
ghost_lines_height,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1765,56 +1254,15 @@ impl Element for TextElement {
|
|||||||
|
|
||||||
let invisible_top_padding = prepaint.last_layout.visible_top;
|
let invisible_top_padding = prepaint.last_layout.visible_top;
|
||||||
|
|
||||||
// Paint active line
|
|
||||||
let mut offset_y = px(0.);
|
|
||||||
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
|
|
||||||
offset_y += invisible_top_padding;
|
|
||||||
|
|
||||||
// Each item is the normal lines.
|
|
||||||
for (lines, _) in line_numbers
|
|
||||||
.iter()
|
|
||||||
.zip(prepaint.last_layout.visible_buffer_lines.iter())
|
|
||||||
{
|
|
||||||
let height = line_height * lines.len() as f32;
|
|
||||||
offset_y += height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paint indent guides
|
|
||||||
if let Some(path) = prepaint.indent_guides_path.take() {
|
|
||||||
window.paint_path(path, cx.theme().border.opacity(0.85));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paint selections
|
// Paint selections
|
||||||
if window.is_window_active() {
|
if window.is_window_active()
|
||||||
let secondary_selection = cx.theme().selection;
|
&& let Some(path) = prepaint.selection_path.take()
|
||||||
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);
|
window.paint_path(path, cx.theme().selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint hover highlight
|
// Paint text
|
||||||
if let Some(path) = prepaint.hover_highlight_path.take() {
|
|
||||||
window.paint_path(path, secondary_selection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paint document colors
|
|
||||||
for (path, color) in prepaint.document_color_paths.iter() {
|
|
||||||
window.paint_path(path.clone(), *color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paint text with inline completion ghost line support
|
|
||||||
let mut offset_y = invisible_top_padding;
|
let mut offset_y = invisible_top_padding;
|
||||||
let ghost_lines = &prepaint.ghost_lines;
|
|
||||||
let has_ghost_lines = !ghost_lines.is_empty();
|
|
||||||
|
|
||||||
// Keep scrollbar offset always be positive,Start from the left position
|
// Keep scrollbar offset always be positive,Start from the left position
|
||||||
let scroll_offset = if text_align == TextAlign::Right {
|
let scroll_offset = if text_align == TextAlign::Right {
|
||||||
@@ -1827,16 +1275,12 @@ impl Element for TextElement {
|
|||||||
px(0.)
|
px(0.)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Track the y-position of the cursor row for positioning the first line suffix
|
for (line, _buffer_line) in prepaint
|
||||||
let mut cursor_row_y = None;
|
|
||||||
|
|
||||||
for (line, &buffer_line) in prepaint
|
|
||||||
.last_layout
|
.last_layout
|
||||||
.lines
|
.lines
|
||||||
.iter()
|
.iter()
|
||||||
.zip(prepaint.last_layout.visible_buffer_lines.iter())
|
.zip(prepaint.last_layout.visible_buffer_lines.iter())
|
||||||
{
|
{
|
||||||
let row = buffer_line;
|
|
||||||
let line_y = origin.y + offset_y;
|
let line_y = origin.y + offset_y;
|
||||||
let p = point(
|
let p = point(
|
||||||
origin.x + prepaint.last_layout.line_number_width + (scroll_offset),
|
origin.x + prepaint.last_layout.line_number_width + (scroll_offset),
|
||||||
@@ -1853,40 +1297,6 @@ impl Element for TextElement {
|
|||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
offset_y += line.size(line_height).height;
|
offset_y += line.size(line_height).height;
|
||||||
|
|
||||||
if Some(row) == prepaint.current_row {
|
|
||||||
cursor_row_y = Some(line_y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// After the cursor row, paint ghost lines (which shifts subsequent content down)
|
|
||||||
if has_ghost_lines && Some(row) == prepaint.current_row {
|
|
||||||
let ghost_x = origin.x + prepaint.last_layout.line_number_width;
|
|
||||||
|
|
||||||
for ghost_line in ghost_lines {
|
|
||||||
let ghost_p = point(ghost_x, origin.y + offset_y);
|
|
||||||
|
|
||||||
// Paint semi-transparent background for ghost line
|
|
||||||
let ghost_bounds = Bounds::new(
|
|
||||||
ghost_p,
|
|
||||||
size(
|
|
||||||
bounds.size.width - prepaint.last_layout.line_number_width,
|
|
||||||
line_height,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
window.paint_quad(fill(ghost_bounds, cx.theme().surface_background));
|
|
||||||
|
|
||||||
// Paint ghost line text
|
|
||||||
_ = ghost_line.paint(
|
|
||||||
ghost_p,
|
|
||||||
line_height,
|
|
||||||
text_align,
|
|
||||||
Some(prepaint.last_layout.content_width),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
offset_y += line_height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint blinking cursor
|
// Paint blinking cursor
|
||||||
@@ -1897,49 +1307,6 @@ impl Element for TextElement {
|
|||||||
window.paint_quad(fill(cursor_bounds, cx.theme().cursor));
|
window.paint_quad(fill(cursor_bounds, cx.theme().cursor));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint line numbers
|
|
||||||
let mut offset_y = px(0.);
|
|
||||||
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
|
|
||||||
offset_y += invisible_top_padding;
|
|
||||||
|
|
||||||
window.paint_quad(fill(
|
|
||||||
Bounds {
|
|
||||||
origin: input_bounds.origin,
|
|
||||||
size: size(
|
|
||||||
prepaint.last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN,
|
|
||||||
input_bounds.size.height + prepaint.ghost_lines_height,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
cx.theme().surface_background,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Each item is the normal lines.
|
|
||||||
for (lines, &buffer_line) in line_numbers
|
|
||||||
.iter()
|
|
||||||
.zip(prepaint.last_layout.visible_buffer_lines.iter())
|
|
||||||
{
|
|
||||||
let p = point(input_bounds.origin.x, origin.y + offset_y);
|
|
||||||
|
|
||||||
for line in lines {
|
|
||||||
_ = line.paint(p, line_height, TextAlign::Left, None, window, cx);
|
|
||||||
offset_y += line_height;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ghost line height after cursor row for line numbers alignment
|
|
||||||
if !prepaint.ghost_lines.is_empty() && prepaint.current_row == Some(buffer_line) {
|
|
||||||
offset_y += prepaint.ghost_lines_height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paint fold icons (only visible on hover or for current line)
|
|
||||||
self.paint_fold_icons(
|
|
||||||
&mut prepaint.fold_icon_layout,
|
|
||||||
prepaint.current_row,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
self.state.update(cx, |state, cx| {
|
self.state.update(cx, |state, cx| {
|
||||||
state.last_layout = Some(prepaint.last_layout.clone());
|
state.last_layout = Some(prepaint.last_layout.clone());
|
||||||
state.last_bounds = Some(bounds);
|
state.last_bounds = Some(bounds);
|
||||||
@@ -1953,27 +1320,6 @@ impl Element for TextElement {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(hitbox) = prepaint.hover_definition_hitbox.as_ref() {
|
|
||||||
window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paint inline completion first line suffix (after cursor on same line)
|
|
||||||
if focused
|
|
||||||
&& let Some(first_line) = &prepaint.ghost_first_line
|
|
||||||
&& let (Some(cursor_bounds), Some(cursor_row_y)) =
|
|
||||||
(prepaint.cursor_bounds_with_scroll(), cursor_row_y)
|
|
||||||
{
|
|
||||||
let first_line_x = cursor_bounds.origin.x + cursor_bounds.size.width;
|
|
||||||
let p = point(first_line_x, cursor_row_y);
|
|
||||||
|
|
||||||
// Paint background to cover any existing text
|
|
||||||
let bg_bounds = Bounds::new(p, size(first_line.width + px(4.), line_height));
|
|
||||||
window.paint_quad(fill(bg_bounds, cx.theme().surface_background));
|
|
||||||
|
|
||||||
// Paint first line completion text
|
|
||||||
_ = first_line.paint(p, line_height, text_align, None, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.paint_mouse_listeners(window, cx);
|
self.paint_mouse_listeners(window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
use gpui::{
|
use gpui::{Context, EntityInputHandler, SharedString, Window};
|
||||||
Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels, SharedString,
|
|
||||||
TextRun, TextStyle, Window, point, px,
|
|
||||||
};
|
|
||||||
use ropey::RopeSlice;
|
use ropey::RopeSlice;
|
||||||
|
|
||||||
use crate::input::element::TextElement;
|
|
||||||
use crate::input::mode::InputMode;
|
use crate::input::mode::InputMode;
|
||||||
use crate::input::{Indent, IndentInline, InputState, LastLayout, Outdent, OutdentInline, RopeExt};
|
use crate::input::{Indent, IndentInline, InputState, Outdent, OutdentInline};
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct TabSize {
|
pub struct TabSize {
|
||||||
@@ -49,165 +45,14 @@ impl TabSize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputMode {
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn is_indentable(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
InputMode::PlainText { multi_line, .. } | InputMode::CodeEditor { multi_line, .. } => {
|
|
||||||
*multi_line
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn has_indent_guides(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
InputMode::CodeEditor {
|
|
||||||
indent_guides,
|
|
||||||
multi_line,
|
|
||||||
..
|
|
||||||
} => *indent_guides && *multi_line,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn tab_size(&self) -> TabSize {
|
|
||||||
match self {
|
|
||||||
InputMode::PlainText { tab, .. } => *tab,
|
|
||||||
InputMode::CodeEditor { tab, .. } => *tab,
|
|
||||||
_ => TabSize::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextElement {
|
|
||||||
/// Measure the indent width in pixels for given column count.
|
|
||||||
fn measure_indent_width(&self, style: &TextStyle, column: usize, window: &Window) -> Pixels {
|
|
||||||
let font_size = style.font_size.to_pixels(window.rem_size());
|
|
||||||
let layout = window.text_system().shape_line(
|
|
||||||
SharedString::from(" ".repeat(column)),
|
|
||||||
font_size,
|
|
||||||
&[TextRun {
|
|
||||||
len: column,
|
|
||||||
font: style.font(),
|
|
||||||
color: Hsla::default(),
|
|
||||||
background_color: None,
|
|
||||||
strikethrough: None,
|
|
||||||
underline: None,
|
|
||||||
}],
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
layout.width
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn layout_indent_guides(
|
|
||||||
&self,
|
|
||||||
state: &InputState,
|
|
||||||
bounds: &Bounds<Pixels>,
|
|
||||||
last_layout: &LastLayout,
|
|
||||||
text_style: &TextStyle,
|
|
||||||
window: &mut Window,
|
|
||||||
) -> Option<Path<Pixels>> {
|
|
||||||
if !state.mode.has_indent_guides() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let indent_width =
|
|
||||||
self.measure_indent_width(text_style, state.mode.tab_size().tab_size, window);
|
|
||||||
|
|
||||||
let tab_size = state.mode.tab_size();
|
|
||||||
let line_height = last_layout.line_height;
|
|
||||||
let mut builder = PathBuilder::stroke(px(1.));
|
|
||||||
let mut offset_y = last_layout.visible_top;
|
|
||||||
let mut last_indents = vec![];
|
|
||||||
|
|
||||||
for (&buffer_line, line_layout) in last_layout
|
|
||||||
.visible_buffer_lines
|
|
||||||
.iter()
|
|
||||||
.zip(last_layout.lines.iter())
|
|
||||||
{
|
|
||||||
let line = state.text.slice_line(buffer_line);
|
|
||||||
let mut current_indents = vec![];
|
|
||||||
if line.len() > 0 {
|
|
||||||
let indent_count = tab_size.indent_count(&line);
|
|
||||||
for offset in (0..indent_count).step_by(tab_size.tab_size) {
|
|
||||||
let x = if indent_count > 0 {
|
|
||||||
indent_width * offset as f32 / tab_size.tab_size as f32
|
|
||||||
} else {
|
|
||||||
px(0.)
|
|
||||||
};
|
|
||||||
|
|
||||||
let pos = point(x + last_layout.line_number_width, offset_y);
|
|
||||||
|
|
||||||
builder.move_to(pos);
|
|
||||||
builder.line_to(point(pos.x, pos.y + line_height));
|
|
||||||
current_indents.push(pos.x);
|
|
||||||
}
|
|
||||||
} else if !last_indents.is_empty() {
|
|
||||||
for x in &last_indents {
|
|
||||||
let pos = point(*x, offset_y);
|
|
||||||
builder.move_to(pos);
|
|
||||||
builder.line_to(point(pos.x, pos.y + line_height));
|
|
||||||
}
|
|
||||||
current_indents = last_indents.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
offset_y += line_layout.wrapped_lines.len() * line_height;
|
|
||||||
last_indents = current_indents;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.translate(bounds.origin);
|
|
||||||
let path = builder.build().unwrap();
|
|
||||||
Some(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputState {
|
impl InputState {
|
||||||
/// Set whether to show indent guides in code editor mode, default is true.
|
|
||||||
///
|
|
||||||
/// Only for [`InputMode::CodeEditor`] mode.
|
|
||||||
pub fn indent_guides(mut self, indent_guides: bool) -> Self {
|
|
||||||
debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
|
|
||||||
if let InputMode::CodeEditor {
|
|
||||||
indent_guides: l, ..
|
|
||||||
} = &mut self.mode
|
|
||||||
{
|
|
||||||
*l = indent_guides;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set indent guides in code editor mode.
|
|
||||||
///
|
|
||||||
/// Only for [`InputMode::CodeEditor`] mode.
|
|
||||||
pub fn set_indent_guides(
|
|
||||||
&mut self,
|
|
||||||
indent_guides: bool,
|
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
debug_assert!(self.mode.is_code_editor());
|
|
||||||
if let InputMode::CodeEditor {
|
|
||||||
indent_guides: l, ..
|
|
||||||
} = &mut self.mode
|
|
||||||
{
|
|
||||||
*l = indent_guides;
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the tab size for the input.
|
/// Set the tab size for the input.
|
||||||
///
|
///
|
||||||
/// Only for [`InputMode::PlainText`] and [`InputMode::CodeEditor`] mode with multi_line.
|
/// Only for [`InputMode::PlainText`] mode with multi_line.
|
||||||
pub fn tab_size(mut self, tab: TabSize) -> Self {
|
pub fn tab_size(mut self, tab: TabSize) -> Self {
|
||||||
debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor());
|
debug_assert!(self.mode.is_multi_line());
|
||||||
match &mut self.mode {
|
if let InputMode::PlainText { tab: t, .. } = &mut self.mode {
|
||||||
InputMode::PlainText { tab: t, .. } => *t = tab,
|
*t = tab;
|
||||||
InputMode::CodeEditor { tab: t, .. } => *t = tab,
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ pub struct Input {
|
|||||||
cleanable: bool,
|
cleanable: bool,
|
||||||
mask_toggle: bool,
|
mask_toggle: bool,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
bordered: bool,
|
|
||||||
focus_bordered: bool,
|
|
||||||
tab_index: isize,
|
tab_index: isize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
}
|
}
|
||||||
@@ -73,8 +71,6 @@ impl Input {
|
|||||||
cleanable: false,
|
cleanable: false,
|
||||||
mask_toggle: false,
|
mask_toggle: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
bordered: true,
|
|
||||||
focus_bordered: true,
|
|
||||||
tab_index: 0,
|
tab_index: 0,
|
||||||
selected: false,
|
selected: false,
|
||||||
}
|
}
|
||||||
@@ -108,18 +104,6 @@ impl Input {
|
|||||||
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 whether to show the clear button when the input field is not empty, default is false.
|
/// Set whether to show the clear button when the input field is not empty, default is false.
|
||||||
pub fn cleanable(mut self, cleanable: bool) -> Self {
|
pub fn cleanable(mut self, cleanable: bool) -> Self {
|
||||||
self.cleanable = cleanable;
|
self.cleanable = cleanable;
|
||||||
@@ -234,12 +218,6 @@ impl RenderOnce for Input {
|
|||||||
|
|
||||||
let (bg, _) = input_style(state.disabled, cx);
|
let (bg, _) = input_style(state.disabled, cx);
|
||||||
|
|
||||||
let bg = if state.mode.is_code_editor() {
|
|
||||||
cx.theme().surface_background
|
|
||||||
} else {
|
|
||||||
bg
|
|
||||||
};
|
|
||||||
|
|
||||||
let prefix = self.prefix;
|
let prefix = self.prefix;
|
||||||
let suffix = self.suffix;
|
let suffix = self.suffix;
|
||||||
let show_clear_button = self.cleanable
|
let show_clear_button = self.cleanable
|
||||||
@@ -338,11 +316,6 @@ impl RenderOnce for Input {
|
|||||||
this.bg(bg)
|
this.bg(bg)
|
||||||
.when(self.disabled, |this| this.opacity(0.5))
|
.when(self.disabled, |this| this.opacity(0.5))
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.when(self.bordered, |this| {
|
|
||||||
this.border_color(cx.theme().border)
|
|
||||||
.border_1()
|
|
||||||
.when(cx.theme().shadow, |this| this.shadow_xs())
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap(gap_x)
|
.gap(gap_x)
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ mod state;
|
|||||||
|
|
||||||
pub(crate) use clear_button::*;
|
pub(crate) use clear_button::*;
|
||||||
pub use cursor::*;
|
pub use cursor::*;
|
||||||
#[cfg(target_family = "wasm")]
|
pub use display_map::DisplayMap;
|
||||||
pub use display_map::folding::Tree;
|
|
||||||
pub use display_map::{BufferPoint, DisplayMap, DisplayPoint, FoldRange};
|
|
||||||
pub use indent::TabSize;
|
pub use indent::TabSize;
|
||||||
pub use input::*;
|
pub use input::*;
|
||||||
pub use mask_pattern::MaskPattern;
|
pub use mask_pattern::MaskPattern;
|
||||||
|
|||||||
+12
-78
@@ -1,26 +1,11 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::{SharedString, Task};
|
|
||||||
use ropey::Rope;
|
|
||||||
|
|
||||||
use super::display_map::DisplayMap;
|
use super::display_map::DisplayMap;
|
||||||
use crate::input::TabSize;
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(super) struct PendingBackgroundParse {
|
|
||||||
pub parse_task: Rc<RefCell<Option<Task<()>>>>,
|
|
||||||
pub language: SharedString,
|
|
||||||
pub text: Rope,
|
|
||||||
pub is_folding: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) enum InputMode {
|
pub(crate) enum InputMode {
|
||||||
/// A plain text input mode.
|
/// A plain text input mode.
|
||||||
PlainText {
|
PlainText {
|
||||||
multi_line: bool,
|
multi_line: bool,
|
||||||
tab: TabSize,
|
tab: crate::input::indent::TabSize,
|
||||||
rows: usize,
|
rows: usize,
|
||||||
},
|
},
|
||||||
/// An auto grow input mode.
|
/// An auto grow input mode.
|
||||||
@@ -29,18 +14,6 @@ pub(crate) enum InputMode {
|
|||||||
min_rows: usize,
|
min_rows: usize,
|
||||||
max_rows: usize,
|
max_rows: usize,
|
||||||
},
|
},
|
||||||
/// A code editor input mode.
|
|
||||||
CodeEditor {
|
|
||||||
multi_line: bool,
|
|
||||||
tab: TabSize,
|
|
||||||
rows: usize,
|
|
||||||
/// Show line number
|
|
||||||
line_number: bool,
|
|
||||||
language: SharedString,
|
|
||||||
indent_guides: bool,
|
|
||||||
folding: bool,
|
|
||||||
parse_task: Rc<RefCell<Option<Task<()>>>>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for InputMode {
|
impl Default for InputMode {
|
||||||
@@ -55,25 +28,11 @@ impl InputMode {
|
|||||||
pub(super) fn plain_text() -> Self {
|
pub(super) fn plain_text() -> Self {
|
||||||
InputMode::PlainText {
|
InputMode::PlainText {
|
||||||
multi_line: false,
|
multi_line: false,
|
||||||
tab: TabSize::default(),
|
tab: crate::input::indent::TabSize::default(),
|
||||||
rows: 1,
|
rows: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a code editor input mode with default settings.
|
|
||||||
pub(super) fn code_editor(language: impl Into<SharedString>) -> Self {
|
|
||||||
InputMode::CodeEditor {
|
|
||||||
rows: 2,
|
|
||||||
multi_line: true,
|
|
||||||
tab: TabSize::default(),
|
|
||||||
language: language.into(),
|
|
||||||
line_number: true,
|
|
||||||
indent_guides: true,
|
|
||||||
folding: true,
|
|
||||||
parse_task: Rc::new(RefCell::new(None)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create an auto grow input mode with given min and max rows.
|
/// Create an auto grow input mode with given min and max rows.
|
||||||
pub(super) fn auto_grow(min_rows: usize, max_rows: usize) -> Self {
|
pub(super) fn auto_grow(min_rows: usize, max_rows: usize) -> Self {
|
||||||
InputMode::AutoGrow {
|
InputMode::AutoGrow {
|
||||||
@@ -86,7 +45,6 @@ impl InputMode {
|
|||||||
pub(super) fn multi_line(mut self, multi_line: bool) -> Self {
|
pub(super) fn multi_line(mut self, multi_line: bool) -> Self {
|
||||||
match &mut self {
|
match &mut self {
|
||||||
InputMode::PlainText { multi_line: ml, .. } => *ml = multi_line,
|
InputMode::PlainText { multi_line: ml, .. } => *ml = multi_line,
|
||||||
InputMode::CodeEditor { multi_line: ml, .. } => *ml = multi_line,
|
|
||||||
InputMode::AutoGrow { .. } => {}
|
InputMode::AutoGrow { .. } => {}
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
@@ -97,28 +55,6 @@ impl InputMode {
|
|||||||
!self.is_multi_line()
|
!self.is_multi_line()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn is_code_editor(&self) -> bool {
|
|
||||||
matches!(self, InputMode::CodeEditor { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return true if the mode is code editor and `folding: true`, `multi_line: true`.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn is_folding(&self) -> bool {
|
|
||||||
if cfg!(target_family = "wasm") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
matches!(
|
|
||||||
self,
|
|
||||||
InputMode::CodeEditor {
|
|
||||||
folding: true,
|
|
||||||
multi_line: true,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn is_auto_grow(&self) -> bool {
|
pub(super) fn is_auto_grow(&self) -> bool {
|
||||||
matches!(self, InputMode::AutoGrow { .. })
|
matches!(self, InputMode::AutoGrow { .. })
|
||||||
@@ -128,7 +64,6 @@ impl InputMode {
|
|||||||
pub(super) fn is_multi_line(&self) -> bool {
|
pub(super) fn is_multi_line(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
InputMode::PlainText { multi_line, .. } => *multi_line,
|
InputMode::PlainText { multi_line, .. } => *multi_line,
|
||||||
InputMode::CodeEditor { multi_line, .. } => *multi_line,
|
|
||||||
InputMode::AutoGrow { max_rows, .. } => *max_rows > 1,
|
InputMode::AutoGrow { max_rows, .. } => *max_rows > 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,9 +73,6 @@ impl InputMode {
|
|||||||
InputMode::PlainText { rows, .. } => {
|
InputMode::PlainText { rows, .. } => {
|
||||||
*rows = new_rows;
|
*rows = new_rows;
|
||||||
}
|
}
|
||||||
InputMode::CodeEditor { rows, .. } => {
|
|
||||||
*rows = new_rows;
|
|
||||||
}
|
|
||||||
InputMode::AutoGrow {
|
InputMode::AutoGrow {
|
||||||
rows,
|
rows,
|
||||||
min_rows,
|
min_rows,
|
||||||
@@ -168,7 +100,6 @@ impl InputMode {
|
|||||||
|
|
||||||
match self {
|
match self {
|
||||||
InputMode::PlainText { rows, .. } => *rows,
|
InputMode::PlainText { rows, .. } => *rows,
|
||||||
InputMode::CodeEditor { rows, .. } => *rows,
|
|
||||||
InputMode::AutoGrow { rows, .. } => *rows,
|
InputMode::AutoGrow { rows, .. } => *rows,
|
||||||
}
|
}
|
||||||
.max(1)
|
.max(1)
|
||||||
@@ -196,16 +127,19 @@ impl InputMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return false if the mode is not [`InputMode::CodeEditor`].
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn line_number(&self) -> bool {
|
pub(super) fn is_indentable(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
InputMode::CodeEditor {
|
InputMode::PlainText { multi_line, .. } => *multi_line,
|
||||||
line_number,
|
|
||||||
multi_line,
|
|
||||||
..
|
|
||||||
} => *line_number && *multi_line,
|
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn tab_size(&self) -> crate::input::indent::TabSize {
|
||||||
|
match self {
|
||||||
|
InputMode::PlainText { tab, .. } => *tab,
|
||||||
|
_ => crate::input::indent::TabSize::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,337 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
Action, AnyElement, App, AppContext, Context, DismissEvent, Empty, Entity, EventEmitter,
|
|
||||||
InteractiveElement as _, IntoElement, ParentElement, Pixels, Point, Render, RenderOnce,
|
|
||||||
SharedString, Styled, StyledText, Subscription, Window, deferred, div, px, relative,
|
|
||||||
};
|
|
||||||
use lsp_types::CodeAction;
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
|
|
||||||
const MAX_MENU_WIDTH: Pixels = px(320.);
|
|
||||||
const MAX_MENU_HEIGHT: Pixels = px(480.);
|
|
||||||
|
|
||||||
use crate::input::popovers::editor_popover;
|
|
||||||
use crate::input::{self, InputState};
|
|
||||||
use crate::list::{List, ListDelegate, ListEvent, ListState};
|
|
||||||
use crate::{IndexPath, Selectable, actions, h_flex};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub(crate) struct CodeActionItem {
|
|
||||||
/// The `id` of the `CodeActionProvider` that provided this item.
|
|
||||||
pub(crate) provider_id: SharedString,
|
|
||||||
pub(crate) action: CodeAction,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MenuDelegate {
|
|
||||||
menu: Entity<CodeActionMenu>,
|
|
||||||
items: Vec<Rc<CodeActionItem>>,
|
|
||||||
selected_ix: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MenuDelegate {
|
|
||||||
fn set_items(&mut self, items: Vec<CodeActionItem>) {
|
|
||||||
self.items = items.into_iter().map(Rc::new).collect();
|
|
||||||
self.selected_ix = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected_item(&self) -> Option<&Rc<CodeActionItem>> {
|
|
||||||
self.items.get(self.selected_ix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
struct MenuItem {
|
|
||||||
ix: usize,
|
|
||||||
item: Rc<CodeActionItem>,
|
|
||||||
children: Vec<AnyElement>,
|
|
||||||
selected: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MenuItem {
|
|
||||||
fn new(ix: usize, item: Rc<CodeActionItem>) -> Self {
|
|
||||||
Self {
|
|
||||||
ix,
|
|
||||||
item,
|
|
||||||
children: vec![],
|
|
||||||
selected: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Selectable for MenuItem {
|
|
||||||
fn selected(mut self, selected: bool) -> Self {
|
|
||||||
self.selected = selected;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_selected(&self) -> bool {
|
|
||||||
self.selected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParentElement for MenuItem {
|
|
||||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
||||||
self.children.extend(elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl RenderOnce for MenuItem {
|
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
let item = self.item;
|
|
||||||
|
|
||||||
let highlights = vec![];
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.id(self.ix)
|
|
||||||
.gap_2()
|
|
||||||
.p_1()
|
|
||||||
.text_xs()
|
|
||||||
.line_height(relative(1.))
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.hover(|this| this.bg(cx.theme().secondary_hover))
|
|
||||||
.when(self.selected, |this| {
|
|
||||||
this.bg(cx.theme().secondary_background)
|
|
||||||
.text_color(cx.theme().secondary_foreground)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div().child(StyledText::new(item.action.title.clone()).with_highlights(highlights)),
|
|
||||||
)
|
|
||||||
.children(self.children)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for MenuDelegate {}
|
|
||||||
|
|
||||||
impl ListDelegate for MenuDelegate {
|
|
||||||
type Item = MenuItem;
|
|
||||||
|
|
||||||
fn items_count(&self, _: usize, _: &gpui::App) -> usize {
|
|
||||||
self.items.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_item(
|
|
||||||
&mut self,
|
|
||||||
ix: crate::IndexPath,
|
|
||||||
_: &mut Window,
|
|
||||||
_: &mut Context<ListState<Self>>,
|
|
||||||
) -> Option<Self::Item> {
|
|
||||||
let item = self.items.get(ix.row)?;
|
|
||||||
Some(MenuItem::new(ix.row, item.clone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_selected_index(
|
|
||||||
&mut self,
|
|
||||||
ix: Option<crate::IndexPath>,
|
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut Context<ListState<Self>>,
|
|
||||||
) {
|
|
||||||
self.selected_ix = ix.map(|i| i.row).unwrap_or(0);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
|
|
||||||
let Some(item) = self.selected_item() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.menu.update(cx, |this, cx| {
|
|
||||||
this.select_item(&item, window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A context menu for code completions and code actions.
|
|
||||||
pub struct CodeActionMenu {
|
|
||||||
offset: usize,
|
|
||||||
state: Entity<InputState>,
|
|
||||||
list: Entity<ListState<MenuDelegate>>,
|
|
||||||
open: bool,
|
|
||||||
|
|
||||||
_subscriptions: Vec<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CodeActionMenu {
|
|
||||||
/// Creates a new `CompletionMenu` with the given offset and completion items.
|
|
||||||
///
|
|
||||||
/// NOTE: This element should not call from InputState::new, unless that will stack overflow.
|
|
||||||
pub(crate) fn new(
|
|
||||||
state: Entity<InputState>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Entity<Self> {
|
|
||||||
cx.new(|cx| {
|
|
||||||
let view = cx.entity();
|
|
||||||
let menu = MenuDelegate {
|
|
||||||
menu: view,
|
|
||||||
items: vec![],
|
|
||||||
selected_ix: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let list = cx.new(|cx| ListState::new(menu, window, cx));
|
|
||||||
|
|
||||||
let _subscriptions =
|
|
||||||
vec![
|
|
||||||
cx.subscribe(&list, |this: &mut Self, _, ev: &ListEvent, cx| {
|
|
||||||
match ev {
|
|
||||||
ListEvent::Confirm(_) => {
|
|
||||||
this.hide(cx);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
Self {
|
|
||||||
offset: 0,
|
|
||||||
state,
|
|
||||||
list,
|
|
||||||
open: false,
|
|
||||||
_subscriptions,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_item(&mut self, item: &CodeActionItem, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let state = self.state.clone();
|
|
||||||
let item = item.clone();
|
|
||||||
|
|
||||||
cx.spawn_in(window, {
|
|
||||||
async move |_, cx| {
|
|
||||||
state.update_in(cx, |state, window, cx| {
|
|
||||||
state.perform_code_action(&item, window, cx);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
self.hide(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn handle_action(
|
|
||||||
&mut self,
|
|
||||||
action: Box<dyn Action>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> bool {
|
|
||||||
if !self.open {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.propagate();
|
|
||||||
if input::Enter::is_primary(&*action) {
|
|
||||||
self.on_action_enter(window, cx);
|
|
||||||
} else if action.partial_eq(&input::Escape) {
|
|
||||||
self.on_action_escape(window, cx);
|
|
||||||
} else if action.partial_eq(&input::MoveUp) {
|
|
||||||
self.on_action_up(window, cx);
|
|
||||||
} else if action.partial_eq(&input::MoveDown) {
|
|
||||||
self.on_action_down(window, cx);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_action_enter(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Some(item) = self.list.read(cx).delegate().selected_item().cloned() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
self.select_item(&item, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_action_escape(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.hide(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_action_up(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.list.update(cx, |this, cx| {
|
|
||||||
this.on_action_select_prev(&actions::SelectUp, window, cx)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_action_down(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.list.update(cx, |this, cx| {
|
|
||||||
this.on_action_select_next(&actions::SelectDown, window, cx)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_open(&self) -> bool {
|
|
||||||
self.open
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hide the completion menu and reset the trigger start offset.
|
|
||||||
pub(crate) fn hide(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.open = false;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn show(
|
|
||||||
&mut self,
|
|
||||||
offset: usize,
|
|
||||||
items: impl Into<Vec<CodeActionItem>>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let items = items.into();
|
|
||||||
self.offset = offset;
|
|
||||||
self.open = true;
|
|
||||||
self.list.update(cx, |this, cx| {
|
|
||||||
this.delegate_mut().set_items(items);
|
|
||||||
this.set_selected_index(Some(IndexPath::new(0)), window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn origin(&self, cx: &App) -> Option<Point<Pixels>> {
|
|
||||||
let state = self.state.read(cx);
|
|
||||||
let Some(last_layout) = state.last_layout.as_ref() else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let Some(cursor_origin) = last_layout.cursor_bounds.map(|b| b.origin) else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let scroll_origin = self.state.read(cx).scroll_handle.offset();
|
|
||||||
|
|
||||||
Some(
|
|
||||||
scroll_origin + cursor_origin - state.input_bounds.origin
|
|
||||||
+ Point::new(-px(4.), last_layout.line_height + px(4.)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for CodeActionMenu {
|
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
if !self.open {
|
|
||||||
return Empty.into_any_element();
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.list.read(cx).delegate().items.is_empty() {
|
|
||||||
self.open = false;
|
|
||||||
return Empty.into_any_element();
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(pos) = self.origin(cx) else {
|
|
||||||
return Empty.into_any_element();
|
|
||||||
};
|
|
||||||
|
|
||||||
let max_width = MAX_MENU_WIDTH.min(window.bounds().size.width - pos.x);
|
|
||||||
|
|
||||||
deferred(
|
|
||||||
editor_popover("code-action-menu", cx)
|
|
||||||
.absolute()
|
|
||||||
.left(pos.x)
|
|
||||||
.top(pos.y)
|
|
||||||
.max_w(max_width)
|
|
||||||
.min_w(px(120.))
|
|
||||||
.child(List::new(&self.list).max_h(MAX_MENU_HEIGHT))
|
|
||||||
.on_mouse_down_out(cx.listener(|this, _, _, cx| {
|
|
||||||
this.hide(cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,446 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
Action, AnyElement, App, AppContext, Context, DismissEvent, Empty, Entity, EventEmitter,
|
|
||||||
Half as _, HighlightStyle, InteractiveElement as _, IntoElement, ParentElement, Pixels, Point,
|
|
||||||
Render, RenderOnce, SharedString, Styled, StyledText, Subscription, Window, deferred, div, px,
|
|
||||||
relative,
|
|
||||||
};
|
|
||||||
use lsp_types::{CompletionItem, CompletionTextEdit};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
|
|
||||||
const MAX_MENU_WIDTH: Pixels = px(320.);
|
|
||||||
const MAX_MENU_HEIGHT: Pixels = px(240.);
|
|
||||||
const POPOVER_GAP: Pixels = px(4.);
|
|
||||||
|
|
||||||
use crate::input::popovers::{editor_popover, render_markdown};
|
|
||||||
use crate::input::{self, InputState, RopeExt};
|
|
||||||
use crate::list::{List, ListDelegate, ListEvent, ListState};
|
|
||||||
use crate::{IndexPath, Selectable, actions, h_flex};
|
|
||||||
|
|
||||||
struct ContextMenuDelegate {
|
|
||||||
query: SharedString,
|
|
||||||
menu: Entity<CompletionMenu>,
|
|
||||||
items: Vec<Rc<CompletionItem>>,
|
|
||||||
selected_ix: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContextMenuDelegate {
|
|
||||||
fn set_items(&mut self, items: Vec<CompletionItem>) {
|
|
||||||
self.items = items.into_iter().map(Rc::new).collect();
|
|
||||||
self.selected_ix = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected_item(&self) -> Option<&Rc<CompletionItem>> {
|
|
||||||
self.items.get(self.selected_ix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
struct CompletionMenuItem {
|
|
||||||
ix: usize,
|
|
||||||
item: Rc<CompletionItem>,
|
|
||||||
children: Vec<AnyElement>,
|
|
||||||
selected: bool,
|
|
||||||
highlight_prefix: SharedString,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CompletionMenuItem {
|
|
||||||
fn new(ix: usize, item: Rc<CompletionItem>) -> Self {
|
|
||||||
Self {
|
|
||||||
ix,
|
|
||||||
item,
|
|
||||||
children: vec![],
|
|
||||||
selected: false,
|
|
||||||
highlight_prefix: "".into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlight_prefix(mut self, s: impl Into<SharedString>) -> Self {
|
|
||||||
self.highlight_prefix = s.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Selectable for CompletionMenuItem {
|
|
||||||
fn selected(mut self, selected: bool) -> Self {
|
|
||||||
self.selected = selected;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_selected(&self) -> bool {
|
|
||||||
self.selected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParentElement for CompletionMenuItem {
|
|
||||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
||||||
self.children.extend(elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for CompletionMenuItem {
|
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
let item = self.item;
|
|
||||||
|
|
||||||
let matched_len = item
|
|
||||||
.filter_text
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| s.len())
|
|
||||||
.unwrap_or(self.highlight_prefix.len())
|
|
||||||
.min(item.label.len());
|
|
||||||
|
|
||||||
let highlights = vec![(
|
|
||||||
0..matched_len,
|
|
||||||
HighlightStyle {
|
|
||||||
color: Some(cx.theme().selection),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)];
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.id(self.ix)
|
|
||||||
.gap_2()
|
|
||||||
.p_1()
|
|
||||||
.text_xs()
|
|
||||||
.line_height(relative(1.))
|
|
||||||
.rounded(cx.theme().radius.half())
|
|
||||||
.when(item.deprecated.unwrap_or(false), |this| this.line_through())
|
|
||||||
.hover(|this| this.bg(cx.theme().secondary_hover))
|
|
||||||
.when(self.selected, |this| {
|
|
||||||
this.bg(cx.theme().secondary_background)
|
|
||||||
.text_color(cx.theme().secondary_foreground)
|
|
||||||
})
|
|
||||||
.child(div().child(StyledText::new(item.label.clone()).with_highlights(highlights)))
|
|
||||||
.children(self.children)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for ContextMenuDelegate {}
|
|
||||||
|
|
||||||
impl ListDelegate for ContextMenuDelegate {
|
|
||||||
type Item = CompletionMenuItem;
|
|
||||||
|
|
||||||
fn items_count(&self, _: usize, _: &gpui::App) -> usize {
|
|
||||||
self.items.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_item(
|
|
||||||
&mut self,
|
|
||||||
ix: crate::IndexPath,
|
|
||||||
_: &mut Window,
|
|
||||||
_: &mut Context<ListState<Self>>,
|
|
||||||
) -> Option<Self::Item> {
|
|
||||||
let item = self.items.get(ix.row)?;
|
|
||||||
Some(CompletionMenuItem::new(ix.row, item.clone()).highlight_prefix(self.query.clone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_selected_index(
|
|
||||||
&mut self,
|
|
||||||
ix: Option<crate::IndexPath>,
|
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut Context<ListState<Self>>,
|
|
||||||
) {
|
|
||||||
self.selected_ix = ix.map(|i| i.row).unwrap_or(0);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
|
|
||||||
let Some(item) = self.selected_item() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.menu.update(cx, |this, cx| {
|
|
||||||
this.select_item(&item, window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A context menu for code completions and code actions.
|
|
||||||
pub struct CompletionMenu {
|
|
||||||
offset: usize,
|
|
||||||
editor: Entity<InputState>,
|
|
||||||
list: Entity<ListState<ContextMenuDelegate>>,
|
|
||||||
open: bool,
|
|
||||||
|
|
||||||
/// The offset of the first character that triggered the completion.
|
|
||||||
pub(crate) trigger_start_offset: Option<usize>,
|
|
||||||
query: SharedString,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CompletionMenu {
|
|
||||||
/// Creates a new `CompletionMenu` with the given offset and completion items.
|
|
||||||
///
|
|
||||||
/// NOTE: This element should not call from InputState::new, unless that will stack overflow.
|
|
||||||
pub(crate) fn new(
|
|
||||||
editor: Entity<InputState>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Entity<Self> {
|
|
||||||
cx.new(|cx| {
|
|
||||||
let view = cx.entity();
|
|
||||||
let menu = ContextMenuDelegate {
|
|
||||||
query: SharedString::default(),
|
|
||||||
menu: view,
|
|
||||||
items: vec![],
|
|
||||||
selected_ix: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let list = cx.new(|cx| ListState::new(menu, window, cx));
|
|
||||||
|
|
||||||
let _subscriptions =
|
|
||||||
vec![
|
|
||||||
cx.subscribe(&list, |this: &mut Self, _, ev: &ListEvent, cx| {
|
|
||||||
match ev {
|
|
||||||
ListEvent::Confirm(_) => {
|
|
||||||
this.hide(cx);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
Self {
|
|
||||||
offset: 0,
|
|
||||||
editor,
|
|
||||||
list,
|
|
||||||
open: false,
|
|
||||||
trigger_start_offset: None,
|
|
||||||
query: SharedString::default(),
|
|
||||||
_subscriptions,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_item(&mut self, item: &CompletionItem, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let offset = self.offset;
|
|
||||||
let item = item.clone();
|
|
||||||
let mut range = self.trigger_start_offset.unwrap_or(self.offset)..self.offset;
|
|
||||||
|
|
||||||
let editor = self.editor.clone();
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |_, cx| {
|
|
||||||
editor.update_in(cx, |editor, window, cx| {
|
|
||||||
editor.completion_inserting = true;
|
|
||||||
|
|
||||||
let mut new_text = item.label.clone();
|
|
||||||
if let Some(text_edit) = item.text_edit.as_ref() {
|
|
||||||
match text_edit {
|
|
||||||
CompletionTextEdit::Edit(edit) => {
|
|
||||||
new_text = edit.new_text.clone();
|
|
||||||
range.start = editor.text.position_to_offset(&edit.range.start);
|
|
||||||
range.end = editor.text.position_to_offset(&edit.range.end);
|
|
||||||
}
|
|
||||||
CompletionTextEdit::InsertAndReplace(edit) => {
|
|
||||||
new_text = edit.new_text.clone();
|
|
||||||
range.start = editor.text.position_to_offset(&edit.replace.start);
|
|
||||||
range.end = editor.text.position_to_offset(&edit.replace.end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if let Some(insert_text) = item.insert_text.clone() {
|
|
||||||
new_text = insert_text;
|
|
||||||
range = offset..offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.replace_text_in_range_silent(
|
|
||||||
Some(editor.range_to_utf16(&range)),
|
|
||||||
&new_text,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
editor.completion_inserting = false;
|
|
||||||
// FIXME: Input not get the focus
|
|
||||||
editor.focus(window, cx);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
self.hide(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn handle_action(
|
|
||||||
&mut self,
|
|
||||||
action: Box<dyn Action>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> bool {
|
|
||||||
if !self.open {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.propagate();
|
|
||||||
if input::Enter::is_primary(&*action) {
|
|
||||||
self.on_action_enter(window, cx);
|
|
||||||
} else if action.partial_eq(&input::Escape) {
|
|
||||||
self.on_action_escape(window, cx);
|
|
||||||
} else if action.partial_eq(&input::MoveUp) {
|
|
||||||
self.on_action_up(window, cx);
|
|
||||||
} else if action.partial_eq(&input::MoveDown) {
|
|
||||||
self.on_action_down(window, cx);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_action_enter(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Some(item) = self.list.read(cx).delegate().selected_item().cloned() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
self.select_item(&item, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_action_escape(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.hide(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_action_up(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.list.update(cx, |this, cx| {
|
|
||||||
this.on_action_select_prev(&actions::SelectUp, window, cx)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_action_down(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.list.update(cx, |this, cx| {
|
|
||||||
this.on_action_select_next(&actions::SelectDown, window, cx)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_open(&self) -> bool {
|
|
||||||
self.open
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hide the completion menu and reset the trigger start offset.
|
|
||||||
pub(crate) fn hide(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.open = false;
|
|
||||||
self.trigger_start_offset = None;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the trigger start offset if it is not already set.
|
|
||||||
pub(crate) fn update_query(&mut self, start_offset: usize, query: impl Into<SharedString>) {
|
|
||||||
if self.trigger_start_offset.is_none() {
|
|
||||||
self.trigger_start_offset = Some(start_offset);
|
|
||||||
}
|
|
||||||
self.query = query.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn show(
|
|
||||||
&mut self,
|
|
||||||
offset: usize,
|
|
||||||
items: impl Into<Vec<CompletionItem>>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let items = items.into();
|
|
||||||
self.offset = offset;
|
|
||||||
self.open = true;
|
|
||||||
self.list.update(cx, |this, cx| {
|
|
||||||
let longest_ix = items
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.max_by_key(|(_, item)| {
|
|
||||||
item.label.len() + item.detail.as_ref().map(|d| d.len()).unwrap_or(0)
|
|
||||||
})
|
|
||||||
.map(|(ix, _)| ix)
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
this.delegate_mut().query = self.query.clone();
|
|
||||||
this.delegate_mut().set_items(items);
|
|
||||||
this.set_selected_index(Some(IndexPath::new(0)), window, cx);
|
|
||||||
this.set_item_to_measure_index(IndexPath::new(longest_ix), window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn origin(&self, cx: &App) -> Option<Point<Pixels>> {
|
|
||||||
let editor = self.editor.read(cx);
|
|
||||||
let Some(last_layout) = editor.last_layout.as_ref() else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let Some(cursor_origin) = last_layout.cursor_bounds.map(|b| b.origin) else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let scroll_origin = self.editor.read(cx).scroll_handle.offset();
|
|
||||||
|
|
||||||
Some(
|
|
||||||
scroll_origin + cursor_origin - editor.input_bounds.origin
|
|
||||||
+ Point::new(-px(4.), last_layout.line_height + px(4.)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for CompletionMenu {
|
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
if !self.open {
|
|
||||||
return Empty.into_any_element();
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.list.read(cx).delegate().items.is_empty() {
|
|
||||||
self.open = false;
|
|
||||||
return Empty.into_any_element();
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(pos) = self.origin(cx) else {
|
|
||||||
return Empty.into_any_element();
|
|
||||||
};
|
|
||||||
|
|
||||||
let selected_documentation = self
|
|
||||||
.list
|
|
||||||
.read(cx)
|
|
||||||
.delegate()
|
|
||||||
.selected_item()
|
|
||||||
.and_then(|item| item.documentation.clone());
|
|
||||||
|
|
||||||
let max_width = MAX_MENU_WIDTH.min(window.bounds().size.width - pos.x);
|
|
||||||
let abs_pos = self.editor.read(cx).input_bounds.origin + pos;
|
|
||||||
let vertical_layout =
|
|
||||||
abs_pos.x + MAX_MENU_WIDTH + POPOVER_GAP + MAX_MENU_WIDTH + POPOVER_GAP
|
|
||||||
> window.bounds().size.width;
|
|
||||||
|
|
||||||
deferred(
|
|
||||||
div()
|
|
||||||
.absolute()
|
|
||||||
.left(pos.x)
|
|
||||||
.top(pos.y)
|
|
||||||
.flex()
|
|
||||||
.flex_row()
|
|
||||||
.gap(POPOVER_GAP)
|
|
||||||
.items_start()
|
|
||||||
.when(vertical_layout, |this| this.flex_col())
|
|
||||||
.child(
|
|
||||||
editor_popover("completion-menu", cx)
|
|
||||||
.max_w(max_width)
|
|
||||||
.min_w(px(120.))
|
|
||||||
.child(List::new(&self.list).max_h(MAX_MENU_HEIGHT)),
|
|
||||||
)
|
|
||||||
.when_some(selected_documentation, |this, documentation| {
|
|
||||||
let mut doc = match documentation {
|
|
||||||
lsp_types::Documentation::String(s) => s.clone(),
|
|
||||||
lsp_types::Documentation::MarkupContent(mc) => mc.value.clone(),
|
|
||||||
};
|
|
||||||
if vertical_layout {
|
|
||||||
doc = doc.split("\n").next().unwrap_or_default().to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.child(
|
|
||||||
div().child(
|
|
||||||
editor_popover("completion-menu", cx)
|
|
||||||
.w(MAX_MENU_WIDTH)
|
|
||||||
.px_2()
|
|
||||||
.child(render_markdown("doc", doc, window, cx)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.on_mouse_down_out(cx.listener(|this, _, _, cx| {
|
|
||||||
this.hide(cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
|
||||||
use gpui::{
|
|
||||||
Anchor, App, AppContext as _, Context, DismissEvent, Entity, IntoElement, MouseDownEvent,
|
|
||||||
ParentElement as _, Pixels, Point, Render, Styled, Subscription, Window, anchored, deferred,
|
|
||||||
div, px,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::input::popovers::ContextMenu;
|
|
||||||
use crate::input::{self, InputState};
|
|
||||||
use crate::menu::PopupMenu;
|
|
||||||
|
|
||||||
/// Context menu for mouse right clicks.
|
|
||||||
pub(crate) struct InputContextMenu {
|
|
||||||
editor: Entity<InputState>,
|
|
||||||
menu: Entity<PopupMenu>,
|
|
||||||
mouse_position: Point<Pixels>,
|
|
||||||
open: bool,
|
|
||||||
|
|
||||||
_subscriptions: Vec<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputState {
|
|
||||||
pub(crate) fn handle_right_click_menu(
|
|
||||||
&mut self,
|
|
||||||
event: &MouseDownEvent,
|
|
||||||
offset: usize,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
// Show Mouse context menu
|
|
||||||
if !self.selected_range.contains(offset) {
|
|
||||||
self.move_to(offset, None, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.context_menu_content = Some(ContextMenu::RightClick(self.context_menu.clone()));
|
|
||||||
|
|
||||||
let is_code_editor = self.mode.is_code_editor();
|
|
||||||
if is_code_editor {
|
|
||||||
self.handle_hover_definition(offset, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_enable = !self.disabled;
|
|
||||||
let has_goto_definition = is_enable && self.lsp.definition_provider.is_some();
|
|
||||||
let has_code_action = is_enable && !self.lsp.code_action_providers.is_empty();
|
|
||||||
let is_selected = !self.selected_range.is_empty();
|
|
||||||
let has_paste = is_enable && cx.read_from_clipboard().is_some();
|
|
||||||
|
|
||||||
let action_context = self.focus_handle.clone();
|
|
||||||
self.context_menu.update(cx, |this, cx| {
|
|
||||||
this.mouse_position = event.position;
|
|
||||||
this.menu.update(cx, |menu, cx| {
|
|
||||||
let new_menu = if let Some(builder) = &self.context_menu_builder {
|
|
||||||
builder(PopupMenu::new(cx), window, cx)
|
|
||||||
} else {
|
|
||||||
PopupMenu::new(cx)
|
|
||||||
.when(is_code_editor, |m| {
|
|
||||||
m.menu_with_enable(
|
|
||||||
"Go to Definition",
|
|
||||||
Box::new(input::GoToDefinition),
|
|
||||||
has_goto_definition,
|
|
||||||
)
|
|
||||||
.menu_with_enable(
|
|
||||||
"Show Code Actions",
|
|
||||||
Box::new(input::ToggleCodeActions),
|
|
||||||
has_code_action,
|
|
||||||
)
|
|
||||||
.separator()
|
|
||||||
})
|
|
||||||
.menu_with_enable("Cut", Box::new(input::Cut), is_enable && is_selected)
|
|
||||||
.menu_with_enable("Copy", Box::new(input::Copy), is_selected)
|
|
||||||
.menu_with_enable("Paste", Box::new(input::Paste), has_paste)
|
|
||||||
.separator()
|
|
||||||
.menu("Select All", Box::new(input::SelectAll))
|
|
||||||
};
|
|
||||||
|
|
||||||
menu.menu_items = new_menu.menu_items;
|
|
||||||
menu.action_context = Some(action_context);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
cx.defer_in(window, |this, _, cx| {
|
|
||||||
this.open = true;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputContextMenu {
|
|
||||||
pub(crate) fn new(
|
|
||||||
editor: Entity<InputState>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Entity<Self> {
|
|
||||||
cx.new(|cx| {
|
|
||||||
let menu = cx.new(|cx| PopupMenu::new(cx).small());
|
|
||||||
|
|
||||||
let _subscriptions = vec![cx.subscribe_in(&menu, window, {
|
|
||||||
move |this: &mut Self, _, _: &DismissEvent, window, cx| {
|
|
||||||
this.close(window, cx);
|
|
||||||
}
|
|
||||||
})];
|
|
||||||
|
|
||||||
Self {
|
|
||||||
editor,
|
|
||||||
menu,
|
|
||||||
mouse_position: Point::default(),
|
|
||||||
open: false,
|
|
||||||
_subscriptions,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn is_open(&self) -> bool {
|
|
||||||
self.open
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn close(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.open = false;
|
|
||||||
self.editor.update(cx, |this, cx| {
|
|
||||||
this.focus(window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for InputContextMenu {
|
|
||||||
fn render(&mut self, _: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
if !self.open {
|
|
||||||
return div().into_any_element();
|
|
||||||
}
|
|
||||||
|
|
||||||
deferred(
|
|
||||||
anchored()
|
|
||||||
.snap_to_window_with_margin(px(8.))
|
|
||||||
.anchor(Anchor::TopLeft)
|
|
||||||
.position(self.mouse_position)
|
|
||||||
.child(div().cursor_default().child(self.menu.clone())),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::{
|
|
||||||
prelude::FluentBuilder as _, px, App, AppContext as _, Bounds, Context, Empty, Entity,
|
|
||||||
IntoElement, Pixels, Point, Render, Styled, Window,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
highlighter::DiagnosticEntry,
|
|
||||||
input::{
|
|
||||||
popovers::{render_markdown, Popover},
|
|
||||||
InputState,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct DiagnosticPopover {
|
|
||||||
state: Entity<InputState>,
|
|
||||||
pub(crate) diagnostic: Rc<DiagnosticEntry>,
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
open: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DiagnosticPopover {
|
|
||||||
pub fn new(
|
|
||||||
diagnostic: &DiagnosticEntry,
|
|
||||||
state: Entity<InputState>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Entity<Self> {
|
|
||||||
let diagnostic = Rc::new(diagnostic.clone());
|
|
||||||
|
|
||||||
cx.new(|_| Self {
|
|
||||||
diagnostic,
|
|
||||||
state,
|
|
||||||
bounds: Bounds::default(),
|
|
||||||
open: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn show(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.open = true;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn hide(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.open = false;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn check_to_hide(&mut self, mouse_position: Point<Pixels>, cx: &mut Context<Self>) {
|
|
||||||
if !self.open {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let padding = px(5.);
|
|
||||||
let bounds = Bounds {
|
|
||||||
origin: self.bounds.origin.map(|v| v - padding),
|
|
||||||
size: self.bounds.size.map(|v| v + padding * 2.),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !bounds.contains(&mouse_position) {
|
|
||||||
self.hide(cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for DiagnosticPopover {
|
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
if !self.open {
|
|
||||||
return Empty.into_any_element();
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = self.diagnostic.message.clone();
|
|
||||||
|
|
||||||
let (border, bg, fg) = (
|
|
||||||
self.diagnostic.severity.border(cx),
|
|
||||||
self.diagnostic.severity.bg(cx),
|
|
||||||
self.diagnostic.severity.fg(cx),
|
|
||||||
);
|
|
||||||
|
|
||||||
Popover::new(
|
|
||||||
"diagnostic-popover",
|
|
||||||
self.state.clone(),
|
|
||||||
self.diagnostic.range.clone(),
|
|
||||||
move |window, cx| render_markdown("message", message.clone(), window, cx),
|
|
||||||
)
|
|
||||||
.when(!self.open, |this| this.invisible())
|
|
||||||
.px_1()
|
|
||||||
.py_0p5()
|
|
||||||
.bg(bg)
|
|
||||||
.text_color(fg)
|
|
||||||
.border_1()
|
|
||||||
.border_color(border)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
use std::{ops::Range, rc::Rc};
|
|
||||||
|
|
||||||
use gpui::{
|
|
||||||
AnyElement, App, AppContext as _, AvailableSpace, Bounds, Element, ElementId, Entity,
|
|
||||||
InteractiveElement, IntoElement, MouseDownEvent, MouseMoveEvent, ParentElement as _, Pixels,
|
|
||||||
Render, StatefulInteractiveElement as _, StyleRefinement, Styled, Window, deferred, div, point,
|
|
||||||
px,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
StyledExt,
|
|
||||||
input::{InputState, popovers::render_markdown},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct HoverPopover {
|
|
||||||
editor: Entity<InputState>,
|
|
||||||
/// The symbol range byte of the hover trigger.
|
|
||||||
pub(crate) symbol_range: Range<usize>,
|
|
||||||
pub(crate) hover: Rc<lsp_types::Hover>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HoverPopover {
|
|
||||||
pub fn new(
|
|
||||||
editor: Entity<InputState>,
|
|
||||||
symbol_range: Range<usize>,
|
|
||||||
hover: &lsp_types::Hover,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Entity<Self> {
|
|
||||||
let hover = Rc::new(hover.clone());
|
|
||||||
|
|
||||||
cx.new(|_| Self {
|
|
||||||
editor,
|
|
||||||
symbol_range,
|
|
||||||
hover,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_same(&self, offset: usize) -> bool {
|
|
||||||
self.symbol_range.contains(&offset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for HoverPopover {
|
|
||||||
fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
|
|
||||||
let contents = match self.hover.contents.clone() {
|
|
||||||
lsp_types::HoverContents::Scalar(scalar) => match scalar {
|
|
||||||
lsp_types::MarkedString::String(s) => s,
|
|
||||||
lsp_types::MarkedString::LanguageString(ls) => ls.value,
|
|
||||||
},
|
|
||||||
lsp_types::HoverContents::Array(arr) => arr
|
|
||||||
.into_iter()
|
|
||||||
.map(|item| match item {
|
|
||||||
lsp_types::MarkedString::String(s) => s,
|
|
||||||
lsp_types::MarkedString::LanguageString(ls) => ls.value,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n\n"),
|
|
||||||
lsp_types::HoverContents::Markup(markup) => markup.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
Popover::new(
|
|
||||||
"hover-popover",
|
|
||||||
self.editor.clone(),
|
|
||||||
self.symbol_range.clone(),
|
|
||||||
move |window, cx| render_markdown("message", contents.clone(), window, cx),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct Popover {
|
|
||||||
id: ElementId,
|
|
||||||
style: StyleRefinement,
|
|
||||||
editor: Entity<InputState>,
|
|
||||||
range: Range<usize>,
|
|
||||||
width_limit: Range<Pixels>,
|
|
||||||
content_builder: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Styled for Popover {
|
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
|
||||||
&mut self.style
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Popover {
|
|
||||||
pub fn new<F, E>(
|
|
||||||
id: impl Into<ElementId>,
|
|
||||||
editor: Entity<InputState>,
|
|
||||||
range: Range<usize>,
|
|
||||||
f: F,
|
|
||||||
) -> Self
|
|
||||||
where
|
|
||||||
F: Fn(&mut Window, &mut App) -> E + 'static,
|
|
||||||
E: IntoElement,
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
id: id.into(),
|
|
||||||
editor,
|
|
||||||
range,
|
|
||||||
style: StyleRefinement::default(),
|
|
||||||
width_limit: px(200.)..px(500.),
|
|
||||||
content_builder: Box::new(move |window, cx| (f)(window, cx).into_any_element()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the bounds of the range in the editor, if it is visible.
|
|
||||||
fn trigger_bounds(&self, cx: &App) -> Option<Bounds<Pixels>> {
|
|
||||||
let editor = self.editor.read(cx);
|
|
||||||
let Some(last_layout) = editor.last_layout.as_ref() else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(last_bounds) = editor.last_bounds else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let (_, _, start_pos) = editor.line_and_position_for_offset(self.range.start);
|
|
||||||
let (_, _, end_pos) = editor.line_and_position_for_offset(self.range.end);
|
|
||||||
|
|
||||||
let Some(start_pos) = start_pos else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let Some(end_pos) = end_pos else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(Bounds::from_corners(
|
|
||||||
last_bounds.origin + start_pos,
|
|
||||||
last_bounds.origin + end_pos + point(px(0.), last_layout.line_height),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoElement for Popover {
|
|
||||||
type Element = Self;
|
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct PopoverLayoutState {
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
element: Option<AnyElement>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element for Popover {
|
|
||||||
type RequestLayoutState = PopoverLayoutState;
|
|
||||||
type PrepaintState = ();
|
|
||||||
|
|
||||||
fn id(&self) -> Option<ElementId> {
|
|
||||||
Some(self.id.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_layout(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
|
||||||
let trigger_bounds = match self.trigger_bounds(cx) {
|
|
||||||
Some(bounds) => bounds,
|
|
||||||
None => {
|
|
||||||
return (
|
|
||||||
div().into_any_element().request_layout(window, cx),
|
|
||||||
PopoverLayoutState {
|
|
||||||
bounds: Bounds::default(),
|
|
||||||
element: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let max_width = self
|
|
||||||
.width_limit
|
|
||||||
.end
|
|
||||||
.min(window.bounds().size.width - SNAP_TO_EDGE * 2)
|
|
||||||
.max(px(200.));
|
|
||||||
let max_height = (window.bounds().size.height - SNAP_TO_EDGE * 2).min(px(320.));
|
|
||||||
|
|
||||||
let mut popover = deferred(
|
|
||||||
div()
|
|
||||||
.id("hover-popover-content")
|
|
||||||
.flex_none()
|
|
||||||
.occlude()
|
|
||||||
.p_1()
|
|
||||||
.text_xs()
|
|
||||||
.popover_style(cx)
|
|
||||||
.shadow_md()
|
|
||||||
.max_w(max_width)
|
|
||||||
.max_h(max_height)
|
|
||||||
.overflow_y_scroll()
|
|
||||||
.refine_style(&self.style)
|
|
||||||
.child((self.content_builder)(window, cx)),
|
|
||||||
)
|
|
||||||
.into_any_element();
|
|
||||||
|
|
||||||
let popover_size = popover.layout_as_root(AvailableSpace::min_size(), window, cx);
|
|
||||||
const SNAP_TO_EDGE: Pixels = px(8.);
|
|
||||||
let top_space = trigger_bounds.top() - SNAP_TO_EDGE;
|
|
||||||
let right_space = window.bounds().size.width - trigger_bounds.left() - SNAP_TO_EDGE;
|
|
||||||
|
|
||||||
let mut pos = point(
|
|
||||||
trigger_bounds.left(),
|
|
||||||
trigger_bounds.top() - popover_size.height,
|
|
||||||
);
|
|
||||||
if popover_size.height > top_space {
|
|
||||||
pos.y = trigger_bounds.bottom();
|
|
||||||
}
|
|
||||||
if popover_size.width > right_space {
|
|
||||||
pos.x = trigger_bounds.right() - popover_size.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut empty = div().into_any_element();
|
|
||||||
let layout_id = empty.request_layout(window, cx);
|
|
||||||
(
|
|
||||||
layout_id,
|
|
||||||
PopoverLayoutState {
|
|
||||||
bounds: Bounds {
|
|
||||||
origin: pos,
|
|
||||||
size: popover_size,
|
|
||||||
},
|
|
||||||
element: Some(popover),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepaint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
_: Bounds<Pixels>,
|
|
||||||
request_layout: &mut Self::RequestLayoutState,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Self::PrepaintState {
|
|
||||||
let bounds = request_layout.bounds;
|
|
||||||
let Some(popover) = request_layout.element.as_mut() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
window.with_absolute_element_offset(bounds.origin, |window| {
|
|
||||||
popover.prepaint(window, cx);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
_: Bounds<Pixels>,
|
|
||||||
request_layout: &mut Self::RequestLayoutState,
|
|
||||||
_: &mut Self::PrepaintState,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
let bounds = request_layout.bounds;
|
|
||||||
let Some(popover) = request_layout.element.as_mut() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
popover.paint(window, cx);
|
|
||||||
|
|
||||||
let editor = self.editor.clone();
|
|
||||||
// Mouse down out to hide.
|
|
||||||
window.on_mouse_event(move |event: &MouseDownEvent, _, _, cx| {
|
|
||||||
if !bounds.contains(&event.position) {
|
|
||||||
let _ = editor.update(cx, |editor, cx| {
|
|
||||||
editor.clear_hover_state(cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mouse out of trigger + popover bounds
|
|
||||||
let editor = self.editor.clone();
|
|
||||||
let trigger_bounds = self.trigger_bounds(cx).unwrap_or(bounds);
|
|
||||||
let keep_open_region = trigger_bounds.union(&bounds);
|
|
||||||
window.on_mouse_event(move |event: &MouseMoveEvent, _, _, cx| {
|
|
||||||
if !keep_open_region.contains(&event.position) {
|
|
||||||
let _ = editor.update(cx, |editor, cx| {
|
|
||||||
editor.clear_hover_state(cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
mod code_action_menu;
|
|
||||||
mod completion_menu;
|
|
||||||
mod context_menu;
|
|
||||||
mod diagnostic_popover;
|
|
||||||
mod hover_popover;
|
|
||||||
|
|
||||||
pub(crate) use code_action_menu::*;
|
|
||||||
pub(crate) use completion_menu::*;
|
|
||||||
pub(crate) use context_menu::*;
|
|
||||||
pub(crate) use diagnostic_popover::*;
|
|
||||||
use gpui::{
|
|
||||||
App, Div, ElementId, Entity, InteractiveElement as _, IntoElement, SharedString, Stateful,
|
|
||||||
StyleRefinement, Styled as _, Window, div, px, rems,
|
|
||||||
};
|
|
||||||
pub(crate) use hover_popover::*;
|
|
||||||
|
|
||||||
use crate::StyledExt as _;
|
|
||||||
|
|
||||||
pub(crate) enum ContextMenu {
|
|
||||||
Completion(Entity<CompletionMenu>),
|
|
||||||
CodeAction(Entity<CodeActionMenu>),
|
|
||||||
RightClick(Entity<InputContextMenu>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContextMenu {
|
|
||||||
pub(crate) fn is_open(&self, cx: &App) -> bool {
|
|
||||||
match self {
|
|
||||||
ContextMenu::Completion(menu) => menu.read(cx).is_open(),
|
|
||||||
ContextMenu::CodeAction(menu) => menu.read(cx).is_open(),
|
|
||||||
ContextMenu::RightClick(menu) => menu.read(cx).is_open(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn render(&self) -> impl IntoElement {
|
|
||||||
match self {
|
|
||||||
ContextMenu::Completion(menu) => menu.clone().into_any_element(),
|
|
||||||
ContextMenu::CodeAction(menu) => menu.clone().into_any_element(),
|
|
||||||
ContextMenu::RightClick(menu) => menu.clone().into_any_element(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -99,9 +99,6 @@ actions!(
|
|||||||
MoveToPreviousWord,
|
MoveToPreviousWord,
|
||||||
MoveToNextWord,
|
MoveToNextWord,
|
||||||
Escape,
|
Escape,
|
||||||
ToggleCodeActions,
|
|
||||||
Search,
|
|
||||||
GoToDefinition,
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -250,14 +247,6 @@ pub(crate) fn init(cx: &mut App) {
|
|||||||
KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)),
|
KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)),
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
KeyBinding::new("ctrl-y", Redo, Some(CONTEXT)),
|
KeyBinding::new("ctrl-y", Redo, Some(CONTEXT)),
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
KeyBinding::new("cmd-.", ToggleCodeActions, Some(CONTEXT)),
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
KeyBinding::new("ctrl-.", ToggleCodeActions, Some(CONTEXT)),
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
KeyBinding::new("cmd-f", Search, Some(CONTEXT)),
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
KeyBinding::new("ctrl-f", Search, Some(CONTEXT)),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,7 +344,6 @@ pub struct InputState {
|
|||||||
pub(super) clean_on_escape: bool,
|
pub(super) clean_on_escape: bool,
|
||||||
pub(super) submit_on_enter: bool,
|
pub(super) submit_on_enter: bool,
|
||||||
pub(super) soft_wrap: bool,
|
pub(super) soft_wrap: bool,
|
||||||
pub(super) show_whitespaces: bool,
|
|
||||||
/// This flag tells the renderer to prefer the end of the current visual line.
|
/// This flag tells the renderer to prefer the end of the current visual line.
|
||||||
pub(crate) cursor_line_end_affinity: bool,
|
pub(crate) cursor_line_end_affinity: bool,
|
||||||
pub(super) pattern: Option<regex::Regex>,
|
pub(super) pattern: Option<regex::Regex>,
|
||||||
@@ -395,7 +383,7 @@ impl InputState {
|
|||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let focus_handle = cx.focus_handle().tab_stop(true);
|
let focus_handle = cx.focus_handle().tab_stop(true);
|
||||||
let blink_cursor = cx.new(|_| BlinkCursor::new());
|
let blink_cursor = cx.new(|_| BlinkCursor::new());
|
||||||
let history = History::new().group_interval(std::time::Duration::from_secs(1));
|
let history = History::new().group_interval(instant::Duration::from_secs(1));
|
||||||
|
|
||||||
let _subscriptions = vec![
|
let _subscriptions = vec![
|
||||||
// Observe the blink cursor to repaint the view when it changes.
|
// Observe the blink cursor to repaint the view when it changes.
|
||||||
@@ -435,7 +423,6 @@ impl InputState {
|
|||||||
clean_on_escape: false,
|
clean_on_escape: false,
|
||||||
submit_on_enter: false,
|
submit_on_enter: false,
|
||||||
soft_wrap: true,
|
soft_wrap: true,
|
||||||
show_whitespaces: false,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
pattern: None,
|
pattern: None,
|
||||||
validate: None,
|
validate: None,
|
||||||
@@ -480,33 +467,6 @@ impl InputState {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set Input to use [`InputMode::CodeEditor`] mode.
|
|
||||||
///
|
|
||||||
/// Default options:
|
|
||||||
///
|
|
||||||
/// - line_number: true
|
|
||||||
/// - tab_size: 2
|
|
||||||
/// - hard_tabs: false
|
|
||||||
/// - height: 100%
|
|
||||||
/// - multi_line: true
|
|
||||||
/// - indent_guides: true
|
|
||||||
///
|
|
||||||
/// If `highlighter` is None, will use the default highlighter.
|
|
||||||
///
|
|
||||||
/// Code Editor aim for help used to simple code editing or display, not a full-featured code editor.
|
|
||||||
///
|
|
||||||
/// ## Features
|
|
||||||
///
|
|
||||||
/// - Syntax Highlighting
|
|
||||||
/// - Auto Indent
|
|
||||||
/// - Line Number
|
|
||||||
/// - Large Text support, up to 50K lines.
|
|
||||||
pub fn code_editor(mut self, language: impl Into<SharedString>) -> Self {
|
|
||||||
let language: SharedString = language.into();
|
|
||||||
self.mode = InputMode::code_editor(language);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set whether search UI allows replacement, default is true.
|
/// Set whether search UI allows replacement, default is true.
|
||||||
pub fn replaceable(mut self, allow: bool) -> Self {
|
pub fn replaceable(mut self, allow: bool) -> Self {
|
||||||
self.replaceable = allow;
|
self.replaceable = allow;
|
||||||
@@ -519,49 +479,6 @@ impl InputState {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set enable/disable code folding, only for [`InputMode::CodeEditor`] mode.
|
|
||||||
///
|
|
||||||
/// Default: true
|
|
||||||
pub fn folding(mut self, folding: bool) -> Self {
|
|
||||||
debug_assert!(self.mode.is_code_editor());
|
|
||||||
if let InputMode::CodeEditor { folding: f, .. } = &mut self.mode {
|
|
||||||
*f = folding;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set code folding at runtime, only for [`InputMode::CodeEditor`] mode.
|
|
||||||
///
|
|
||||||
/// When disabling, all existing folds are cleared.
|
|
||||||
pub fn set_folding(&mut self, folding: bool, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
debug_assert!(self.mode.is_code_editor());
|
|
||||||
if let InputMode::CodeEditor { folding: f, .. } = &mut self.mode {
|
|
||||||
*f = folding;
|
|
||||||
}
|
|
||||||
if !folding {
|
|
||||||
self.display_map.clear_folds();
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set enable/disable line number, only for [`InputMode::CodeEditor`] mode.
|
|
||||||
pub fn line_number(mut self, line_number: bool) -> Self {
|
|
||||||
debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
|
|
||||||
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
|
|
||||||
*l = line_number;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set line number, only for [`InputMode::CodeEditor`] mode.
|
|
||||||
pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
|
|
||||||
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
|
|
||||||
*l = line_number;
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the number of rows for the multi-line Textarea.
|
/// Set the number of rows for the multi-line Textarea.
|
||||||
///
|
///
|
||||||
/// This is only used when `multi_line` is set to true.
|
/// This is only used when `multi_line` is set to true.
|
||||||
@@ -569,9 +486,7 @@ impl InputState {
|
|||||||
/// default: 2
|
/// default: 2
|
||||||
pub fn rows(mut self, rows: usize) -> Self {
|
pub fn rows(mut self, rows: usize) -> Self {
|
||||||
match &mut self.mode {
|
match &mut self.mode {
|
||||||
InputMode::PlainText { rows: r, .. } | InputMode::CodeEditor { rows: r, .. } => {
|
InputMode::PlainText { rows: r, .. } => *r = rows,
|
||||||
*r = rows
|
|
||||||
}
|
|
||||||
InputMode::AutoGrow {
|
InputMode::AutoGrow {
|
||||||
max_rows: max_r,
|
max_rows: max_r,
|
||||||
rows: r,
|
rows: r,
|
||||||
@@ -584,31 +499,6 @@ impl InputState {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set highlighter language for for [`InputMode::CodeEditor`] mode.
|
|
||||||
pub fn set_highlighter(
|
|
||||||
&mut self,
|
|
||||||
new_language: impl Into<SharedString>,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if let InputMode::CodeEditor {
|
|
||||||
language,
|
|
||||||
parse_task,
|
|
||||||
..
|
|
||||||
} = &mut self.mode
|
|
||||||
{
|
|
||||||
*language = new_language.into();
|
|
||||||
parse_task.borrow_mut().take();
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset_highlighter(&mut self, cx: &mut Context<Self>) {
|
|
||||||
if let InputMode::CodeEditor { parse_task, .. } = &mut self.mode {
|
|
||||||
parse_task.borrow_mut().take();
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set placeholder
|
/// Set placeholder
|
||||||
pub fn set_placeholder(
|
pub fn set_placeholder(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -726,7 +616,6 @@ impl InputState {
|
|||||||
let text: SharedString = text.into();
|
let text: SharedString = text.into();
|
||||||
let range = 0..self.text.chars().map(|c| c.len_utf16()).sum();
|
let range = 0..self.text.chars().map(|c| c.len_utf16()).sum();
|
||||||
self.replace_text_in_range_silent(Some(range), &text, window, cx);
|
self.replace_text_in_range_silent(Some(range), &text, window, cx);
|
||||||
self.reset_highlighter(cx);
|
|
||||||
self.disabled = was_disabled;
|
self.disabled = was_disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -779,12 +668,6 @@ impl InputState {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set whether to show whitespace characters.
|
|
||||||
pub fn show_whitespaces(mut self, show: bool) -> Self {
|
|
||||||
self.show_whitespaces = show;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the soft wrap mode for multi-line input, default is true.
|
/// Update the soft wrap mode for multi-line input, default is true.
|
||||||
pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
debug_assert!(self.mode.is_multi_line());
|
debug_assert!(self.mode.is_multi_line());
|
||||||
@@ -808,12 +691,6 @@ impl InputState {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update whether to show whitespace characters.
|
|
||||||
pub fn set_show_whitespaces(&mut self, show: bool, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.show_whitespaces = show;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the regular expression pattern of the input field.
|
/// Set the regular expression pattern of the input field.
|
||||||
///
|
///
|
||||||
/// Only for [`InputMode::SingleLine`] mode.
|
/// Only for [`InputMode::SingleLine`] mode.
|
||||||
@@ -1038,7 +915,7 @@ impl InputState {
|
|||||||
let row = self.text.offset_to_point(self.cursor()).row;
|
let row = self.text.offset_to_point(self.cursor()).row;
|
||||||
let logical_start = self.text.line_start_offset(row);
|
let logical_start = self.text.line_start_offset(row);
|
||||||
|
|
||||||
if self.soft_wrap && self.mode.is_code_editor() {
|
if self.soft_wrap {
|
||||||
let wrap_point = self.display_map.offset_to_wrap_display_point(self.cursor());
|
let wrap_point = self.display_map.offset_to_wrap_display_point(self.cursor());
|
||||||
if let Some(line) = self.display_map.lines().get(row)
|
if let Some(line) = self.display_map.lines().get(row)
|
||||||
&& let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
|
&& let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
|
||||||
@@ -1066,7 +943,7 @@ impl InputState {
|
|||||||
let logical_start = self.text.line_start_offset(row);
|
let logical_start = self.text.line_start_offset(row);
|
||||||
let logical_end = self.text.line_end_offset(row);
|
let logical_end = self.text.line_end_offset(row);
|
||||||
|
|
||||||
if self.soft_wrap && self.mode.is_code_editor() {
|
if self.soft_wrap {
|
||||||
let wrap_point = self.display_map.offset_to_wrap_display_point(self.cursor());
|
let wrap_point = self.display_map.offset_to_wrap_display_point(self.cursor());
|
||||||
if let Some(line) = self.display_map.lines().get(row)
|
if let Some(line) = self.display_map.lines().get(row)
|
||||||
&& let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
|
&& let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
|
||||||
@@ -1264,7 +1141,7 @@ impl InputState {
|
|||||||
|
|
||||||
if insert_newline {
|
if insert_newline {
|
||||||
// Get current line indent
|
// Get current line indent
|
||||||
let indent = if self.mode.is_code_editor() {
|
let indent = if self.mode.is_indentable() {
|
||||||
self.indent_of_next_line()
|
self.indent_of_next_line()
|
||||||
} else {
|
} else {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
@@ -1465,11 +1342,7 @@ impl InputState {
|
|||||||
|
|
||||||
// Check if row_offset_y is out of the viewport
|
// Check if row_offset_y is out of the viewport
|
||||||
// If row offset is not in the viewport, scroll to make it visible
|
// If row offset is not in the viewport, scroll to make it visible
|
||||||
let edge_height = if direction.is_some() && self.mode.is_code_editor() {
|
let edge_height = line_height;
|
||||||
3 * line_height
|
|
||||||
} else {
|
|
||||||
line_height
|
|
||||||
};
|
|
||||||
if row_offset_y - edge_height + line_height < -scroll_offset.y {
|
if row_offset_y - edge_height + line_height < -scroll_offset.y {
|
||||||
// Scroll up
|
// Scroll up
|
||||||
scroll_offset.y = -row_offset_y + edge_height - line_height;
|
scroll_offset.y = -row_offset_y + edge_height - line_height;
|
||||||
@@ -1749,34 +1622,6 @@ impl InputState {
|
|||||||
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
|
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If offset falls on a hidden (folded) line, clamp backward to the end of
|
|
||||||
/// the fold header line (last visible position before the fold).
|
|
||||||
fn clamp_offset_to_visible_backward(&self, offset: usize) -> usize {
|
|
||||||
let line = self.text.offset_to_point(offset).row;
|
|
||||||
if self.display_map.is_buffer_line_hidden(line) {
|
|
||||||
for fold in self.display_map.folded_ranges() {
|
|
||||||
if line > fold.start_line && line <= fold.end_line {
|
|
||||||
return self.text.line_end_offset(fold.start_line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
offset
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If offset falls on a hidden (folded) line, clamp forward to the start of
|
|
||||||
/// the fold end line (first visible position after the fold).
|
|
||||||
fn clamp_offset_to_visible_forward(&self, offset: usize) -> usize {
|
|
||||||
let line = self.text.offset_to_point(offset).row;
|
|
||||||
if self.display_map.is_buffer_line_hidden(line) {
|
|
||||||
for fold in self.display_map.folded_ranges() {
|
|
||||||
if line > fold.start_line && line <= fold.end_line {
|
|
||||||
return self.text.line_start_offset(fold.end_line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
offset
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn previous_boundary(&self, offset: usize) -> usize {
|
pub(super) fn previous_boundary(&self, offset: usize) -> usize {
|
||||||
let mut offset = self.text.clip_offset(offset.saturating_sub(1), Bias::Left);
|
let mut offset = self.text.clip_offset(offset.saturating_sub(1), Bias::Left);
|
||||||
if let Some(ch) = self.text.char_at(offset)
|
if let Some(ch) = self.text.char_at(offset)
|
||||||
@@ -1785,7 +1630,7 @@ impl InputState {
|
|||||||
offset -= 1;
|
offset -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.clamp_offset_to_visible_backward(offset)
|
offset
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn next_boundary(&self, offset: usize) -> usize {
|
pub(super) fn next_boundary(&self, offset: usize) -> usize {
|
||||||
@@ -1796,7 +1641,7 @@ impl InputState {
|
|||||||
offset += 1;
|
offset += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.clamp_offset_to_visible_forward(offset)
|
offset
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the true to let InputElement to render cursor, when Input is focused and current BlinkCursor is visible.
|
/// Returns the true to let InputElement to render cursor, when Input is focused and current BlinkCursor is visible.
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -9,6 +8,7 @@ use gpui::{
|
|||||||
SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task,
|
SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task,
|
||||||
UniformListScrollHandle, Window, div, px, size, uniform_list,
|
UniformListScrollHandle, Window, div, px, size, uniform_list,
|
||||||
};
|
};
|
||||||
|
use instant::Duration;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
|
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ impl<T: 'static, E: 'static + Render> Element for ResizeHandle<T, E> {
|
|||||||
cx.theme().border
|
cx.theme().border
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut el = div()
|
let mut ele = div()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
.occlude()
|
.occlude()
|
||||||
.absolute()
|
.absolute()
|
||||||
@@ -160,16 +160,15 @@ impl<T: 'static, E: 'static + Render> Element for ResizeHandle<T, E> {
|
|||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.bg(bg_color)
|
|
||||||
.group_hover("handle", |this| this.bg(bg_color))
|
.group_hover("handle", |this| this.bg(bg_color))
|
||||||
.when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
|
.when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
|
||||||
.when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)),
|
.when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)),
|
||||||
)
|
)
|
||||||
.into_any_element();
|
.into_any_element();
|
||||||
|
|
||||||
let layout_id = el.request_layout(window, cx);
|
let layout_id = ele.request_layout(window, cx);
|
||||||
|
|
||||||
((layout_id, el), state)
|
((layout_id, ele), state)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use std::cell::Cell;
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::panic::Location;
|
use std::panic::Location;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Anchor, App, Axis, BorderStyle, Bounds, ContentMask, CursorStyle, Edges, Element, ElementId,
|
Anchor, App, Axis, BorderStyle, Bounds, ContentMask, CursorStyle, Edges, Element, ElementId,
|
||||||
@@ -11,6 +10,7 @@ use gpui::{
|
|||||||
Position, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, fill,
|
Position, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, fill,
|
||||||
point, px, relative, size,
|
point, px, relative, size,
|
||||||
};
|
};
|
||||||
|
use instant::{Duration, Instant};
|
||||||
use theme::{ActiveTheme, AxisExt, ScrollbarMode};
|
use theme::{ActiveTheme, AxisExt, ScrollbarMode};
|
||||||
|
|
||||||
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
|
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
|
||||||
@@ -157,7 +157,7 @@ impl ScrollbarStateInner {
|
|||||||
let mut state = *self;
|
let mut state = *self;
|
||||||
state.hovered_axis = axis;
|
state.hovered_axis = axis;
|
||||||
if axis.is_some() {
|
if axis.is_some() {
|
||||||
state.last_scroll_time = Some(std::time::Instant::now());
|
state.last_scroll_time = Some(instant::Instant::now());
|
||||||
}
|
}
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
@@ -166,7 +166,7 @@ impl ScrollbarStateInner {
|
|||||||
let mut state = *self;
|
let mut state = *self;
|
||||||
state.hovered_on_thumb = axis;
|
state.hovered_on_thumb = axis;
|
||||||
if self.is_scrollbar_visible() && axis.is_some() {
|
if self.is_scrollbar_visible() && axis.is_some() {
|
||||||
state.last_scroll_time = Some(std::time::Instant::now());
|
state.last_scroll_time = Some(instant::Instant::now());
|
||||||
}
|
}
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
bounce, div, ease_in_out, Animation, AnimationExt, IntoElement, RenderOnce, StyleRefinement,
|
bounce, div, ease_in_out, Animation, AnimationExt, IntoElement, RenderOnce, StyleRefinement,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ pub trait StyledExt: Styled + Sized {
|
|||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.shadow_md()
|
.shadow_md()
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius_lg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
|||||||
+18
-494
@@ -2,396 +2,15 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, ClickEvent, Div, Edges, Hsla, InteractiveElement, IntoElement, MouseButton,
|
AnyElement, App, ClickEvent, Div, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
||||||
ParentElement, Pixels, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, px, relative,
|
||||||
div, px, relative,
|
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||||
|
|
||||||
use crate::{Icon, IconName, Selectable, Sizable, Size, StyledExt, h_flex};
|
use crate::{Icon, IconName, Selectable, h_flex};
|
||||||
|
|
||||||
pub mod tab_bar;
|
pub mod tab_bar;
|
||||||
|
|
||||||
/// Tab variants.
|
|
||||||
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum TabVariant {
|
|
||||||
#[default]
|
|
||||||
Tab,
|
|
||||||
Outline,
|
|
||||||
Pill,
|
|
||||||
Segmented,
|
|
||||||
Underline,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TabVariant {
|
|
||||||
fn height(&self, size: Size) -> Pixels {
|
|
||||||
match size {
|
|
||||||
Size::XSmall => match self {
|
|
||||||
TabVariant::Underline => px(26.),
|
|
||||||
_ => px(20.),
|
|
||||||
},
|
|
||||||
Size::Small => match self {
|
|
||||||
TabVariant::Underline => px(30.),
|
|
||||||
_ => px(24.),
|
|
||||||
},
|
|
||||||
Size::Large => match self {
|
|
||||||
TabVariant::Underline => px(44.),
|
|
||||||
_ => px(36.),
|
|
||||||
},
|
|
||||||
_ => match self {
|
|
||||||
TabVariant::Underline => px(36.),
|
|
||||||
_ => px(32.),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inner_height(&self, size: Size) -> Pixels {
|
|
||||||
match size {
|
|
||||||
Size::XSmall => match self {
|
|
||||||
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(18.),
|
|
||||||
TabVariant::Segmented => px(16.),
|
|
||||||
TabVariant::Underline => px(20.),
|
|
||||||
},
|
|
||||||
Size::Small => match self {
|
|
||||||
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(22.),
|
|
||||||
TabVariant::Segmented => px(18.),
|
|
||||||
TabVariant::Underline => px(22.),
|
|
||||||
},
|
|
||||||
Size::Large => match self {
|
|
||||||
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(36.),
|
|
||||||
TabVariant::Segmented => px(28.),
|
|
||||||
TabVariant::Underline => px(32.),
|
|
||||||
},
|
|
||||||
_ => match self {
|
|
||||||
TabVariant::Tab => px(30.),
|
|
||||||
TabVariant::Outline | TabVariant::Pill => px(26.),
|
|
||||||
TabVariant::Segmented => px(24.),
|
|
||||||
TabVariant::Underline => px(26.),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Default px(12) to match panel px_3, See [`crate::dock::TabPanel`]
|
|
||||||
fn inner_paddings(&self, size: Size) -> Edges<Pixels> {
|
|
||||||
let mut padding_x = match size {
|
|
||||||
Size::XSmall => px(8.),
|
|
||||||
Size::Small => px(10.),
|
|
||||||
Size::Large => px(16.),
|
|
||||||
_ => px(12.),
|
|
||||||
};
|
|
||||||
|
|
||||||
if matches!(self, TabVariant::Underline) {
|
|
||||||
padding_x = px(0.);
|
|
||||||
}
|
|
||||||
|
|
||||||
Edges {
|
|
||||||
left: padding_x,
|
|
||||||
right: padding_x,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inner_margins(&self, size: Size) -> Edges<Pixels> {
|
|
||||||
match size {
|
|
||||||
Size::XSmall => match self {
|
|
||||||
TabVariant::Underline => Edges {
|
|
||||||
top: px(1.),
|
|
||||||
bottom: px(2.),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
_ => Edges::all(px(0.)),
|
|
||||||
},
|
|
||||||
Size::Small => match self {
|
|
||||||
TabVariant::Underline => Edges {
|
|
||||||
top: px(2.),
|
|
||||||
bottom: px(3.),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
_ => Edges::all(px(0.)),
|
|
||||||
},
|
|
||||||
Size::Large => match self {
|
|
||||||
TabVariant::Underline => Edges {
|
|
||||||
top: px(5.),
|
|
||||||
bottom: px(6.),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
_ => Edges::all(px(0.)),
|
|
||||||
},
|
|
||||||
_ => match self {
|
|
||||||
TabVariant::Underline => Edges {
|
|
||||||
top: px(3.),
|
|
||||||
bottom: px(4.),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
_ => Edges::all(px(0.)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normal(&self, cx: &App) -> TabStyle {
|
|
||||||
match self {
|
|
||||||
TabVariant::Tab => TabStyle {
|
|
||||||
fg: cx.theme().tab_foreground,
|
|
||||||
bg: gpui::transparent_black(),
|
|
||||||
borders: Edges {
|
|
||||||
left: px(1.),
|
|
||||||
right: px(1.),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
border_color: gpui::transparent_black(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Outline => TabStyle {
|
|
||||||
fg: cx.theme().tab_foreground,
|
|
||||||
bg: gpui::transparent_black(),
|
|
||||||
borders: Edges::all(px(1.)),
|
|
||||||
border_color: cx.theme().border,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Pill => TabStyle {
|
|
||||||
fg: cx.theme().text,
|
|
||||||
bg: gpui::transparent_black(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Segmented => TabStyle {
|
|
||||||
fg: cx.theme().tab_foreground,
|
|
||||||
bg: gpui::transparent_black(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Underline => TabStyle {
|
|
||||||
fg: cx.theme().tab_foreground,
|
|
||||||
bg: gpui::transparent_black(),
|
|
||||||
inner_bg: gpui::transparent_black(),
|
|
||||||
borders: Edges {
|
|
||||||
bottom: px(2.),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
border_color: gpui::transparent_black(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hovered(&self, selected: bool, cx: &App) -> TabStyle {
|
|
||||||
match self {
|
|
||||||
TabVariant::Tab => TabStyle {
|
|
||||||
fg: cx.theme().tab_foreground,
|
|
||||||
bg: gpui::transparent_black(),
|
|
||||||
borders: Edges {
|
|
||||||
left: px(1.),
|
|
||||||
right: px(1.),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
border_color: gpui::transparent_black(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Outline => TabStyle {
|
|
||||||
fg: cx.theme().secondary_foreground,
|
|
||||||
bg: cx.theme().secondary_hover,
|
|
||||||
borders: Edges::all(px(1.)),
|
|
||||||
border_color: cx.theme().border,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Pill => TabStyle {
|
|
||||||
fg: cx.theme().secondary_foreground,
|
|
||||||
bg: cx.theme().secondary_background,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Segmented => TabStyle {
|
|
||||||
fg: cx.theme().tab_foreground,
|
|
||||||
bg: gpui::transparent_black(),
|
|
||||||
inner_bg: if selected {
|
|
||||||
cx.theme().background
|
|
||||||
} else {
|
|
||||||
gpui::transparent_black()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Underline => TabStyle {
|
|
||||||
fg: cx.theme().tab_foreground,
|
|
||||||
bg: gpui::transparent_black(),
|
|
||||||
inner_bg: gpui::transparent_black(),
|
|
||||||
borders: Edges {
|
|
||||||
bottom: px(2.),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
border_color: gpui::transparent_black(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected(&self, cx: &App) -> TabStyle {
|
|
||||||
match self {
|
|
||||||
TabVariant::Tab => TabStyle {
|
|
||||||
fg: cx.theme().tab_active_foreground,
|
|
||||||
bg: cx.theme().tab_active_background,
|
|
||||||
borders: Edges {
|
|
||||||
left: px(1.),
|
|
||||||
right: px(1.),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
border_color: cx.theme().border,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Outline => TabStyle {
|
|
||||||
fg: cx.theme().text_accent,
|
|
||||||
bg: gpui::transparent_black(),
|
|
||||||
borders: Edges::all(px(1.)),
|
|
||||||
border_color: cx.theme().element_active,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Pill => TabStyle {
|
|
||||||
fg: cx.theme().element_foreground,
|
|
||||||
bg: cx.theme().element_background,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Segmented => TabStyle {
|
|
||||||
fg: cx.theme().tab_active_foreground,
|
|
||||||
bg: gpui::transparent_black(),
|
|
||||||
inner_bg: cx.theme().background,
|
|
||||||
shadow: true,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Underline => TabStyle {
|
|
||||||
fg: cx.theme().tab_active_foreground,
|
|
||||||
bg: gpui::transparent_black(),
|
|
||||||
borders: Edges {
|
|
||||||
bottom: px(2.),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
border_color: cx.theme().element_active,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn disabled(&self, selected: bool, cx: &App) -> TabStyle {
|
|
||||||
match self {
|
|
||||||
TabVariant::Tab => TabStyle {
|
|
||||||
fg: cx.theme().text_muted,
|
|
||||||
bg: gpui::transparent_black(),
|
|
||||||
border_color: if selected {
|
|
||||||
cx.theme().border
|
|
||||||
} else {
|
|
||||||
gpui::transparent_black()
|
|
||||||
},
|
|
||||||
borders: Edges {
|
|
||||||
left: px(1.),
|
|
||||||
right: px(1.),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Outline => TabStyle {
|
|
||||||
fg: cx.theme().text_muted,
|
|
||||||
bg: gpui::transparent_black(),
|
|
||||||
borders: Edges::all(px(1.)),
|
|
||||||
border_color: if selected {
|
|
||||||
cx.theme().element_active
|
|
||||||
} else {
|
|
||||||
cx.theme().border
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Pill => TabStyle {
|
|
||||||
fg: if selected {
|
|
||||||
cx.theme().element_foreground.opacity(0.5)
|
|
||||||
} else {
|
|
||||||
cx.theme().text_muted
|
|
||||||
},
|
|
||||||
bg: if selected {
|
|
||||||
cx.theme().element_background.opacity(0.5)
|
|
||||||
} else {
|
|
||||||
gpui::transparent_black()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Segmented => TabStyle {
|
|
||||||
fg: cx.theme().text_muted,
|
|
||||||
bg: cx.theme().tab_background,
|
|
||||||
inner_bg: if selected {
|
|
||||||
cx.theme().background
|
|
||||||
} else {
|
|
||||||
gpui::transparent_black()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
TabVariant::Underline => TabStyle {
|
|
||||||
fg: cx.theme().text_muted,
|
|
||||||
bg: gpui::transparent_black(),
|
|
||||||
border_color: if selected {
|
|
||||||
cx.theme().border
|
|
||||||
} else {
|
|
||||||
gpui::transparent_black()
|
|
||||||
},
|
|
||||||
borders: Edges {
|
|
||||||
bottom: px(2.),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn tab_bar_radius(&self, size: Size, cx: &App) -> Pixels {
|
|
||||||
if *self != TabVariant::Segmented {
|
|
||||||
return px(0.);
|
|
||||||
}
|
|
||||||
|
|
||||||
match size {
|
|
||||||
Size::XSmall | Size::Small => cx.theme().radius,
|
|
||||||
Size::Large => cx.theme().radius_lg,
|
|
||||||
_ => cx.theme().radius_lg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn radius(&self, size: Size, cx: &App) -> Pixels {
|
|
||||||
match self {
|
|
||||||
TabVariant::Outline | TabVariant::Pill => px(99.),
|
|
||||||
TabVariant::Segmented => match size {
|
|
||||||
Size::XSmall | Size::Small => cx.theme().radius,
|
|
||||||
Size::Large => cx.theme().radius_lg,
|
|
||||||
_ => cx.theme().radius_lg,
|
|
||||||
},
|
|
||||||
_ => px(0.),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inner_radius(&self, size: Size, cx: &App) -> Pixels {
|
|
||||||
match self {
|
|
||||||
TabVariant::Segmented => match size {
|
|
||||||
Size::Large => self.tab_bar_radius(size, cx) - px(3.),
|
|
||||||
_ => self.tab_bar_radius(size, cx) - px(2.),
|
|
||||||
},
|
|
||||||
_ => px(0.),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
struct TabStyle {
|
|
||||||
borders: Edges<Pixels>,
|
|
||||||
border_color: Hsla,
|
|
||||||
bg: Hsla,
|
|
||||||
fg: Hsla,
|
|
||||||
shadow: bool,
|
|
||||||
inner_bg: Hsla,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TabStyle {
|
|
||||||
fn default() -> Self {
|
|
||||||
TabStyle {
|
|
||||||
borders: Edges::all(px(0.)),
|
|
||||||
border_color: gpui::transparent_white(),
|
|
||||||
bg: gpui::transparent_white(),
|
|
||||||
fg: gpui::transparent_white(),
|
|
||||||
shadow: false,
|
|
||||||
inner_bg: gpui::transparent_white(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
/// A Tab element for the [`super::TabBar`].
|
/// A Tab element for the [`super::TabBar`].
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
@@ -404,8 +23,6 @@ pub struct Tab {
|
|||||||
pub(super) tab_bar_prefix: Option<bool>,
|
pub(super) tab_bar_prefix: Option<bool>,
|
||||||
suffix: Option<AnyElement>,
|
suffix: Option<AnyElement>,
|
||||||
children: Vec<AnyElement>,
|
children: Vec<AnyElement>,
|
||||||
variant: TabVariant,
|
|
||||||
size: Size,
|
|
||||||
pub(super) disabled: bool,
|
pub(super) disabled: bool,
|
||||||
pub(super) selected: bool,
|
pub(super) selected: bool,
|
||||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||||
@@ -454,8 +71,6 @@ impl Default for Tab {
|
|||||||
selected: false,
|
selected: false,
|
||||||
prefix: None,
|
prefix: None,
|
||||||
suffix: None,
|
suffix: None,
|
||||||
variant: TabVariant::default(),
|
|
||||||
size: Size::default(),
|
|
||||||
on_click: None,
|
on_click: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -479,36 +94,6 @@ impl Tab {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set Tab Variant.
|
|
||||||
pub fn with_variant(mut self, variant: TabVariant) -> Self {
|
|
||||||
self.variant = variant;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use Pill variant.
|
|
||||||
pub fn pill(mut self) -> Self {
|
|
||||||
self.variant = TabVariant::Pill;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use outline variant.
|
|
||||||
pub fn outline(mut self) -> Self {
|
|
||||||
self.variant = TabVariant::Outline;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use Segmented variant.
|
|
||||||
pub fn segmented(mut self) -> Self {
|
|
||||||
self.variant = TabVariant::Segmented;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use Underline variant.
|
|
||||||
pub fn underline(mut self) -> Self {
|
|
||||||
self.variant = TabVariant::Underline;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the left side of the tab
|
/// Set the left side of the tab
|
||||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||||
self.prefix = Some(prefix.into_any_element());
|
self.prefix = Some(prefix.into_any_element());
|
||||||
@@ -580,42 +165,16 @@ impl Styled for Tab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sizable for Tab {
|
|
||||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
|
||||||
self.size = size.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for Tab {
|
impl RenderOnce for Tab {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let mut tab_style = if self.selected {
|
let fg = if self.disabled {
|
||||||
self.variant.selected(cx)
|
cx.theme().text_muted
|
||||||
|
} else if self.selected {
|
||||||
|
cx.theme().tab_active_foreground
|
||||||
} else {
|
} else {
|
||||||
self.variant.normal(cx)
|
cx.theme().tab_foreground
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut hover_style = self.variant.hovered(self.selected, cx);
|
|
||||||
|
|
||||||
if self.disabled {
|
|
||||||
tab_style = self.variant.disabled(self.selected, cx);
|
|
||||||
hover_style = self.variant.disabled(self.selected, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
let tab_bar_prefix = self.tab_bar_prefix.unwrap_or_default();
|
|
||||||
|
|
||||||
if !tab_bar_prefix && self.ix == 0 && self.variant == TabVariant::Tab {
|
|
||||||
tab_style.borders.left = px(0.);
|
|
||||||
hover_style.borders.left = px(0.);
|
|
||||||
}
|
|
||||||
|
|
||||||
let radius = self.variant.radius(self.size, cx);
|
|
||||||
let inner_radius = self.variant.inner_radius(self.size, cx);
|
|
||||||
let inner_paddings = self.variant.inner_paddings(self.size);
|
|
||||||
let inner_margins = self.variant.inner_margins(self.size);
|
|
||||||
let inner_height = self.variant.inner_height(self.size);
|
|
||||||
let height = self.variant.height(self.size);
|
|
||||||
|
|
||||||
self.base
|
self.base
|
||||||
.id(self.ix)
|
.id(self.ix)
|
||||||
.flex()
|
.flex()
|
||||||
@@ -623,74 +182,39 @@ impl RenderOnce for Tab {
|
|||||||
.gap_1()
|
.gap_1()
|
||||||
.items_center()
|
.items_center()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.h(height)
|
.h(TABBAR_HEIGHT)
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.text_color(tab_style.fg)
|
.text_color(fg)
|
||||||
.map(|this| match self.size {
|
.text_sm()
|
||||||
Size::XSmall => this.text_xs(),
|
|
||||||
Size::Large => this.text_base(),
|
|
||||||
_ => this.text_sm(),
|
|
||||||
})
|
|
||||||
.bg(tab_style.bg)
|
|
||||||
.border_l(tab_style.borders.left)
|
|
||||||
.border_r(tab_style.borders.right)
|
|
||||||
.border_t(tab_style.borders.top)
|
|
||||||
.border_b(tab_style.borders.bottom)
|
|
||||||
.border_color(tab_style.border_color)
|
|
||||||
.rounded(radius)
|
|
||||||
.when(!self.selected && !self.disabled, |this| {
|
.when(!self.selected && !self.disabled, |this| {
|
||||||
this.hover(|this| {
|
this.hover(|this| this.text_color(cx.theme().secondary_foreground))
|
||||||
this.text_color(hover_style.fg)
|
|
||||||
.bg(hover_style.bg)
|
|
||||||
.border_l(hover_style.borders.left)
|
|
||||||
.border_r(hover_style.borders.right)
|
|
||||||
.border_t(hover_style.borders.top)
|
|
||||||
.border_b(hover_style.borders.bottom)
|
|
||||||
.border_color(hover_style.border_color)
|
|
||||||
.rounded(radius)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.h(inner_height)
|
.h(px(30.))
|
||||||
.line_height(relative(1.))
|
.line_height(relative(1.))
|
||||||
.whitespace_nowrap()
|
.whitespace_nowrap()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.margins(inner_margins)
|
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
|
.px_3()
|
||||||
.map(|this| match self.icon {
|
.map(|this| match self.icon {
|
||||||
Some(icon) => {
|
Some(icon) => this.w(px(38.)).child(icon.size_4()),
|
||||||
this.w(inner_height * 1.25)
|
|
||||||
.child(icon.map(|this| match self.size {
|
|
||||||
Size::XSmall => this.size_2p5(),
|
|
||||||
Size::Small => this.size_3p5(),
|
|
||||||
Size::Large => this.size_4(),
|
|
||||||
_ => this.size_4(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
None => this
|
None => this
|
||||||
.paddings(inner_paddings)
|
|
||||||
.map(|this| match self.label {
|
.map(|this| match self.label {
|
||||||
Some(label) => this.child(label),
|
Some(label) => this.child(label),
|
||||||
None => this,
|
None => this,
|
||||||
})
|
})
|
||||||
.children(self.children),
|
.children(self.children),
|
||||||
})
|
}),
|
||||||
.bg(tab_style.inner_bg)
|
|
||||||
.rounded(inner_radius)
|
|
||||||
.when(tab_style.shadow, |this| this.shadow_xs())
|
|
||||||
.hover(|this| this.bg(hover_style.inner_bg).rounded(inner_radius)),
|
|
||||||
)
|
)
|
||||||
.when_some(self.suffix, |this, suffix| {
|
.when_some(self.suffix, |this, suffix| {
|
||||||
this.child(div().pr_2().child(suffix))
|
this.child(div().pr_2().child(suffix))
|
||||||
})
|
})
|
||||||
.on_mouse_down(MouseButton::Left, |_, _, cx| {
|
.on_mouse_down(MouseButton::Left, |_ev, _window, cx| {
|
||||||
// Stop propagation behavior, for works on TitleBar.
|
|
||||||
// https://github.com/longbridge/gpui-component/issues/1836
|
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
})
|
})
|
||||||
.when(!self.disabled, |this| {
|
.when(!self.disabled, |this| {
|
||||||
|
|||||||
@@ -2,19 +2,17 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Anchor, AnyElement, App, Div, Edges, ElementId, InteractiveElement, IntoElement, ParentElement,
|
Anchor, AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
|
||||||
RenderOnce, ScrollHandle, Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled,
|
RenderOnce, ScrollHandle, Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled,
|
||||||
Window, div, px,
|
Window, div, px,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use theme::ActiveTheme;
|
|
||||||
|
|
||||||
use super::{Tab, TabVariant};
|
use super::Tab;
|
||||||
use crate::button::{Button, ButtonVariants as _};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
use crate::menu::{DropdownMenu as _, PopupMenuItem};
|
use crate::menu::{DropdownMenu as _, PopupMenuItem};
|
||||||
use crate::{IconName, Selectable, Sizable, Size, StyledExt, h_flex};
|
use crate::{IconName, Selectable, Sizable, StyledExt, h_flex};
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
/// A TabBar element that contains multiple [`Tab`] items.
|
/// A TabBar element that contains multiple [`Tab`] items.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct TabBar {
|
pub struct TabBar {
|
||||||
@@ -26,9 +24,8 @@ pub struct TabBar {
|
|||||||
children: SmallVec<[Tab; 2]>,
|
children: SmallVec<[Tab; 2]>,
|
||||||
last_empty_space: AnyElement,
|
last_empty_space: AnyElement,
|
||||||
selected_index: Option<usize>,
|
selected_index: Option<usize>,
|
||||||
variant: TabVariant,
|
|
||||||
size: Size,
|
|
||||||
menu: bool,
|
menu: bool,
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
|
on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +39,6 @@ impl TabBar {
|
|||||||
scroll_handle: None,
|
scroll_handle: None,
|
||||||
prefix: None,
|
prefix: None,
|
||||||
suffix: None,
|
suffix: None,
|
||||||
variant: TabVariant::default(),
|
|
||||||
size: Size::default(),
|
|
||||||
last_empty_space: div().w_3().into_any_element(),
|
last_empty_space: div().w_3().into_any_element(),
|
||||||
selected_index: None,
|
selected_index: None,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
@@ -51,36 +46,6 @@ impl TabBar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the Tab variant, all children will inherit the variant.
|
|
||||||
pub fn with_variant(mut self, variant: TabVariant) -> Self {
|
|
||||||
self.variant = variant;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the Tab variant to Pill, all children will inherit the variant.
|
|
||||||
pub fn pill(mut self) -> Self {
|
|
||||||
self.variant = TabVariant::Pill;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the Tab variant to Outline, all children will inherit the variant.
|
|
||||||
pub fn outline(mut self) -> Self {
|
|
||||||
self.variant = TabVariant::Outline;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the Tab variant to Segmented, all children will inherit the variant.
|
|
||||||
pub fn segmented(mut self) -> Self {
|
|
||||||
self.variant = TabVariant::Segmented;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the Tab variant to Underline, all children will inherit the variant.
|
|
||||||
pub fn underline(mut self) -> Self {
|
|
||||||
self.variant = TabVariant::Underline;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set whether to show the menu button when tabs overflow, default is false.
|
/// Set whether to show the menu button when tabs overflow, default is false.
|
||||||
pub fn menu(mut self, menu: bool) -> Self {
|
pub fn menu(mut self, menu: bool) -> Self {
|
||||||
self.menu = menu;
|
self.menu = menu;
|
||||||
@@ -105,13 +70,13 @@ impl TabBar {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add children of the TabBar, all children will inherit the variant.
|
/// Add children of the TabBar.
|
||||||
pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Tab>>) -> Self {
|
pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Tab>>) -> Self {
|
||||||
self.children.extend(children.into_iter().map(Into::into));
|
self.children.extend(children.into_iter().map(Into::into));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add child of the TabBar, tab will inherit the variant.
|
/// Add child of the TabBar.
|
||||||
pub fn child(mut self, child: impl Into<Tab>) -> Self {
|
pub fn child(mut self, child: impl Into<Tab>) -> Self {
|
||||||
self.children.push(child.into());
|
self.children.push(child.into());
|
||||||
self
|
self
|
||||||
@@ -147,60 +112,8 @@ impl Styled for TabBar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sizable for TabBar {
|
|
||||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
|
||||||
self.size = size.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for TabBar {
|
impl RenderOnce for TabBar {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
let default_gap = match self.size {
|
|
||||||
Size::Small | Size::XSmall => px(8.),
|
|
||||||
Size::Large => px(16.),
|
|
||||||
_ => px(12.),
|
|
||||||
};
|
|
||||||
let (bg, paddings, gap) = match self.variant {
|
|
||||||
TabVariant::Tab => {
|
|
||||||
let padding = Edges::all(px(0.));
|
|
||||||
(cx.theme().tab_background, padding, px(0.))
|
|
||||||
}
|
|
||||||
TabVariant::Outline => {
|
|
||||||
let padding = Edges::all(px(0.));
|
|
||||||
(gpui::transparent_black(), padding, default_gap)
|
|
||||||
}
|
|
||||||
TabVariant::Pill => {
|
|
||||||
let padding = Edges::all(px(0.));
|
|
||||||
(gpui::transparent_black(), padding, px(4.))
|
|
||||||
}
|
|
||||||
TabVariant::Segmented => {
|
|
||||||
let padding_x = match self.size {
|
|
||||||
Size::XSmall => px(2.),
|
|
||||||
Size::Small => px(3.),
|
|
||||||
_ => px(4.),
|
|
||||||
};
|
|
||||||
let padding = Edges {
|
|
||||||
left: padding_x,
|
|
||||||
right: padding_x,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
(cx.theme().tab_background, padding, px(2.))
|
|
||||||
}
|
|
||||||
TabVariant::Underline => {
|
|
||||||
// This gap is same as the tab inner_paddings
|
|
||||||
let gap = match self.size {
|
|
||||||
Size::XSmall => px(10.),
|
|
||||||
Size::Small => px(12.),
|
|
||||||
Size::Large => px(20.),
|
|
||||||
_ => px(16.),
|
|
||||||
};
|
|
||||||
|
|
||||||
(gpui::transparent_black(), Edges::all(px(0.)), gap)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut item_labels = Vec::new();
|
let mut item_labels = Vec::new();
|
||||||
let selected_index = self.selected_index;
|
let selected_index = self.selected_index;
|
||||||
let on_click = self.on_click.clone();
|
let on_click = self.on_click.clone();
|
||||||
@@ -210,25 +123,6 @@ impl RenderOnce for TabBar {
|
|||||||
.relative()
|
.relative()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.bg(bg)
|
|
||||||
.text_color(cx.theme().tab_foreground)
|
|
||||||
.when(
|
|
||||||
self.variant == TabVariant::Underline || self.variant == TabVariant::Tab,
|
|
||||||
|this| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.id("border-b")
|
|
||||||
.absolute()
|
|
||||||
.left_0()
|
|
||||||
.bottom_0()
|
|
||||||
.size_full()
|
|
||||||
.border_b_1()
|
|
||||||
.border_color(cx.theme().border),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.rounded(self.variant.tab_bar_radius(self.size, cx))
|
|
||||||
.paddings(paddings)
|
|
||||||
.refine_style(&self.style)
|
.refine_style(&self.style)
|
||||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||||
.child(
|
.child(
|
||||||
@@ -239,15 +133,13 @@ impl RenderOnce for TabBar {
|
|||||||
.when_some(self.scroll_handle, |this, scroll_handle| {
|
.when_some(self.scroll_handle, |this, scroll_handle| {
|
||||||
this.track_scroll(&scroll_handle)
|
this.track_scroll(&scroll_handle)
|
||||||
})
|
})
|
||||||
.gap(gap)
|
.gap(px(0.))
|
||||||
.children(self.children.into_iter().enumerate().map(|(ix, child)| {
|
.children(self.children.into_iter().enumerate().map(|(ix, child)| {
|
||||||
item_labels.push((child.label.clone(), child.disabled));
|
item_labels.push((child.label.clone(), child.disabled));
|
||||||
let tab_bar_prefix = child.tab_bar_prefix.unwrap_or(true);
|
let tab_bar_prefix = child.tab_bar_prefix.unwrap_or(true);
|
||||||
child
|
child
|
||||||
.ix(ix)
|
.ix(ix)
|
||||||
.tab_bar_prefix(tab_bar_prefix)
|
.tab_bar_prefix(tab_bar_prefix)
|
||||||
.with_variant(self.variant)
|
|
||||||
.with_size(self.size)
|
|
||||||
.when_some(self.selected_index, |this, selected_ix| {
|
.when_some(self.selected_index, |this, selected_ix| {
|
||||||
this.selected(selected_ix == ix)
|
this.selected(selected_ix == ix)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ use theme::ActiveTheme;
|
|||||||
use crate::{Icon, IconName, InteractiveElementExt as _, Sizable as _, StyledExt, h_flex};
|
use crate::{Icon, IconName, InteractiveElementExt as _, Sizable as _, StyledExt, h_flex};
|
||||||
|
|
||||||
pub const TITLE_BAR_HEIGHT: Pixels = px(34.);
|
pub const TITLE_BAR_HEIGHT: Pixels = px(34.);
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub const TRAFFIC_LIGHT_PADDING: f32 = 80.;
|
pub const TRAFFIC_LIGHT_PADDING: f32 = 80.;
|
||||||
|
|
||||||
/// TitleBar used to customize the appearance of the title bar.
|
/// TitleBar used to customize the appearance of the title bar.
|
||||||
@@ -284,8 +283,6 @@ impl RenderOnce for TitleBar {
|
|||||||
this.px_2()
|
this.px_2()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.border_b_1()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.bg(cx.theme().title_bar)
|
.bg(cx.theme().title_bar)
|
||||||
.refine_style(&self.style)
|
.refine_style(&self.style)
|
||||||
.when(is_linux, |this| {
|
.when(is_linux, |this| {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ person = { path = "../person" }
|
|||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
instant.workspace = true
|
||||||
nostr-connect.workspace = true
|
nostr-connect.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||||
Subscription, Task, Window, div,
|
Subscription, Task, Window, div,
|
||||||
};
|
};
|
||||||
|
use instant::Duration;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use state::{CoopAuthUrlHandler, NostrRegistry, USER_KEYRING};
|
use state::{CoopAuthUrlHandler, NostrRegistry, USER_KEYRING};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
@@ -36,7 +35,7 @@ pub struct ImportIdentity {
|
|||||||
|
|
||||||
impl ImportIdentity {
|
impl ImportIdentity {
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let key_input = cx.new(|cx| InputState::new(window, cx));
|
let key_input = cx.new(|cx| InputState::new(window, cx).placeholder("nsec or bunker://"));
|
||||||
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||||
let error = cx.new(|_| None);
|
let error = cx.new(|_| None);
|
||||||
|
|
||||||
@@ -135,7 +134,6 @@ impl ImportIdentity {
|
|||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -146,7 +144,7 @@ impl ImportIdentity {
|
|||||||
let password = uri.to_string();
|
let password = uri.to_string();
|
||||||
let save = cx.write_credentials(USER_KEYRING, "bunker", password.as_bytes());
|
let save = cx.write_credentials(USER_KEYRING, "bunker", password.as_bytes());
|
||||||
|
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
self.tasks.push(cx.spawn_in(window, async move |_this, cx| {
|
||||||
let keys = master_keys.await;
|
let keys = master_keys.await;
|
||||||
let timeout = Duration::from_secs(30);
|
let timeout = Duration::from_secs(30);
|
||||||
|
|
||||||
@@ -162,9 +160,8 @@ impl ImportIdentity {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok::<(), anyhow::Error>(())
|
Ok(())
|
||||||
})
|
}));
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
@@ -209,6 +206,10 @@ impl Render for ImportIdentity {
|
|||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
|
.gap_4()
|
||||||
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -229,20 +230,21 @@ impl Render for ImportIdentity {
|
|||||||
.when(key_warning, |this| {
|
.when(key_warning, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.v_flex()
|
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_warning)
|
.text_color(cx.theme().text_warning)
|
||||||
.child(div().font_semibold().child("Warning"))
|
.child(div().font_semibold().child("Warning"))
|
||||||
.child(div().child(MSG)),
|
.child(div().child(MSG)),
|
||||||
)
|
)
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("login")
|
Button::new("login")
|
||||||
.label("Continue")
|
.label("Continue")
|
||||||
.primary()
|
.primary()
|
||||||
|
.font_semibold()
|
||||||
.loading(self.loading)
|
.loading(self.loading)
|
||||||
.disabled(self.loading)
|
.disabled(self.loading)
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
this.login(window, cx);
|
this.login(window, cx);
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use device::DeviceRegistry;
|
use device::DeviceRegistry;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use common::TimestampExt;
|
use common::TimestampExt;
|
||||||
@@ -8,6 +7,7 @@ use gpui::{
|
|||||||
App, AppContext, Context, Div, Entity, InteractiveElement, IntoElement, ParentElement, Render,
|
App, AppContext, Context, Div, Entity, InteractiveElement, IntoElement, ParentElement, Render,
|
||||||
SharedString, Styled, Subscription, Task, Window, div, px, relative, uniform_list,
|
SharedString, Styled, Subscription, Task, Window, div, px, relative, uniform_list,
|
||||||
};
|
};
|
||||||
|
use instant::Duration;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{Person, PersonRegistry, shorten_pubkey};
|
use person::{Person, PersonRegistry, shorten_pubkey};
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
@@ -55,7 +55,7 @@ impl Screening {
|
|||||||
window.close_all_modals(cx);
|
window.close_all_modals(cx);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
cx.defer_in(window, move |this, _window, cx| {
|
cx.defer_in(window, |this, _window, cx| {
|
||||||
this.check_contact(cx);
|
this.check_contact(cx);
|
||||||
this.check_wot(cx);
|
this.check_wot(cx);
|
||||||
this.check_last_activity(cx);
|
this.check_last_activity(cx);
|
||||||
@@ -234,7 +234,8 @@ impl Screening {
|
|||||||
}
|
}
|
||||||
.to_tag();
|
.to_tag();
|
||||||
|
|
||||||
let event = EventBuilder::report(vec![tag], "")
|
let event = EventBuilder::new(Kind::Reporting, "")
|
||||||
|
.tag(tag)
|
||||||
.finalize_async(&signer)
|
.finalize_async(&signer)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use gpui::{
|
|||||||
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||||
Window, div, px,
|
Window, div, px,
|
||||||
};
|
};
|
||||||
use settings::{AppSettings, AuthMode};
|
use settings::AppSettings;
|
||||||
use theme::{ActiveTheme, Theme, ThemeMode};
|
use theme::{ActiveTheme, Theme, ThemeMode};
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::group_box::{GroupBox, GroupBoxVariants};
|
use ui::group_box::{GroupBox, GroupBoxVariants};
|
||||||
@@ -60,13 +60,11 @@ impl Render for Preferences {
|
|||||||
const AVATAR: &str = "Hide all avatar pictures to improve performance.";
|
const AVATAR: &str = "Hide all avatar pictures to improve performance.";
|
||||||
const MODE: &str = "Use the selected light or dark theme, or to follow the OS.";
|
const MODE: &str = "Use the selected light or dark theme, or to follow the OS.";
|
||||||
const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages.";
|
const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages.";
|
||||||
const AUTH: &str = "Choose the authentication behavior for relays.";
|
|
||||||
const RESET: &str = "Reset the theme to the default one.";
|
const RESET: &str = "Reset the theme to the default one.";
|
||||||
|
|
||||||
let screening = AppSettings::get_screening(cx);
|
let screening = AppSettings::get_screening(cx);
|
||||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||||
let nip4e = AppSettings::get_nip4e(cx);
|
let nip4e = AppSettings::get_nip4e(cx);
|
||||||
let auth_mode = AppSettings::get_auth_mode(cx);
|
|
||||||
let theme_mode = AppSettings::get_theme_mode(cx);
|
let theme_mode = AppSettings::get_theme_mode(cx);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -93,52 +91,6 @@ impl Render for Preferences {
|
|||||||
.on_click(move |_, _window, cx| {
|
.on_click(move |_, _window, cx| {
|
||||||
AppSettings::update_hide_avatar(!hide_avatar, cx);
|
AppSettings::update_hide_avatar(!hide_avatar, cx);
|
||||||
}),
|
}),
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_3()
|
|
||||||
.justify_between()
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.child(SharedString::from("Relay authentication")),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from(AUTH)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("auth")
|
|
||||||
.label(auth_mode.to_string())
|
|
||||||
.ghost_alt()
|
|
||||||
.small()
|
|
||||||
.dropdown_menu(|this, _window, _cx| {
|
|
||||||
this.min_w(px(256.))
|
|
||||||
.item(
|
|
||||||
PopupMenuItem::new("Auto authentication").on_click(
|
|
||||||
|_ev, _window, cx| {
|
|
||||||
AppSettings::update_auth_mode(
|
|
||||||
AuthMode::Auto,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.item(PopupMenuItem::new("Ask every time").on_click(
|
|
||||||
|_ev, _window, cx| {
|
|
||||||
AppSettings::update_auth_mode(
|
|
||||||
AuthMode::Manual,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
+53
-129
@@ -1,14 +1,14 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ::settings::AppSettings;
|
use ::settings::AppSettings;
|
||||||
|
use anyhow::Error;
|
||||||
use chat::{ChatEvent, ChatRegistry};
|
use chat::{ChatEvent, ChatRegistry};
|
||||||
use common::{CoopImageCache, download_dir};
|
use common::{CoopImageCache, download_dir};
|
||||||
use device::{DeviceEvent, DeviceRegistry};
|
use device::{DeviceEvent, DeviceRegistry};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, div,
|
Render, SharedString, Styled, Subscription, Task, Window, div, image_cache, px,
|
||||||
image_cache, px, relative,
|
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{PersonRegistry, shorten_pubkey};
|
use person::{PersonRegistry, shorten_pubkey};
|
||||||
@@ -26,7 +26,8 @@ use ui::{Icon, IconName, Root, Sizable, TitleBar, WindowExtension, h_flex, v_fle
|
|||||||
use crate::dialogs::import::ImportIdentity;
|
use crate::dialogs::import::ImportIdentity;
|
||||||
use crate::dialogs::restore::RestoreEncryption;
|
use crate::dialogs::restore::RestoreEncryption;
|
||||||
use crate::dialogs::settings;
|
use crate::dialogs::settings;
|
||||||
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list, trash};
|
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list};
|
||||||
|
use crate::sidebar::Sidebar;
|
||||||
|
|
||||||
mod dialogs;
|
mod dialogs;
|
||||||
mod panels;
|
mod panels;
|
||||||
@@ -59,12 +60,16 @@ enum Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
|
sidebar: Entity<Sidebar>,
|
||||||
/// App's Dock Area
|
/// App's Dock Area
|
||||||
dock: Entity<DockArea>,
|
dock: Entity<DockArea>,
|
||||||
|
|
||||||
/// App's Image Cache
|
/// App's Image Cache
|
||||||
image_cache: Entity<CoopImageCache>,
|
image_cache: Entity<CoopImageCache>,
|
||||||
|
|
||||||
|
/// Async tasks
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
/// Event subscriptions
|
/// Event subscriptions
|
||||||
_subscriptions: SmallVec<[Subscription; 6]>,
|
_subscriptions: SmallVec<[Subscription; 6]>,
|
||||||
}
|
}
|
||||||
@@ -75,20 +80,12 @@ impl Workspace {
|
|||||||
let device = DeviceRegistry::global(cx);
|
let device = DeviceRegistry::global(cx);
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
|
let sidebar = cx.new(|cx| Sidebar::new(window, cx));
|
||||||
let dock = cx.new(|cx| DockArea::new(window, cx));
|
let dock = cx.new(|cx| DockArea::new(window, cx));
|
||||||
let image_cache = CoopImageCache::new(IMAGE_CACHE_SIZE, cx);
|
let image_cache = CoopImageCache::new(IMAGE_CACHE_SIZE, cx);
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Observe sign in state changes
|
|
||||||
cx.observe_in(&nostr, window, move |this, nostr, window, cx| {
|
|
||||||
if nostr.read(cx).current_user().is_some() {
|
|
||||||
this.set_center_layout(window, cx);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe system appearance and update theme
|
// Observe system appearance and update theme
|
||||||
cx.observe_window_appearance(window, |_this, window, cx| {
|
cx.observe_window_appearance(window, |_this, window, cx| {
|
||||||
@@ -101,11 +98,7 @@ impl Workspace {
|
|||||||
cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| {
|
cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| {
|
||||||
match event {
|
match event {
|
||||||
StateEvent::SignerChanged => {
|
StateEvent::SignerChanged => {
|
||||||
this.set_center_layout(window, cx);
|
|
||||||
|
|
||||||
cx.defer_in(window, |_this, window, cx| {
|
|
||||||
window.close_all_modals(cx);
|
window.close_all_modals(cx);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
StateEvent::NoSigner => {
|
StateEvent::NoSigner => {
|
||||||
this.import_identity(window, cx);
|
this.import_identity(window, cx);
|
||||||
@@ -226,25 +219,22 @@ impl Workspace {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Observe the chat registry
|
|
||||||
cx.observe(&chat, move |this, chat, cx| {
|
|
||||||
let ids = this.panel_ids(cx);
|
|
||||||
|
|
||||||
chat.update(cx, |this, cx| {
|
|
||||||
this.refresh_rooms(&ids, cx);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set the layout at the end of cycle
|
|
||||||
cx.defer_in(window, |this, window, cx| {
|
cx.defer_in(window, |this, window, cx| {
|
||||||
this.set_layout(window, cx);
|
let dock = this.dock.downgrade();
|
||||||
|
let greeter = Arc::new(greeter::init(window, cx));
|
||||||
|
let tabs = DockItem::tabs(vec![greeter], None, &dock, window, cx);
|
||||||
|
let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx);
|
||||||
|
|
||||||
|
this.dock.update(cx, |this, cx| {
|
||||||
|
this.set_center(center, window, cx);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
sidebar,
|
||||||
dock,
|
dock,
|
||||||
image_cache,
|
image_cache,
|
||||||
|
tasks: vec![],
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,40 +255,6 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all panel ids
|
|
||||||
fn panel_ids(&self, cx: &App) -> Vec<u64> {
|
|
||||||
self.dock
|
|
||||||
.read(cx)
|
|
||||||
.items
|
|
||||||
.panel_ids(cx)
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|panel| panel.parse::<u64>().ok())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the dock layout
|
|
||||||
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
|
|
||||||
|
|
||||||
// Update the dock layout with sidebar on the left
|
|
||||||
self.dock.update(cx, |this, cx| {
|
|
||||||
this.set_left_dock(left, Some(SIDEBAR_WIDTH), true, window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the center dock layout
|
|
||||||
fn set_center_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let dock = self.dock.downgrade();
|
|
||||||
let greeter = Arc::new(greeter::init(window, cx));
|
|
||||||
let tabs = DockItem::tabs(vec![greeter], None, &dock, window, cx);
|
|
||||||
let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx);
|
|
||||||
|
|
||||||
// Update the layout with center dock
|
|
||||||
self.dock.update(cx, |this, cx| {
|
|
||||||
this.set_center(center, window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle command events
|
/// Handle command events
|
||||||
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
|
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
match command {
|
match command {
|
||||||
@@ -320,7 +276,7 @@ impl Workspace {
|
|||||||
self.dock.update(cx, |this, cx| {
|
self.dock.update(cx, |this, cx| {
|
||||||
this.add_panel(
|
this.add_panel(
|
||||||
Arc::new(profile::init(public_key, window, cx)),
|
Arc::new(profile::init(public_key, window, cx)),
|
||||||
DockPlacement::Right,
|
DockPlacement::Left,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -331,7 +287,7 @@ impl Workspace {
|
|||||||
self.dock.update(cx, |this, cx| {
|
self.dock.update(cx, |this, cx| {
|
||||||
this.add_panel(
|
this.add_panel(
|
||||||
Arc::new(contact_list::init(window, cx)),
|
Arc::new(contact_list::init(window, cx)),
|
||||||
DockPlacement::Right,
|
DockPlacement::Left,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -341,7 +297,7 @@ impl Workspace {
|
|||||||
self.dock.update(cx, |this, cx| {
|
self.dock.update(cx, |this, cx| {
|
||||||
this.add_panel(
|
this.add_panel(
|
||||||
Arc::new(backup::init(window, cx)),
|
Arc::new(backup::init(window, cx)),
|
||||||
DockPlacement::Right,
|
DockPlacement::Left,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -351,7 +307,7 @@ impl Workspace {
|
|||||||
self.dock.update(cx, |this, cx| {
|
self.dock.update(cx, |this, cx| {
|
||||||
this.add_panel(
|
this.add_panel(
|
||||||
Arc::new(messaging_relays::init(window, cx)),
|
Arc::new(messaging_relays::init(window, cx)),
|
||||||
DockPlacement::Right,
|
DockPlacement::Left,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -361,7 +317,7 @@ impl Workspace {
|
|||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
// Trigger a refresh of the chat registry
|
// Trigger a refresh of the chat registry
|
||||||
chat.update(cx, |this, cx| {
|
chat.update(cx, |this, cx| {
|
||||||
this.refresh(cx);
|
this.reload(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Command::ShowRelayList => {
|
Command::ShowRelayList => {
|
||||||
@@ -390,7 +346,7 @@ impl Workspace {
|
|||||||
let device = DeviceRegistry::global(cx).downgrade();
|
let device = DeviceRegistry::global(cx).downgrade();
|
||||||
let save_dialog = cx.prompt_for_new_path(download_dir(), Some("encryption.txt"));
|
let save_dialog = cx.prompt_for_new_path(download_dir(), Some("encryption.txt"));
|
||||||
|
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
self.tasks.push(cx.spawn_in(window, async move |_this, cx| {
|
||||||
// Get the output path from the save dialog
|
// Get the output path from the save dialog
|
||||||
let output_path = match save_dialog.await {
|
let output_path = match save_dialog.await {
|
||||||
Ok(Ok(Some(path))) => path,
|
Ok(Ok(Some(path))) => path,
|
||||||
@@ -417,9 +373,8 @@ impl Workspace {
|
|||||||
cx.open_with_system(output_path.as_path());
|
cx.open_with_system(output_path.as_path());
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok::<_, anyhow::Error>(())
|
Ok(())
|
||||||
})
|
}));
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
Command::ImportEncryption => {
|
Command::ImportEncryption => {
|
||||||
self.import_encryption(window, cx);
|
self.import_encryption(window, cx);
|
||||||
@@ -479,10 +434,11 @@ impl Workspace {
|
|||||||
let import = cx.new(|cx| ImportIdentity::new(window, cx));
|
let import = cx.new(|cx| ImportIdentity::new(window, cx));
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, _cx| {
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
this.width(px(420.))
|
this.width(px(450.))
|
||||||
.show_close(false)
|
.show_close(false)
|
||||||
.overlay_closable(false)
|
.overlay_closable(false)
|
||||||
.title("Import Identity")
|
.keyboard(false)
|
||||||
|
.title("Onboarding")
|
||||||
.child(import.clone())
|
.child(import.clone())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -642,9 +598,7 @@ impl Workspace {
|
|||||||
|
|
||||||
fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let trash_messages = chat.read(cx).count_trash_messages(cx);
|
let nip4e_enabled = AppSettings::get_nip4e(cx);
|
||||||
|
|
||||||
let is_nip4e_enabled = AppSettings::get_nip4e(cx);
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
let Some(public_key) = nostr.read(cx).current_user() else {
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
@@ -658,39 +612,7 @@ impl Workspace {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.when(!cx.theme().platform.is_mac(), |this| this.pr_2())
|
.when(!cx.theme().platform.is_mac(), |this| this.pr_2())
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.when(trash_messages > 0, |this| {
|
.when(nip4e_enabled, |this| {
|
||||||
this.child(
|
|
||||||
h_flex()
|
|
||||||
.id("trash-messages")
|
|
||||||
.h_6()
|
|
||||||
.px_1()
|
|
||||||
.gap_1()
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Warning)
|
|
||||||
.small()
|
|
||||||
.text_color(cx.theme().text_danger),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.line_height(relative(1.))
|
|
||||||
.child(format!("{trash_messages}")),
|
|
||||||
)
|
|
||||||
.on_click(move |_ev, window, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
// Add the trash panel to the center workspace
|
|
||||||
Self::add_panel(
|
|
||||||
trash::init(window, cx),
|
|
||||||
DockPlacement::Center,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(is_nip4e_enabled, |this| {
|
|
||||||
this.child(
|
this.child(
|
||||||
Button::new("key")
|
Button::new("key")
|
||||||
.icon(IconName::UserKey)
|
.icon(IconName::UserKey)
|
||||||
@@ -782,17 +704,7 @@ impl Workspace {
|
|||||||
.w_full()
|
.w_full()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(
|
.child(url.clone())
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.size_1p5()
|
|
||||||
.rounded_full()
|
|
||||||
.bg(cx.theme().icon_accent),
|
|
||||||
)
|
|
||||||
.child(url.clone()),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
@@ -804,11 +716,6 @@ impl Workspace {
|
|||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
menu.separator()
|
menu.separator()
|
||||||
.menu_with_icon(
|
|
||||||
"Reload",
|
|
||||||
IconName::Refresh,
|
|
||||||
Box::new(Command::RefreshMessagingRelays),
|
|
||||||
)
|
|
||||||
.menu_with_icon(
|
.menu_with_icon(
|
||||||
"Manage gossip relays",
|
"Manage gossip relays",
|
||||||
IconName::Relay,
|
IconName::Relay,
|
||||||
@@ -816,9 +723,15 @@ impl Workspace {
|
|||||||
)
|
)
|
||||||
.menu_with_icon(
|
.menu_with_icon(
|
||||||
"Manage messaging relays",
|
"Manage messaging relays",
|
||||||
IconName::Settings,
|
IconName::Relay,
|
||||||
Box::new(Command::ShowMessaging),
|
Box::new(Command::ShowMessaging),
|
||||||
)
|
)
|
||||||
|
.separator()
|
||||||
|
.menu_with_icon(
|
||||||
|
"Reload",
|
||||||
|
IconName::Refresh,
|
||||||
|
Box::new(Command::RefreshMessagingRelays),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -830,7 +743,7 @@ impl Render for Workspace {
|
|||||||
let notification_layer = Root::render_notification_layer(window, cx);
|
let notification_layer = Root::render_notification_layer(window, cx);
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(SharedString::from("workspace"))
|
.id("workspace")
|
||||||
.on_action(cx.listener(Self::on_command))
|
.on_action(cx.listener(Self::on_command))
|
||||||
.relative()
|
.relative()
|
||||||
.size_full()
|
.size_full()
|
||||||
@@ -847,9 +760,20 @@ impl Render for Workspace {
|
|||||||
.child(self.titlebar_left(cx))
|
.child(self.titlebar_left(cx))
|
||||||
.child(self.titlebar_right(cx)),
|
.child(self.titlebar_right(cx)),
|
||||||
)
|
)
|
||||||
// Dock
|
// Main
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.size_full()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_shrink_0()
|
||||||
|
.h_full()
|
||||||
|
.w(SIDEBAR_WIDTH)
|
||||||
|
.child(self.sidebar.clone()),
|
||||||
|
)
|
||||||
.child(self.dock.clone()),
|
.child(self.dock.clone()),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
// Notifications
|
// Notifications
|
||||||
.children(notification_layer)
|
.children(notification_layer)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div,
|
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div,
|
||||||
};
|
};
|
||||||
|
use instant::Duration;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use state::USER_KEYRING;
|
use state::USER_KEYRING;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
@@ -155,12 +154,7 @@ impl Render for BackupPanel {
|
|||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::from("Public Key:")),
|
.child(SharedString::from("Public Key:")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(Input::new(&self.npub_input).small().disabled(true)),
|
||||||
Input::new(&self.npub_input)
|
|
||||||
.small()
|
|
||||||
.bordered(false)
|
|
||||||
.disabled(true),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -173,12 +167,7 @@ impl Render for BackupPanel {
|
|||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::from("Secret Key:")),
|
.child(SharedString::from("Secret Key:")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(Input::new(&self.nsec_input).small().disabled(true)),
|
||||||
Input::new(&self.nsec_input)
|
|
||||||
.small()
|
|
||||||
.bordered(false)
|
|
||||||
.disabled(true),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("copy")
|
Button::new("copy")
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
@@ -8,6 +7,7 @@ use gpui::{
|
|||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||||
Task, TextAlign, Window, div, rems,
|
Task, TextAlign, Window, div, rems,
|
||||||
};
|
};
|
||||||
|
use instant::Duration;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
@@ -17,6 +17,7 @@ use ui::avatar::Avatar;
|
|||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::input::{Input, InputEvent, InputState};
|
use ui::input::{Input, InputEvent, InputState};
|
||||||
|
use ui::scroll::ScrollableElement;
|
||||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ContactListPanel> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ContactListPanel> {
|
||||||
@@ -220,8 +221,7 @@ impl ContactListPanel {
|
|||||||
.px_2()
|
.px_2()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.bg(cx.theme().secondary_background)
|
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||||
.text_color(cx.theme().secondary_foreground)
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
@@ -283,7 +283,12 @@ impl Focusable for ContactListPanel {
|
|||||||
|
|
||||||
impl Render for ContactListPanel {
|
impl Render for ContactListPanel {
|
||||||
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 {
|
||||||
v_flex().p_3().gap_3().w_full().child(
|
v_flex()
|
||||||
|
.p_3()
|
||||||
|
.gap_3()
|
||||||
|
.w_full()
|
||||||
|
.overflow_y_scrollbar()
|
||||||
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
@@ -294,7 +299,7 @@ impl Render for ContactListPanel {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::from("New contact:")),
|
.child("New contact:"),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -303,12 +308,7 @@ impl Render for ContactListPanel {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(Input::new(&self.input).small().cleanable(true))
|
||||||
Input::new(&self.input)
|
|
||||||
.small()
|
|
||||||
.bordered(false)
|
|
||||||
.cleanable(true),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
Button::new("add")
|
Button::new("add")
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
@@ -348,7 +348,6 @@ impl Render for ContactListPanel {
|
|||||||
.icon(IconName::CheckCircle)
|
.icon(IconName::CheckCircle)
|
||||||
.label("Update")
|
.label("Update")
|
||||||
.primary()
|
.primary()
|
||||||
.small()
|
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.loading(self.updating)
|
.loading(self.updating)
|
||||||
.disabled(self.updating)
|
.disabled(self.updating)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
use anyhow::Error;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
IntoElement, ParentElement, Render, SharedString, Styled, Window, div, svg,
|
IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div, svg,
|
||||||
};
|
};
|
||||||
use state::NostrRegistry;
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
@@ -8,8 +9,8 @@ use ui::button::{Button, ButtonVariants};
|
|||||||
use ui::dock::{DockPlacement, Panel, PanelEvent};
|
use ui::dock::{DockPlacement, Panel, PanelEvent};
|
||||||
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
||||||
|
|
||||||
use crate::Workspace;
|
|
||||||
use crate::panels::profile;
|
use crate::panels::profile;
|
||||||
|
use crate::{Command, Workspace};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
||||||
cx.new(|cx| GreeterPanel::new(window, cx))
|
cx.new(|cx| GreeterPanel::new(window, cx))
|
||||||
@@ -18,6 +19,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
|||||||
pub struct GreeterPanel {
|
pub struct GreeterPanel {
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GreeterPanel {
|
impl GreeterPanel {
|
||||||
@@ -25,6 +27,7 @@ impl GreeterPanel {
|
|||||||
Self {
|
Self {
|
||||||
name: "Onboarding".into(),
|
name: "Onboarding".into(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
|
tasks: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +35,7 @@ impl GreeterPanel {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
if let Some(public_key) = nostr.read(cx).current_user() {
|
if let Some(public_key) = nostr.read(cx).current_user() {
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
self.tasks.push(cx.spawn_in(window, async move |_this, cx| {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
Workspace::add_panel(
|
Workspace::add_panel(
|
||||||
profile::init(public_key, window, cx),
|
profile::init(public_key, window, cx),
|
||||||
@@ -42,8 +45,9 @@ impl GreeterPanel {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
})
|
|
||||||
.detach();
|
Ok(())
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,17 +146,20 @@ impl Render for GreeterPanel {
|
|||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.justify_start()
|
.justify_start()
|
||||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.add_profile_panel(window, cx)
|
this.add_profile_panel(window, cx)
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("invite")
|
Button::new("theme")
|
||||||
.icon(Icon::new(IconName::Invite))
|
.icon(Icon::new(IconName::Moon))
|
||||||
.label("Invite friends")
|
.label("Change theme")
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.justify_start(),
|
.justify_start()
|
||||||
|
.on_click(cx.listener(move |_, _, _, cx| {
|
||||||
|
cx.dispatch_action(&Command::ToggleTheme);
|
||||||
|
})),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
@@ -8,6 +7,7 @@ use gpui::{
|
|||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||||
Task, TextAlign, Window, div, rems,
|
Task, TextAlign, Window, div, rems,
|
||||||
};
|
};
|
||||||
|
use instant::Duration;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::NostrRegistry;
|
use state::NostrRegistry;
|
||||||
@@ -320,12 +320,7 @@ impl Render for MessagingRelayPanel {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(Input::new(&self.input).small().cleanable(true))
|
||||||
Input::new(&self.input)
|
|
||||||
.small()
|
|
||||||
.bordered(false)
|
|
||||||
.cleanable(true),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
Button::new("add")
|
Button::new("add")
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
@@ -365,7 +360,6 @@ impl Render for MessagingRelayPanel {
|
|||||||
.icon(IconName::CheckCircle)
|
.icon(IconName::CheckCircle)
|
||||||
.label("Update")
|
.label("Update")
|
||||||
.primary()
|
.primary()
|
||||||
.small()
|
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.loading(self.updating)
|
.loading(self.updating)
|
||||||
.disabled(self.updating)
|
.disabled(self.updating)
|
||||||
|
|||||||
@@ -4,4 +4,3 @@ pub mod greeter;
|
|||||||
pub mod messaging_relays;
|
pub mod messaging_relays;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod relay_list;
|
pub mod relay_list;
|
||||||
pub mod trash;
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Context as AnyhowContext, Error};
|
use anyhow::{Context as AnyhowContext, Error};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -7,6 +6,7 @@ use gpui::{
|
|||||||
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
|
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
|
||||||
Window, div,
|
Window, div,
|
||||||
};
|
};
|
||||||
|
use instant::Duration;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{Person, PersonRegistry, shorten_pubkey};
|
use person::{Person, PersonRegistry, shorten_pubkey};
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
@@ -132,7 +132,7 @@ impl ProfilePanel {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
if status {
|
if status {
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
// Reset the copied state after a delay
|
// Reset the copied state after a delay
|
||||||
@@ -143,8 +143,9 @@ impl ProfilePanel {
|
|||||||
.ok();
|
.ok();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
})
|
|
||||||
.detach();
|
Ok(())
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,7 +353,7 @@ impl Render for ProfilePanel {
|
|||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::from("What should people call you?")),
|
.child(SharedString::from("What should people call you?")),
|
||||||
)
|
)
|
||||||
.child(Input::new(&self.name_input).bordered(false).small()),
|
.child(Input::new(&self.name_input).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -363,7 +364,7 @@ impl Render for ProfilePanel {
|
|||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::from("A short introduction about you:")),
|
.child(SharedString::from("A short introduction about you:")),
|
||||||
)
|
)
|
||||||
.child(Input::new(&self.bio_input).bordered(false).small()),
|
.child(Input::new(&self.bio_input).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -374,7 +375,7 @@ impl Render for ProfilePanel {
|
|||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::from("Website:")),
|
.child(SharedString::from("Website:")),
|
||||||
)
|
)
|
||||||
.child(Input::new(&self.website_input).bordered(false).small()),
|
.child(Input::new(&self.website_input).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -418,7 +419,6 @@ impl Render for ProfilePanel {
|
|||||||
.icon(IconName::CheckCircle)
|
.icon(IconName::CheckCircle)
|
||||||
.label("Update")
|
.label("Update")
|
||||||
.primary()
|
.primary()
|
||||||
.small()
|
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.loading(self.updating)
|
.loading(self.updating)
|
||||||
.disabled(self.updating)
|
.disabled(self.updating)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
@@ -8,6 +7,7 @@ use gpui::{
|
|||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||||
Task, TextAlign, Window, div, px, rems,
|
Task, TextAlign, Window, div, px, rems,
|
||||||
};
|
};
|
||||||
|
use instant::Duration;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
@@ -216,7 +216,7 @@ impl RelayListPanel {
|
|||||||
self.set_updating(true, cx);
|
self.set_updating(true, cx);
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
let event = EventBuilder::relay_list(relays)
|
let event = nip65::RelayList::new(relays)
|
||||||
.finalize_async(&signer)
|
.finalize_async(&signer)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -371,12 +371,7 @@ impl Render for RelayListPanel {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(Input::new(&self.input).small().cleanable(true))
|
||||||
Input::new(&self.input)
|
|
||||||
.small()
|
|
||||||
.bordered(false)
|
|
||||||
.cleanable(true),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
Button::new("metadata")
|
Button::new("metadata")
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
@@ -434,7 +429,6 @@ impl Render for RelayListPanel {
|
|||||||
.icon(IconName::CheckCircle)
|
.icon(IconName::CheckCircle)
|
||||||
.label("Update")
|
.label("Update")
|
||||||
.primary()
|
.primary()
|
||||||
.small()
|
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.loading(self.updating)
|
.loading(self.updating)
|
||||||
.disabled(self.updating)
|
.disabled(self.updating)
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
use chat::ChatRegistry;
|
|
||||||
use gpui::{
|
|
||||||
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
|
||||||
Focusable, InteractiveElement, IntoElement, ListAlignment, ListState, ParentElement, Render,
|
|
||||||
SharedString, Styled, Window, div, list, px, relative,
|
|
||||||
};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::dock::{Panel, PanelEvent};
|
|
||||||
use ui::scroll::Scrollbar;
|
|
||||||
use ui::{Icon, IconName, Sizable, h_flex, v_flex};
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<TrashPanel> {
|
|
||||||
cx.new(|cx| TrashPanel::new(window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TrashPanel {
|
|
||||||
name: SharedString,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
|
|
||||||
/// List state for messages
|
|
||||||
list_state: ListState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TrashPanel {
|
|
||||||
fn new(_window: &mut Window, cx: &mut App) -> Self {
|
|
||||||
let chat = ChatRegistry::global(cx);
|
|
||||||
let count = chat.read(cx).count_trash_messages(cx);
|
|
||||||
let list_state = ListState::new(count, ListAlignment::Bottom, px(1024.));
|
|
||||||
|
|
||||||
Self {
|
|
||||||
name: "Trash".into(),
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
list_state,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn copy(&self, ix: usize, cx: &App) {
|
|
||||||
let chat = ChatRegistry::global(cx);
|
|
||||||
let trashes = chat.read(cx).trashes();
|
|
||||||
|
|
||||||
if let Some(message) = trashes.read(cx).iter().nth(ix) {
|
|
||||||
let item = ClipboardItem::new_string(message.raw_event.to_string());
|
|
||||||
cx.write_to_clipboard(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_list_item(
|
|
||||||
&mut self,
|
|
||||||
ix: usize,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> AnyElement {
|
|
||||||
let chat = ChatRegistry::global(cx);
|
|
||||||
let trashes = chat.read(cx).trashes();
|
|
||||||
|
|
||||||
if let Some(message) = trashes.read(cx).iter().nth(ix) {
|
|
||||||
v_flex()
|
|
||||||
.id(ix)
|
|
||||||
.p_2()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.p_2()
|
|
||||||
.w_full()
|
|
||||||
.gap_1()
|
|
||||||
.rounded(cx.theme().radius_lg)
|
|
||||||
.bg(cx.theme().surface_background)
|
|
||||||
.text_sm()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(cx.theme().text_danger)
|
|
||||||
.child(message.reason.clone()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.h_10()
|
|
||||||
.w_full()
|
|
||||||
.px_2()
|
|
||||||
.justify_between()
|
|
||||||
.bg(cx.theme().elevated_surface_background)
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.truncate()
|
|
||||||
.text_ellipsis()
|
|
||||||
.text_xs()
|
|
||||||
.line_height(relative(1.))
|
|
||||||
.child(message.raw_event.clone()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new(format!("copy-{ix}"))
|
|
||||||
.icon(IconName::Copy)
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.on_click(cx.listener(move |this, _ev, _window, cx| {
|
|
||||||
this.copy(ix, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
} else {
|
|
||||||
div().id(ix).into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Panel for TrashPanel {
|
|
||||||
fn panel_id(&self) -> SharedString {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.text_sm()
|
|
||||||
.child(Icon::new(IconName::Warning).small())
|
|
||||||
.child("Errors")
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for TrashPanel {}
|
|
||||||
|
|
||||||
impl Focusable for TrashPanel {
|
|
||||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for TrashPanel {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
v_flex().size_full().relative().child(
|
|
||||||
v_flex()
|
|
||||||
.flex_1()
|
|
||||||
.relative()
|
|
||||||
.child(
|
|
||||||
list(
|
|
||||||
self.list_state.clone(),
|
|
||||||
cx.processor(move |this, ix, window, cx| {
|
|
||||||
this.render_list_item(ix, window, cx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.size_full(),
|
|
||||||
)
|
|
||||||
.child(Scrollbar::vertical(&self.list_state)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
|
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
|
||||||
@@ -12,11 +11,12 @@ use gpui::{
|
|||||||
ParentElement, Render, SharedString, Styled, Subscription, Task, UniformListScrollHandle,
|
ParentElement, Render, SharedString, Styled, Subscription, Task, UniformListScrollHandle,
|
||||||
Window, div, uniform_list,
|
Window, div, uniform_list,
|
||||||
};
|
};
|
||||||
|
use instant::Duration;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{FIND_DELAY, IMAGE_CACHE_SIZE, NostrRegistry};
|
use state::{FIND_DELAY, IMAGE_CACHE_SIZE, NostrRegistry};
|
||||||
use theme::{ActiveTheme, SIDEBAR_WIDTH, TABBAR_HEIGHT};
|
use theme::{ActiveTheme, SIDEBAR_WIDTH};
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::indicator::Indicator;
|
use ui::indicator::Indicator;
|
||||||
@@ -29,13 +29,8 @@ mod entry;
|
|||||||
|
|
||||||
const INPUT_PLACEHOLDER: &str = "Find or start a conversation";
|
const INPUT_PLACEHOLDER: &str = "Find or start a conversation";
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
|
|
||||||
cx.new(|cx| Sidebar::new(window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sidebar.
|
/// Sidebar.
|
||||||
pub struct Sidebar {
|
pub struct Sidebar {
|
||||||
name: SharedString,
|
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
scroll_handle: UniformListScrollHandle,
|
scroll_handle: UniformListScrollHandle,
|
||||||
|
|
||||||
@@ -80,7 +75,7 @@ pub struct Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Sidebar {
|
impl Sidebar {
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let filter = cx.new(|_| RoomKind::Ongoing);
|
let filter = cx.new(|_| RoomKind::Ongoing);
|
||||||
let contact_list = cx.new(|_| None);
|
let contact_list = cx.new(|_| None);
|
||||||
@@ -135,7 +130,6 @@ impl Sidebar {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
name: "Sidebar".into(),
|
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
scroll_handle: UniformListScrollHandle::new(),
|
scroll_handle: UniformListScrollHandle::new(),
|
||||||
find_input,
|
find_input,
|
||||||
@@ -335,7 +329,7 @@ impl Sidebar {
|
|||||||
.organize(&public_key)
|
.organize(&public_key)
|
||||||
.kind(RoomKind::Ongoing)
|
.kind(RoomKind::Ongoing)
|
||||||
});
|
});
|
||||||
this.emit_room(&room, cx);
|
this.emit_room(&room, _window, cx);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Reset the find panel
|
// Reset the find panel
|
||||||
@@ -379,9 +373,9 @@ impl Sidebar {
|
|||||||
let room = item.read(cx);
|
let room = item.read(cx);
|
||||||
let room_clone = item.clone();
|
let room_clone = item.clone();
|
||||||
let public_key = room.display_member(cx).public_key();
|
let public_key = room.display_member(cx).public_key();
|
||||||
let handler = cx.listener(move |_this, _ev, _window, cx| {
|
let handler = cx.listener(move |_this, _ev, window, cx| {
|
||||||
ChatRegistry::global(cx).update(cx, |s, cx| {
|
ChatRegistry::global(cx).update(cx, |s, cx| {
|
||||||
s.emit_room(&room_clone, cx);
|
s.emit_room(&room_clone, window, cx);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -474,7 +468,7 @@ impl Sidebar {
|
|||||||
|
|
||||||
impl Panel for Sidebar {
|
impl Panel for Sidebar {
|
||||||
fn panel_id(&self) -> SharedString {
|
fn panel_id(&self) -> SharedString {
|
||||||
self.name.clone()
|
"Sidebar".into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,8 +482,11 @@ impl Focusable for Sidebar {
|
|||||||
|
|
||||||
impl Render for Sidebar {
|
impl Render for Sidebar {
|
||||||
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 nostr = NostrRegistry::global(cx);
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let loading = chat.read(cx).loading();
|
let logged_in = nostr.read(cx).current_user().is_some();
|
||||||
|
let loading = chat.read(cx).loading() && logged_in;
|
||||||
|
|
||||||
let total_rooms = chat.read(cx).count(self.filter.read(cx), cx);
|
let total_rooms = chat.read(cx).count(self.filter.read(cx), cx);
|
||||||
|
|
||||||
// Whether the find panel should be shown
|
// Whether the find panel should be shown
|
||||||
@@ -507,17 +504,11 @@ impl Render for Sidebar {
|
|||||||
.size_full()
|
.size_full()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex().px_2().py_1().child(
|
||||||
.h(TABBAR_HEIGHT)
|
|
||||||
.border_b_1()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.bg(cx.theme().tab_background)
|
|
||||||
.child(
|
|
||||||
Input::new(&self.find_input)
|
Input::new(&self.find_input)
|
||||||
.appearance(false)
|
|
||||||
.bordered(false)
|
|
||||||
.small()
|
.small()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
|
.disabled(loading)
|
||||||
.when(!self.find_input.read(cx).loading, |this| {
|
.when(!self.find_input.read(cx).loading, |this| {
|
||||||
this.suffix(
|
this.suffix(
|
||||||
Button::new("find-icon")
|
Button::new("find-icon")
|
||||||
@@ -539,7 +530,6 @@ impl Render for Sidebar {
|
|||||||
Button::new("search-results")
|
Button::new("search-results")
|
||||||
.icon(IconName::Search)
|
.icon(IconName::Search)
|
||||||
.tooltip("All search results")
|
.tooltip("All search results")
|
||||||
.small()
|
|
||||||
.ghost_alt()
|
.ghost_alt()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
@@ -555,9 +545,8 @@ impl Render for Sidebar {
|
|||||||
this.icon(IconName::Inbox)
|
this.icon(IconName::Inbox)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.when(!show_find_panel, |this| this.label("Inbox"))
|
.when(!show_find_panel, |this| this.label("Inbox").small())
|
||||||
.tooltip("All ongoing conversations")
|
.tooltip("All ongoing conversations")
|
||||||
.small()
|
|
||||||
.ghost_alt()
|
.ghost_alt()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
@@ -577,9 +566,8 @@ impl Render for Sidebar {
|
|||||||
this.icon(IconName::Fistbump)
|
this.icon(IconName::Fistbump)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.when(!show_find_panel, |this| this.label("Requests"))
|
.when(!show_find_panel, |this| this.label("Requests").small())
|
||||||
.tooltip("Incoming new conversations")
|
.tooltip("Incoming new conversations")
|
||||||
.small()
|
|
||||||
.ghost_alt()
|
.ghost_alt()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
@@ -608,17 +596,13 @@ impl Render for Sidebar {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.text_center()
|
.text_center()
|
||||||
|
.child(div().text_sm().font_semibold().child("No conversations"))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_sm()
|
.text_xs()
|
||||||
.font_semibold()
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::from("No conversations")),
|
.child("Start a conversation with someone to get started."),
|
||||||
)
|
|
||||||
.child(div().text_xs().text_color(cx.theme().text_muted).child(
|
|
||||||
SharedString::from(
|
|
||||||
"Start a conversation with someone to get started.",
|
|
||||||
),
|
),
|
||||||
)),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -643,7 +627,7 @@ impl Render for Sidebar {
|
|||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(Icon::new(IconName::ChevronDown))
|
.child(Icon::new(IconName::ChevronDown))
|
||||||
.child(SharedString::from("Results")),
|
.child("Results"),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
uniform_list(
|
uniform_list(
|
||||||
@@ -670,7 +654,7 @@ impl Render for Sidebar {
|
|||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(Icon::new(IconName::ChevronDown).small())
|
.child(Icon::new(IconName::ChevronDown).small())
|
||||||
.child(SharedString::from("Suggestions")),
|
.child("Contacts"),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
uniform_list(
|
uniform_list(
|
||||||
@@ -707,17 +691,17 @@ impl Render for Sidebar {
|
|||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
.bottom_0()
|
.bottom_2()
|
||||||
.left_0()
|
.left_0()
|
||||||
.h_9()
|
.h_9()
|
||||||
.w_full()
|
.w_full()
|
||||||
.px_2()
|
.px_4()
|
||||||
.child(
|
.child(
|
||||||
Button::new("create")
|
Button::new("create")
|
||||||
.label(button_label)
|
.label(button_label)
|
||||||
.primary()
|
.primary()
|
||||||
.small()
|
.rounded()
|
||||||
.shadow_lg()
|
.shadow_md()
|
||||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
this.create_room(window, cx);
|
this.create_room(window, cx);
|
||||||
})),
|
})),
|
||||||
@@ -740,15 +724,13 @@ impl Render for Sidebar {
|
|||||||
.h_9()
|
.h_9()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.bg(cx.theme().background.opacity(0.85))
|
.bg(cx.theme().background.opacity(0.85))
|
||||||
.border_color(cx.theme().border_disabled)
|
.when(cx.theme().shadow, |this| this.shadow_md())
|
||||||
.border_1()
|
|
||||||
.when(cx.theme().shadow, |this| this.shadow_xs())
|
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(Indicator::new().small().color(cx.theme().icon_accent))
|
.child(Indicator::new().small().color(cx.theme().icon_accent))
|
||||||
.child(SharedString::from("Getting messages...")),
|
.child("Getting messages..."),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
+6
-8
@@ -43,7 +43,7 @@ fn main() {
|
|||||||
}]);
|
}]);
|
||||||
|
|
||||||
// Set up the window bounds
|
// Set up the window bounds
|
||||||
let bounds = Bounds::centered(None, size(px(920.0), px(700.0)), cx);
|
let bounds = Bounds::centered(None, size(px(960.0), px(720.0)), cx);
|
||||||
|
|
||||||
// Set up the window options
|
// Set up the window options
|
||||||
let opts = WindowOptions {
|
let opts = WindowOptions {
|
||||||
@@ -62,10 +62,6 @@ fn main() {
|
|||||||
|
|
||||||
// Open a window with default options
|
// Open a window with default options
|
||||||
cx.open_window(opts, |window, cx| {
|
cx.open_window(opts, |window, cx| {
|
||||||
// Bring the app to the foreground
|
|
||||||
cx.activate(true);
|
|
||||||
|
|
||||||
cx.new(|cx| {
|
|
||||||
// Initialize components
|
// Initialize components
|
||||||
ui::init(cx);
|
ui::init(cx);
|
||||||
|
|
||||||
@@ -92,11 +88,13 @@ fn main() {
|
|||||||
// Initialize auto update
|
// Initialize auto update
|
||||||
auto_update::init(window, cx);
|
auto_update::init(window, cx);
|
||||||
|
|
||||||
// Root Entity
|
// Root view
|
||||||
Root::new(workspace::init(window, cx).into(), window, cx)
|
cx.new(|cx| Root::new(workspace::init(window, cx).into(), window, cx))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.expect("Failed to open window. Please restart the application.");
|
.expect("Failed to open window. Please restart the application.");
|
||||||
|
|
||||||
|
// Bring the app to the foreground
|
||||||
|
cx.activate(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -4,6 +4,9 @@ version.workspace = true
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
workspace = { path = "../crates/workspace" }
|
workspace = { path = "../crates/workspace" }
|
||||||
assets = { path = "../crates/assets" }
|
assets = { path = "../crates/assets" }
|
||||||
@@ -21,15 +24,15 @@ gpui_platform.workspace = true
|
|||||||
gpui_web = { git = "https://github.com/zed-industries/zed" }
|
gpui_web = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
instant = { workspace = true, features = ["wasm-bindgen"] }
|
||||||
|
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
tracing-wasm = "0.2"
|
tracing-wasm = "0.2"
|
||||||
console_log = "1.0"
|
console_log = "1.0"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
|
universal-time = { git = "https://github.com/shadowylab/universal-time" }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
getrandom_0_2 = { package = "getrandom", version = "0.2", features = ["js"] }
|
getrandom_0_2 = { package = "getrandom", version = "0.2", features = ["js"] }
|
||||||
getrandom_0_3 = { package = "getrandom", version = "0.3", features = ["wasm_js"] }
|
getrandom_0_3 = { package = "getrandom", version = "0.3", features = ["wasm_js"] }
|
||||||
getrandom_0_4 = { package = "getrandom", version = "0.4", features = ["wasm_js"] }
|
getrandom_0_4 = { package = "getrandom", version = "0.4", features = ["wasm_js"] }
|
||||||
tokio = { version = "1", default-features = false, features = ["sync", "macros", "io-util", "rt", "time"] }
|
|
||||||
errno = { version = "0.3.14", default-features = false }
|
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ preview: ## Preview production build
|
|||||||
clean: ## Clean build artifacts
|
clean: ## Clean build artifacts
|
||||||
@echo "Cleaning build artifacts..."
|
@echo "Cleaning build artifacts..."
|
||||||
@rm -rf www/dist
|
@rm -rf www/dist
|
||||||
@rm -rf www/src/wasm/*.js www/src/wasm/*.wasm
|
@rm -rf www/wasm/*.js www/wasm/*.wasm
|
||||||
@cargo clean
|
@cargo clean
|
||||||
|
|
||||||
watch-wasm: ## Watch Rust code changes and auto-rebuild WASM
|
watch-wasm: ## Watch Rust code changes and auto-rebuild WASM
|
||||||
|
|||||||
@@ -20,9 +20,26 @@ if [[ "$1" == "--release" ]]; then
|
|||||||
echo -e "${YELLOW}Building in release mode${NC}"
|
echo -e "${YELLOW}Building in release mode${NC}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# macOS: ensure LLVM from Homebrew is available
|
||||||
|
if [[ "$(uname)" == "Darwin" ]]; then
|
||||||
|
echo -e "${GREEN}Detected macOS, setting up LLVM...${NC}"
|
||||||
|
if brew list llvm &>/dev/null; then
|
||||||
|
echo -e "${GREEN}LLVM already installed, skipping install${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}LLVM not found, installing via Homebrew...${NC}"
|
||||||
|
brew install llvm
|
||||||
|
fi
|
||||||
|
LLVM_PATH=$(brew --prefix llvm)
|
||||||
|
export AR="${LLVM_PATH}/bin/llvm-ar"
|
||||||
|
export CC="${LLVM_PATH}/bin/clang"
|
||||||
|
echo -e "${GREEN}LLVM path: ${LLVM_PATH}${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Step 1: Build WASM
|
# Step 1: Build WASM
|
||||||
echo -e "${GREEN}Step 1: Building WASM...${NC}"
|
echo -e "${GREEN}Step 1: Building WASM...${NC}"
|
||||||
cd "$PROJECT_ROOT"
|
cd "$PROJECT_ROOT"
|
||||||
|
export CARGO_TARGET_DIR="$PROJECT_ROOT/target"
|
||||||
|
RUSTFLAGS='-C target-feature=+bulk-memory -C link-arg=--max-memory=4294967296' \
|
||||||
cargo build --target wasm32-unknown-unknown $RELEASE_FLAG
|
cargo build --target wasm32-unknown-unknown $RELEASE_FLAG
|
||||||
|
|
||||||
# Determine the build directory
|
# Determine the build directory
|
||||||
@@ -33,8 +50,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# WASM file is in workspace target directory
|
# WASM file is in workspace target directory
|
||||||
WORKSPACE_ROOT="$PROJECT_ROOT/../.."
|
WASM_PATH="$PROJECT_ROOT/target/wasm32-unknown-unknown/$BUILD_MODE/coop_web.wasm"
|
||||||
WASM_PATH="$WORKSPACE_ROOT/target/wasm32-unknown-unknown/$BUILD_MODE/coop_web.wasm"
|
|
||||||
|
|
||||||
# Check if WASM file exists
|
# Check if WASM file exists
|
||||||
if [[ ! -f "$WASM_PATH" ]]; then
|
if [[ ! -f "$WASM_PATH" ]]; then
|
||||||
@@ -45,7 +61,7 @@ fi
|
|||||||
# Step 2: Generate JavaScript bindings
|
# Step 2: Generate JavaScript bindings
|
||||||
echo -e "${GREEN}Step 2: Generating JavaScript bindings...${NC}"
|
echo -e "${GREEN}Step 2: Generating JavaScript bindings...${NC}"
|
||||||
wasm-bindgen "$WASM_PATH" \
|
wasm-bindgen "$WASM_PATH" \
|
||||||
--out-dir "$PROJECT_ROOT/www/src/wasm" \
|
--out-dir "$PROJECT_ROOT/www/wasm" \
|
||||||
--target web \
|
--target web \
|
||||||
--no-typescript
|
--no-typescript
|
||||||
|
|
||||||
|
|||||||
+36
-5
@@ -1,7 +1,24 @@
|
|||||||
use gpui::*;
|
use gpui::*;
|
||||||
use ui::Root;
|
use ui::Root;
|
||||||
|
use universal_time::{Instant, MonotonicClock, SystemTime, WallClock, define_time_provider};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
struct CustomTimeProvider;
|
||||||
|
|
||||||
|
impl WallClock for CustomTimeProvider {
|
||||||
|
fn system_time(&self) -> SystemTime {
|
||||||
|
SystemTime::from_unix_duration(instant::Duration::from_secs(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MonotonicClock for CustomTimeProvider {
|
||||||
|
fn instant(&self) -> Instant {
|
||||||
|
Instant::from_ticks(instant::Duration::from_secs(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
define_time_provider!(CustomTimeProvider);
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn run() -> Result<(), JsValue> {
|
pub fn run() -> Result<(), JsValue> {
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
@@ -15,9 +32,23 @@ pub fn run() -> Result<(), JsValue> {
|
|||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
gpui_platform::web_init();
|
gpui_platform::web_init();
|
||||||
|
|
||||||
gpui_platform::application().run(|cx| {
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
let app = gpui_platform::application();
|
||||||
|
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
let app = {
|
||||||
|
let app = gpui_platform::single_threaded_web();
|
||||||
|
|
||||||
|
// Temporary fix: intentionally leak the `Rc<AppCell>` to keep the application alive
|
||||||
|
struct WasmApplication(std::rc::Rc<AppCell>);
|
||||||
|
let wasm_app = unsafe { std::mem::transmute::<Application, WasmApplication>(app) };
|
||||||
|
std::mem::forget(wasm_app.0.clone());
|
||||||
|
unsafe { std::mem::transmute::<WasmApplication, Application>(wasm_app) }
|
||||||
|
};
|
||||||
|
|
||||||
|
app.run(|cx| {
|
||||||
|
// Open the root window
|
||||||
cx.open_window(WindowOptions::default(), |window, cx| {
|
cx.open_window(WindowOptions::default(), |window, cx| {
|
||||||
cx.new(|cx| {
|
|
||||||
// Initialize components
|
// Initialize components
|
||||||
ui::init(cx);
|
ui::init(cx);
|
||||||
|
|
||||||
@@ -41,11 +72,11 @@ pub fn run() -> Result<(), JsValue> {
|
|||||||
// Initialize app registry
|
// Initialize app registry
|
||||||
chat::init(window, cx);
|
chat::init(window, cx);
|
||||||
|
|
||||||
// Root Entity
|
// Root view
|
||||||
Root::new(workspace::init(window, cx).into(), window, cx)
|
cx.new(|cx| Root::new(workspace::init(window, cx).into(), window, cx))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.expect("Failed to open window. Please restart the application.");
|
.expect("Failed to open window. Please restart the application.");
|
||||||
|
|
||||||
cx.activate(true);
|
cx.activate(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Generated
+396
@@ -0,0 +1,396 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"specifiers": {
|
||||||
|
"npm:vite-plugin-static-copy@^3.2.0": "3.4.0_vite@8.1.5",
|
||||||
|
"npm:vite-plugin-wasm@^3.3.0": "3.6.0_vite@8.1.5",
|
||||||
|
"npm:vite@8": "8.1.5"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"@emnapi/core@1.11.1": {
|
||||||
|
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"@emnapi/wasi-threads",
|
||||||
|
"tslib"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@emnapi/runtime@1.11.1": {
|
||||||
|
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||||
|
"dependencies": [
|
||||||
|
"tslib"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@emnapi/wasi-threads@1.2.2": {
|
||||||
|
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||||
|
"dependencies": [
|
||||||
|
"tslib"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@napi-rs/wasm-runtime@1.1.6_@emnapi+core@1.11.1_@emnapi+runtime@1.11.1": {
|
||||||
|
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
|
||||||
|
"dependencies": [
|
||||||
|
"@emnapi/core",
|
||||||
|
"@emnapi/runtime",
|
||||||
|
"@tybys/wasm-util"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@oxc-project/types@0.139.0": {
|
||||||
|
"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="
|
||||||
|
},
|
||||||
|
"@rolldown/binding-android-arm64@1.1.5": {
|
||||||
|
"integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
|
||||||
|
"os": ["android"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@rolldown/binding-darwin-arm64@1.1.5": {
|
||||||
|
"integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@rolldown/binding-darwin-x64@1.1.5": {
|
||||||
|
"integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@rolldown/binding-freebsd-x64@1.1.5": {
|
||||||
|
"integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
|
||||||
|
"os": ["freebsd"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf@1.1.5": {
|
||||||
|
"integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm"]
|
||||||
|
},
|
||||||
|
"@rolldown/binding-linux-arm64-gnu@1.1.5": {
|
||||||
|
"integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@rolldown/binding-linux-arm64-musl@1.1.5": {
|
||||||
|
"integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@rolldown/binding-linux-ppc64-gnu@1.1.5": {
|
||||||
|
"integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["ppc64"]
|
||||||
|
},
|
||||||
|
"@rolldown/binding-linux-s390x-gnu@1.1.5": {
|
||||||
|
"integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["s390x"]
|
||||||
|
},
|
||||||
|
"@rolldown/binding-linux-x64-gnu@1.1.5": {
|
||||||
|
"integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@rolldown/binding-linux-x64-musl@1.1.5": {
|
||||||
|
"integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@rolldown/binding-openharmony-arm64@1.1.5": {
|
||||||
|
"integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
|
||||||
|
"os": ["openharmony"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@rolldown/binding-wasm32-wasi@1.1.5": {
|
||||||
|
"integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
|
||||||
|
"dependencies": [
|
||||||
|
"@emnapi/core",
|
||||||
|
"@emnapi/runtime",
|
||||||
|
"@napi-rs/wasm-runtime"
|
||||||
|
],
|
||||||
|
"cpu": ["wasm32"]
|
||||||
|
},
|
||||||
|
"@rolldown/binding-win32-arm64-msvc@1.1.5": {
|
||||||
|
"integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@rolldown/binding-win32-x64-msvc@1.1.5": {
|
||||||
|
"integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@rolldown/pluginutils@1.0.1": {
|
||||||
|
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="
|
||||||
|
},
|
||||||
|
"@tybys/wasm-util@0.10.3": {
|
||||||
|
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
|
||||||
|
"dependencies": [
|
||||||
|
"tslib"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"anymatch@3.1.3": {
|
||||||
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
|
"dependencies": [
|
||||||
|
"normalize-path",
|
||||||
|
"picomatch@2.3.2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"binary-extensions@2.3.0": {
|
||||||
|
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="
|
||||||
|
},
|
||||||
|
"braces@3.0.3": {
|
||||||
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
|
"dependencies": [
|
||||||
|
"fill-range"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"chokidar@3.6.0": {
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"dependencies": [
|
||||||
|
"anymatch",
|
||||||
|
"braces",
|
||||||
|
"glob-parent",
|
||||||
|
"is-binary-path",
|
||||||
|
"is-glob",
|
||||||
|
"normalize-path",
|
||||||
|
"readdirp"
|
||||||
|
],
|
||||||
|
"optionalDependencies": [
|
||||||
|
"fsevents"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"detect-libc@2.1.2": {
|
||||||
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="
|
||||||
|
},
|
||||||
|
"fdir@6.5.0_picomatch@4.0.5": {
|
||||||
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
|
"dependencies": [
|
||||||
|
"picomatch@4.0.5"
|
||||||
|
],
|
||||||
|
"optionalPeers": [
|
||||||
|
"picomatch@4.0.5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fill-range@7.1.1": {
|
||||||
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
|
"dependencies": [
|
||||||
|
"to-regex-range"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fsevents@2.3.3": {
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"scripts": true
|
||||||
|
},
|
||||||
|
"glob-parent@5.1.2": {
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"dependencies": [
|
||||||
|
"is-glob"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"is-binary-path@2.1.0": {
|
||||||
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
|
"dependencies": [
|
||||||
|
"binary-extensions"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"is-extglob@2.1.1": {
|
||||||
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
|
||||||
|
},
|
||||||
|
"is-glob@4.0.3": {
|
||||||
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
|
"dependencies": [
|
||||||
|
"is-extglob"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"is-number@7.0.0": {
|
||||||
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
||||||
|
},
|
||||||
|
"lightningcss-android-arm64@1.33.0": {
|
||||||
|
"integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
|
||||||
|
"os": ["android"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"lightningcss-darwin-arm64@1.33.0": {
|
||||||
|
"integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"lightningcss-darwin-x64@1.33.0": {
|
||||||
|
"integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"lightningcss-freebsd-x64@1.33.0": {
|
||||||
|
"integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
|
||||||
|
"os": ["freebsd"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"lightningcss-linux-arm-gnueabihf@1.33.0": {
|
||||||
|
"integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm"]
|
||||||
|
},
|
||||||
|
"lightningcss-linux-arm64-gnu@1.33.0": {
|
||||||
|
"integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"lightningcss-linux-arm64-musl@1.33.0": {
|
||||||
|
"integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"lightningcss-linux-x64-gnu@1.33.0": {
|
||||||
|
"integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"lightningcss-linux-x64-musl@1.33.0": {
|
||||||
|
"integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"lightningcss-win32-arm64-msvc@1.33.0": {
|
||||||
|
"integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"lightningcss-win32-x64-msvc@1.33.0": {
|
||||||
|
"integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"lightningcss@1.33.0": {
|
||||||
|
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
|
||||||
|
"dependencies": [
|
||||||
|
"detect-libc"
|
||||||
|
],
|
||||||
|
"optionalDependencies": [
|
||||||
|
"lightningcss-android-arm64",
|
||||||
|
"lightningcss-darwin-arm64",
|
||||||
|
"lightningcss-darwin-x64",
|
||||||
|
"lightningcss-freebsd-x64",
|
||||||
|
"lightningcss-linux-arm-gnueabihf",
|
||||||
|
"lightningcss-linux-arm64-gnu",
|
||||||
|
"lightningcss-linux-arm64-musl",
|
||||||
|
"lightningcss-linux-x64-gnu",
|
||||||
|
"lightningcss-linux-x64-musl",
|
||||||
|
"lightningcss-win32-arm64-msvc",
|
||||||
|
"lightningcss-win32-x64-msvc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nanoid@3.3.16": {
|
||||||
|
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"normalize-path@3.0.0": {
|
||||||
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
|
||||||
|
},
|
||||||
|
"p-map@7.0.6": {
|
||||||
|
"integrity": "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg=="
|
||||||
|
},
|
||||||
|
"picocolors@1.1.1": {
|
||||||
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
||||||
|
},
|
||||||
|
"picomatch@2.3.2": {
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="
|
||||||
|
},
|
||||||
|
"picomatch@4.0.5": {
|
||||||
|
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="
|
||||||
|
},
|
||||||
|
"postcss@8.5.23": {
|
||||||
|
"integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
|
||||||
|
"dependencies": [
|
||||||
|
"nanoid",
|
||||||
|
"picocolors",
|
||||||
|
"source-map-js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"readdirp@3.6.0": {
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"dependencies": [
|
||||||
|
"picomatch@2.3.2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rolldown@1.1.5": {
|
||||||
|
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
|
||||||
|
"dependencies": [
|
||||||
|
"@oxc-project/types",
|
||||||
|
"@rolldown/pluginutils"
|
||||||
|
],
|
||||||
|
"optionalDependencies": [
|
||||||
|
"@rolldown/binding-android-arm64",
|
||||||
|
"@rolldown/binding-darwin-arm64",
|
||||||
|
"@rolldown/binding-darwin-x64",
|
||||||
|
"@rolldown/binding-freebsd-x64",
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf",
|
||||||
|
"@rolldown/binding-linux-arm64-gnu",
|
||||||
|
"@rolldown/binding-linux-arm64-musl",
|
||||||
|
"@rolldown/binding-linux-ppc64-gnu",
|
||||||
|
"@rolldown/binding-linux-s390x-gnu",
|
||||||
|
"@rolldown/binding-linux-x64-gnu",
|
||||||
|
"@rolldown/binding-linux-x64-musl",
|
||||||
|
"@rolldown/binding-openharmony-arm64",
|
||||||
|
"@rolldown/binding-wasm32-wasi",
|
||||||
|
"@rolldown/binding-win32-arm64-msvc",
|
||||||
|
"@rolldown/binding-win32-x64-msvc"
|
||||||
|
],
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"source-map-js@1.2.1": {
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
|
||||||
|
},
|
||||||
|
"tinyglobby@0.2.17": {
|
||||||
|
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||||
|
"dependencies": [
|
||||||
|
"fdir",
|
||||||
|
"picomatch@4.0.5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"to-regex-range@5.0.1": {
|
||||||
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"is-number"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tslib@2.8.1": {
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||||
|
},
|
||||||
|
"vite-plugin-static-copy@3.4.0_vite@8.1.5": {
|
||||||
|
"integrity": "sha512-ekryzCw0ouAOE8tw4RvVL/dfqguXzumsV3FBKoKso4MQ1MUUrUXtl5RI4KpJQUNGqFEsg9kxl4EvDl02YtA9VQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"chokidar",
|
||||||
|
"p-map",
|
||||||
|
"picocolors",
|
||||||
|
"tinyglobby",
|
||||||
|
"vite"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"vite-plugin-wasm@3.6.0_vite@8.1.5": {
|
||||||
|
"integrity": "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==",
|
||||||
|
"dependencies": [
|
||||||
|
"vite"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"vite@8.1.5": {
|
||||||
|
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
|
||||||
|
"dependencies": [
|
||||||
|
"lightningcss",
|
||||||
|
"picomatch@4.0.5",
|
||||||
|
"postcss",
|
||||||
|
"rolldown",
|
||||||
|
"tinyglobby"
|
||||||
|
],
|
||||||
|
"optionalDependencies": [
|
||||||
|
"fsevents"
|
||||||
|
],
|
||||||
|
"bin": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"packageJson": {
|
||||||
|
"dependencies": [
|
||||||
|
"npm:vite-plugin-static-copy@^3.2.0",
|
||||||
|
"npm:vite-plugin-wasm@^3.3.0",
|
||||||
|
"npm:vite@8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import wasm from "vite-plugin-wasm";
|
|
||||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
@@ -8,7 +7,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
wasm(),
|
|
||||||
viteStaticCopy({
|
viteStaticCopy({
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
Reference in New Issue
Block a user