chore: improce performance (#9)
Reviewed-on: https://git.reya.su/reya/signed/pulls/9
This commit was merged in pull request #9.
This commit is contained in:
Generated
+71
-69
@@ -1238,7 +1238,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui_util",
|
"gpui_util",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
@@ -1704,7 +1704,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3484,7 +3484,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
source = "git+https://github.com/zed-industries/zed#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -3555,17 +3555,21 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-base"
|
name = "gpui-base"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
source = "git+https://github.com/longbridge/gpui-component#b4393e22d3a04a880bc187dfdabca9b1eeee270a"
|
source = "git+https://github.com/longbridge/gpui-component#c5ade4889153094b23bf438ada7e58a23c0a96e6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui_macros",
|
"gpui_macros",
|
||||||
"gpui_platform",
|
"gpui_platform",
|
||||||
|
"html5ever",
|
||||||
"instant",
|
"instant",
|
||||||
"lsp-types",
|
"lsp-types",
|
||||||
|
"markdown",
|
||||||
|
"markup5ever_rcdom",
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
"objc2-app-kit 0.3.2",
|
"objc2-app-kit 0.3.2",
|
||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
@@ -3587,25 +3591,22 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-component"
|
name = "gpui-component"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
source = "git+https://github.com/longbridge/gpui-component#b4393e22d3a04a880bc187dfdabca9b1eeee270a"
|
source = "git+https://github.com/longbridge/gpui-component#c5ade4889153094b23bf438ada7e58a23c0a96e6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"core-text",
|
"core-text",
|
||||||
"enum-iterator",
|
"enum-iterator",
|
||||||
"futures",
|
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui-base",
|
"gpui-base",
|
||||||
"gpui-component-assets",
|
"gpui-component-assets",
|
||||||
"gpui-component-macros",
|
"gpui-component-macros",
|
||||||
"gpui_macros",
|
"gpui_macros",
|
||||||
"html5ever",
|
|
||||||
"instant",
|
"instant",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"lsp-types",
|
"lsp-types",
|
||||||
"markdown",
|
"markdown",
|
||||||
"markup5ever_rcdom",
|
|
||||||
"notify",
|
"notify",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
@@ -3667,7 +3668,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-component-assets"
|
name = "gpui-component-assets"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
source = "git+https://github.com/longbridge/gpui-component#b4393e22d3a04a880bc187dfdabca9b1eeee270a"
|
source = "git+https://github.com/longbridge/gpui-component#c5ade4889153094b23bf438ada7e58a23c0a96e6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -3681,7 +3682,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-component-macros"
|
name = "gpui-component-macros"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
source = "git+https://github.com/longbridge/gpui-component#b4393e22d3a04a880bc187dfdabca9b1eeee270a"
|
source = "git+https://github.com/longbridge/gpui-component#c5ade4889153094b23bf438ada7e58a23c0a96e6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3691,7 +3692,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_apple"
|
name = "gpui_apple"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"block",
|
"block",
|
||||||
@@ -3714,7 +3715,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_unix",
|
"accesskit_unix",
|
||||||
@@ -3760,7 +3761,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_macos",
|
"accesskit_macos",
|
||||||
@@ -3806,7 +3807,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -3817,7 +3818,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -3830,7 +3831,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3840,7 +3841,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"log",
|
"log",
|
||||||
@@ -3850,7 +3851,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
@@ -3874,7 +3875,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -3900,7 +3901,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_windows",
|
"accesskit_windows",
|
||||||
@@ -4093,9 +4094,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.5.2"
|
version = "0.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex"
|
name = "hex"
|
||||||
@@ -4204,7 +4205,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
@@ -4224,7 +4225,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"rustls",
|
"rustls",
|
||||||
@@ -5043,9 +5044,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lyon_algorithms"
|
name = "lyon_algorithms"
|
||||||
version = "1.0.20"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8575c0d003ae459399623c4def180c63b77f343b1a7fee64f249b349e7699a31"
|
checksum = "cdfa8785f95e57914ddb35e3b59994aeba6f5e79e9cfd03da1c269f010f36009"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lyon_path",
|
"lyon_path",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
@@ -5074,9 +5075,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lyon_tessellation"
|
name = "lyon_tessellation"
|
||||||
version = "1.0.20"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e43b7e44161571868f5c931d12583592c223c5583eef86b08aa02b7048a3552"
|
checksum = "dabea159dc6eea9171a541b458134b70ca95e3162d068132b2b3b9bc01aad06e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"float_next_after",
|
"float_next_after",
|
||||||
"lyon_path",
|
"lyon_path",
|
||||||
@@ -5180,7 +5181,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen",
|
"bindgen",
|
||||||
@@ -5404,8 +5405,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.45.3"
|
version = "0.45.4"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0"
|
source = "git+https://github.com/rust-nostr/nostr#472c8839ea3f532259435d0513bf155ddeb467ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -5431,7 +5432,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-connect"
|
name = "nostr-connect"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0"
|
source = "git+https://github.com/rust-nostr/nostr#472c8839ea3f532259435d0513bf155ddeb467ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -5445,7 +5446,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0"
|
source = "git+https://github.com/rust-nostr/nostr#472c8839ea3f532259435d0513bf155ddeb467ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nostr",
|
"nostr",
|
||||||
"opaquerr",
|
"opaquerr",
|
||||||
@@ -5454,7 +5455,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip"
|
name = "nostr-gossip"
|
||||||
version = "0.45.0"
|
version = "0.45.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0"
|
source = "git+https://github.com/rust-nostr/nostr#472c8839ea3f532259435d0513bf155ddeb467ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nostr",
|
"nostr",
|
||||||
"opaquerr",
|
"opaquerr",
|
||||||
@@ -5463,7 +5464,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip-memory"
|
name = "nostr-gossip-memory"
|
||||||
version = "0.45.0"
|
version = "0.45.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0"
|
source = "git+https://github.com/rust-nostr/nostr#472c8839ea3f532259435d0513bf155ddeb467ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -5475,7 +5476,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.45.2"
|
version = "0.45.2"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0"
|
source = "git+https://github.com/rust-nostr/nostr#472c8839ea3f532259435d0513bf155ddeb467ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
@@ -5490,7 +5491,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-memory"
|
name = "nostr-memory"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0"
|
source = "git+https://github.com/rust-nostr/nostr#472c8839ea3f532259435d0513bf155ddeb467ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"btreecap",
|
"btreecap",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -5501,7 +5502,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.45.2"
|
version = "0.45.2"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0"
|
source = "git+https://github.com/rust-nostr/nostr#472c8839ea3f532259435d0513bf155ddeb467ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -6228,7 +6229,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -7016,7 +7017,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
]
|
]
|
||||||
@@ -7099,7 +7100,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -7477,7 +7478,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "scheduler"
|
name = "scheduler"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-task",
|
"async-task",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
@@ -7919,6 +7920,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"bitcoin_hashes 1.2.0",
|
"bitcoin_hashes 1.2.0",
|
||||||
"flume 0.11.1",
|
"flume 0.11.1",
|
||||||
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -8171,7 +8173,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heapless 0.9.3",
|
"heapless 0.9.3",
|
||||||
"log",
|
"log",
|
||||||
@@ -8182,15 +8184,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval"
|
name = "sval"
|
||||||
version = "2.21.1"
|
version = "2.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec4a2a7d92fa86fcc6222e4c3845f8486cff899d9db32480b26c91a5dbf2e22d"
|
checksum = "b81b254da21fe1fcc4e3a74fe39b46e25e3a863078f8b71c954d47f84889dbc6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval_buffer"
|
name = "sval_buffer"
|
||||||
version = "2.21.1"
|
version = "2.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f4324db9ac500c609d659b752edf9c8abbf2233f8afd61a503fd6f88ed625032"
|
checksum = "50be352d2822ffafb59e3e2ddac9d5ee60f2eeadbb7b5a2a951b9f3651e87a6f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sval",
|
"sval",
|
||||||
"sval_ref",
|
"sval_ref",
|
||||||
@@ -8199,18 +8201,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval_dynamic"
|
name = "sval_dynamic"
|
||||||
version = "2.21.1"
|
version = "2.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4046add0eecf55e680b9e207edf5fc7737b18a1d950db363d97e7f1b2d7c629c"
|
checksum = "b048ca293b998d9a45659159f94a64063791e74cdc670164943dbb434405573d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sval",
|
"sval",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval_fmt"
|
name = "sval_fmt"
|
||||||
version = "2.21.1"
|
version = "2.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "911a3486b5984a0a4f25edefcf2c2dba23654c29f63e75493b671d338bf24243"
|
checksum = "e6b5888e40f80568733217f27b7317b845f463400ced36c424b1a804730e53b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
@@ -8219,9 +8221,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval_json"
|
name = "sval_json"
|
||||||
version = "2.21.1"
|
version = "2.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da53aae7c737b5b5f1be4bcb0ff20e057bf6b2ee4e9d025560075c5830d09f95"
|
checksum = "e17664d6bb6b74947afaab9d7c991caa9bf5638d4dee16fcbef637f440796049"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
@@ -8230,9 +8232,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval_nested"
|
name = "sval_nested"
|
||||||
version = "2.21.1"
|
version = "2.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df24df43cbdc4bb8c9f5ed19d0d57dc8f60a1a4259cdce52d597fe774ad3a71f"
|
checksum = "07c059969ca5ca163ea7fef6c9661758973d17691aba92abdcf5c428f4ec122c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sval",
|
"sval",
|
||||||
"sval_buffer",
|
"sval_buffer",
|
||||||
@@ -8241,18 +8243,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval_ref"
|
name = "sval_ref"
|
||||||
version = "2.21.1"
|
version = "2.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2bebc17f0f1fad060e57b778728d41ef87627e9111a6365d7463472cb58fc1b3"
|
checksum = "42d6b29ff568c85c87561807f51d2adfff4b6016c6363133f7cd1652a12548f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sval",
|
"sval",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sval_serde"
|
name = "sval_serde"
|
||||||
version = "2.21.1"
|
version = "2.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f26fe3f6a68b40e6c8d654ea48c00e4316272fddf68c80493714c1b034ae70b"
|
checksum = "8f33ec9edc42b12764d5c90ca0a1d84189c6bde81ed27507f1e661c6e4e05853"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"sval",
|
"sval",
|
||||||
@@ -9120,9 +9122,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter-language"
|
name = "tree-sitter-language"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782"
|
checksum = "ca0d1bf6fdd806e43ae5198f82f527056d359def39e54e67a0f478ac09dac081"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter-lua"
|
name = "tree-sitter-lua"
|
||||||
@@ -9555,7 +9557,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"perf",
|
"perf",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -9611,9 +9613,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "value-bag-serde1"
|
name = "value-bag-serde1"
|
||||||
version = "1.13.2"
|
version = "1.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "417d6197dd0ee696783d6be4276ac6ea74b985e00024c85ccfb37aff4f2bed82"
|
checksum = "27780e3aec98b13ac796bc0d5770205ee6e976d1ccb33a52da70b73ff0ff880d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"erased-serde",
|
"erased-serde",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
@@ -9622,9 +9624,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "value-bag-sval2"
|
name = "value-bag-sval2"
|
||||||
version = "1.13.2"
|
version = "1.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c61f7251ecde2c9ed431bbe0659853e7991753447447bbf1ae59d8b31c578d4e"
|
checksum = "1c093d229c7ead9974b640e61170976e0eff3dac447cf6792c645f0b7f28b3de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sval",
|
"sval",
|
||||||
"sval_buffer",
|
"sval_buffer",
|
||||||
@@ -11320,7 +11322,7 @@ checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "zlog"
|
name = "zlog"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -11337,7 +11339,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -11348,7 +11350,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#399258feeaf90ad8a3a208c99221ee87b6452f38"
|
source = "git+https://github.com/zed-industries/zed#ce48461eaadd16c65c31f835511ab96bd3b6e746"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zune-core"
|
name = "zune-core"
|
||||||
|
|||||||
@@ -27,11 +27,7 @@ use crate::tiles::SignedTilesSkin;
|
|||||||
use crate::{TAB_BAR_HEIGHT, panel_handle};
|
use crate::{TAB_BAR_HEIGHT, panel_handle};
|
||||||
|
|
||||||
/// What every part of the skin reads, and the dock area it belongs to.
|
/// What every part of the skin reads, and the dock area it belongs to.
|
||||||
///
|
/// Shared by reference with the per-container renderers.
|
||||||
/// The renderer is the only skin-owned object in the picture, so the settings
|
|
||||||
/// the old `DockArea` carried live here. It is shared by reference with the
|
|
||||||
/// per-container renderers, which are built once each and outlive any one
|
|
||||||
/// frame.
|
|
||||||
pub(crate) struct SkinShared {
|
pub(crate) struct SkinShared {
|
||||||
area: WeakEntity<DockArea>,
|
area: WeakEntity<DockArea>,
|
||||||
toggle_button_visible: Cell<bool>,
|
toggle_button_visible: Cell<bool>,
|
||||||
@@ -201,10 +197,9 @@ impl DockAreaRenderer for SignedDockSkin {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The "unknown panel" message the old `InvalidPanel` drew.
|
/// The "unknown panel" message the old `InvalidPanel` drew. It answers
|
||||||
///
|
/// `dump` with the state it was handed, so a layout written by a build
|
||||||
/// It answers `dump` with the state it was handed, so a layout written by
|
/// that knows the panel survives a load and save here.
|
||||||
/// a build that knows the panel survives a load and save here.
|
|
||||||
fn build_placeholder(
|
fn build_placeholder(
|
||||||
&self,
|
&self,
|
||||||
state: &PanelState,
|
state: &PanelState,
|
||||||
@@ -246,12 +241,10 @@ impl SignedDockSkin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Turns the window's mouse stream into dock resizing.
|
/// Turns the window's mouse stream into dock resizing. A resize is driven
|
||||||
///
|
/// by pointer moves anywhere in the window, so this paints nothing and
|
||||||
/// A resize is driven by pointer moves that land anywhere in the window, not
|
/// exists for its `paint` hook — the only place a window-level mouse
|
||||||
/// only on the handle, so it cannot be expressed as a listener on the handle
|
/// listener can be registered.
|
||||||
/// itself. This element paints nothing and exists for its `paint` hook, which
|
|
||||||
/// is the only place a window-level mouse listener can be registered.
|
|
||||||
struct DockResizeTracker {
|
struct DockResizeTracker {
|
||||||
dock: DockContext,
|
dock: DockContext,
|
||||||
shared: Rc<SkinShared>,
|
shared: Rc<SkinShared>,
|
||||||
@@ -317,10 +310,10 @@ impl Element for DockResizeTracker {
|
|||||||
if !phase.bubble() || shared.resizing_dock().get() != Some(placement) {
|
if !phase.bubble() || shared.resizing_dock().get() != Some(placement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Dragging a closed dock's handle reopens it, as the old dock
|
// Dragging a closed dock's handle reopens it. The live
|
||||||
// did. The live state is read rather than the render-time
|
// state is read rather than the render-time snapshot in
|
||||||
// snapshot in `dock`, which would still say closed for the
|
// `dock`, which would still say closed for the rest of the
|
||||||
// rest of the frame and toggle it shut again on the next move.
|
// frame and toggle it shut again on the next move.
|
||||||
let open = shared
|
let open = shared
|
||||||
.area()
|
.area()
|
||||||
.upgrade()
|
.upgrade()
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ use gpui_component::ActiveTheme as _;
|
|||||||
|
|
||||||
use crate::Panel;
|
use crate::Panel;
|
||||||
|
|
||||||
/// Stands in for a panel this build cannot construct — one whose `panel_name`
|
/// Stands in for a panel this build cannot construct. It reports the
|
||||||
/// no [`PanelRegistry`](gpui_base::dock::PanelRegistry) builder answers to.
|
/// original [`PanelState`] from [`dump`](gpui_base::dock::Panel::dump), so
|
||||||
///
|
/// a layout written by a build that knows the panel survives a load and
|
||||||
/// It reports the original [`PanelState`] from
|
/// save here.
|
||||||
/// [`dump`](gpui_base::dock::Panel::dump), so a layout written by a build that
|
|
||||||
/// knows the panel survives a load and a save here rather than losing it.
|
|
||||||
pub(crate) struct InvalidPanel {
|
pub(crate) struct InvalidPanel {
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
//! The Signed dock skin.
|
//! The Signed dock skin.
|
||||||
//!
|
//!
|
||||||
//! The dock engine lives upstream: `gpui_base::dock` owns the layout tree,
|
//! The dock engine lives upstream (`gpui_base::dock` owns the layout tree,
|
||||||
//! the drags, the zoom and the persistence, and `gpui_component::dock`
|
//! drags, zoom and persistence); this crate is the appearance the app used
|
||||||
//! supplies the default appearance. This crate is the appearance the app
|
//! to vendor from gpui-component — a 44px tab bar that doubles as the
|
||||||
//! used to vendor from gpui-component — a 44px tab bar that doubles as the
|
//! window title bar, with pill tabs, window controls, title-bar dragging
|
||||||
//! window title bar, with pill tabs, window controls, title-bar dragging and
|
//! and previous/next tab buttons.
|
||||||
//! previous/next tab buttons — re-implemented against upstream's renderer
|
|
||||||
//! traits.
|
|
||||||
//!
|
//!
|
||||||
//! Everything `gpui_component::dock` exports is re-exported here, so the app
|
//! Everything `gpui_component::dock` exports is re-exported here, so the app
|
||||||
//! keeps importing the dock from a single place.
|
//! keeps importing the dock from a single place.
|
||||||
|
|||||||
@@ -84,11 +84,9 @@ impl Render for DragPanelPreview {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Where the zoom affordance goes for the group's displayed panel, or `None`
|
/// Where the zoom affordance goes for the group's displayed panel, or `None`
|
||||||
/// when there is none to offer.
|
/// when there is none to offer. Both [`Panel::zoom_control`] (where) and
|
||||||
///
|
/// [`gpui_base::dock::Panel::zoomable`] (whether) must pass; base refuses a
|
||||||
/// Two questions, and both have to be asked. [`Panel::zoom_control`] says
|
/// zoom that fails the latter.
|
||||||
/// *where* the control appears; [`gpui_base::dock::Panel::zoomable`] says
|
|
||||||
/// whether zooming happens at all, and base refuses a zoom that fails it.
|
|
||||||
fn zoom_control(group: &TabGroupContext, cx: &App) -> Option<PanelControl> {
|
fn zoom_control(group: &TabGroupContext, cx: &App) -> Option<PanelControl> {
|
||||||
let panel = group.active_panel()?;
|
let panel = group.active_panel()?;
|
||||||
panel
|
panel
|
||||||
@@ -122,11 +120,8 @@ fn right_top_group(node: &PaneNode) -> Option<NodeId> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One tab group's appearance.
|
/// One tab group's appearance. Built once per container, so the tab bar's
|
||||||
///
|
/// scroll position and measured title-bar geometry belong to the group.
|
||||||
/// Built per group — `DockAreaRenderer::tab_group_renderer` is called once
|
|
||||||
/// per container — so the tab bar's scroll position and the measured
|
|
||||||
/// title-bar geometry belong to the group they describe.
|
|
||||||
pub(crate) struct SignedTabGroupSkin {
|
pub(crate) struct SignedTabGroupSkin {
|
||||||
shared: Rc<SkinShared>,
|
shared: Rc<SkinShared>,
|
||||||
scroll_handle: ScrollHandle,
|
scroll_handle: ScrollHandle,
|
||||||
@@ -177,9 +172,8 @@ impl SignedTabGroupSkin {
|
|||||||
/// The bottom or right dock whose root tab group this group is, if any.
|
/// The bottom or right dock whose root tab group this group is, if any.
|
||||||
///
|
///
|
||||||
/// Base bars a dock's only group from being dragged or closed, so the
|
/// Base bars a dock's only group from being dragged or closed, so the
|
||||||
/// dock cannot be emptied. A bottom/right panel is supposed to be
|
/// dock cannot be emptied — but a bottom/right panel is supposed to be
|
||||||
/// closable and movable, though — the vendored dock allowed exactly that
|
/// closable, so the skin routes around the bar for these groups.
|
||||||
/// — so the skin recognizes the group and routes around the bar.
|
|
||||||
fn is_dock_root_group(&self, group: &TabGroupContext, cx: &App) -> Option<DockPlacement> {
|
fn is_dock_root_group(&self, group: &TabGroupContext, cx: &App) -> Option<DockPlacement> {
|
||||||
let area = self.shared.area().upgrade()?;
|
let area = self.shared.area().upgrade()?;
|
||||||
let area = area.read(cx);
|
let area = area.read(cx);
|
||||||
@@ -270,9 +264,7 @@ impl SignedTabGroupSkin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The previous/next tab buttons shown in the tab bar's leading prefix.
|
/// The previous/next tab buttons shown in the tab bar's leading prefix.
|
||||||
///
|
/// Always rendered, disabled at the ends of the strip (or collapsed).
|
||||||
/// Unlike the dock toggle button they always render, but are disabled at
|
|
||||||
/// the ends of the tab strip (or while the panel is collapsed).
|
|
||||||
fn render_prev_next_tab_buttons(
|
fn render_prev_next_tab_buttons(
|
||||||
&self,
|
&self,
|
||||||
group: &TabGroupContext,
|
group: &TabGroupContext,
|
||||||
@@ -405,10 +397,9 @@ impl SignedTabGroupSkin {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One tab of the pill strip.
|
/// One tab of the pill strip. While collapsed, tabs lose the active
|
||||||
///
|
/// style and all interactions, and the strip becomes the way a closed
|
||||||
/// While collapsed, tabs lose the active style and all interactions, and
|
/// bottom dock is opened again.
|
||||||
/// the strip becomes the way a closed bottom dock is opened again.
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_tab(
|
fn render_tab(
|
||||||
&self,
|
&self,
|
||||||
@@ -578,10 +569,8 @@ impl TabGroupRenderer for SignedTabGroupSkin {
|
|||||||
if group.panels().is_empty() {
|
if group.panels().is_empty() {
|
||||||
return div().id("tab-panel");
|
return div().id("tab-panel");
|
||||||
}
|
}
|
||||||
// Closing the only panel of a bottom/right dock would leave an empty
|
// Closing the only panel of a bottom/right dock would leave an
|
||||||
// dock, which base refuses through the group. The skin removes the
|
// empty dock, which base refuses; the skin removes the dock instead.
|
||||||
// whole dock instead — the vendored dock's close took its split
|
|
||||||
// group away just the same.
|
|
||||||
let dock_to_remove = (group.panels().len() <= 1)
|
let dock_to_remove = (group.panels().len() <= 1)
|
||||||
.then(|| self.is_dock_root_group(group, cx))
|
.then(|| self.is_dock_root_group(group, cx))
|
||||||
.flatten();
|
.flatten();
|
||||||
@@ -601,8 +590,8 @@ impl TabGroupRenderer for SignedTabGroupSkin {
|
|||||||
this.on_action({
|
this.on_action({
|
||||||
let group = group.clone();
|
let group = group.clone();
|
||||||
move |_: &ToggleZoom, window, cx| {
|
move |_: &ToggleZoom, window, cx| {
|
||||||
// The affordance decides the control, so a panel that
|
// The affordance decides the control, so a panel
|
||||||
// offers none is not zoomed *in* by the keybinding
|
// offering none is not zoomed *in* by the keybinding
|
||||||
// either. Zooming out is never refused: a panel that
|
// either. Zooming out is never refused: a panel that
|
||||||
// stopped offering the control while zoomed would
|
// stopped offering the control while zoomed would
|
||||||
// otherwise strand the user with no way back.
|
// otherwise strand the user with no way back.
|
||||||
@@ -671,12 +660,10 @@ impl TabGroupRenderer for SignedTabGroupSkin {
|
|||||||
let right_dock_button = self.dock_toggle_button(DockPlacement::Right, group, cx);
|
let right_dock_button = self.dock_toggle_button(DockPlacement::Right, group, cx);
|
||||||
let is_bottom_dock = bottom_dock_button.is_some();
|
let is_bottom_dock = bottom_dock_button.is_some();
|
||||||
|
|
||||||
// macOS: the traffic lights overlay the window's top-left corner. Only
|
// macOS: the traffic lights overlay the window's top-left corner.
|
||||||
// the group whose tab bar actually sits under them must reserve the
|
// Only the group whose tab bar actually sits under them reserves the
|
||||||
// space: the left dock (sidebar) normally clears them, and when it is
|
// space — the center's left-most, top-most group when the left dock
|
||||||
// closed or absent it is the center's left-most, top-most tab group
|
// is closed or absent.
|
||||||
// that is in the corner. A bottom or right dock is never there, and
|
|
||||||
// neither is the right panel of a center split.
|
|
||||||
let needs_traffic_light_padding = cfg!(target_os = "macos")
|
let needs_traffic_light_padding = cfg!(target_os = "macos")
|
||||||
&& self.shared.area().upgrade().is_some_and(|area| {
|
&& self.shared.area().upgrade().is_some_and(|area| {
|
||||||
let area = area.read(cx);
|
let area = area.read(cx);
|
||||||
@@ -703,11 +690,10 @@ impl TabGroupRenderer for SignedTabGroupSkin {
|
|||||||
self.scroll_handle.scroll_to_item(visible_ix);
|
self.scroll_handle.scroll_to_item(visible_ix);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The tab strip lays out its scrollable content at content width, so
|
// The tab strip lays out at content width, so the area after the
|
||||||
// the area after the last tab only spans `min_w_16` — the rest of the
|
// last tab has no element. Cover that dead zone (last tab's right
|
||||||
// tab bar has no element at all. Cover that dead zone with a
|
// edge to suffix's left edge) with a measured overlay so the whole
|
||||||
// measured overlay so the whole non-interactive area can drag the
|
// non-interactive area can drag the window.
|
||||||
// window. Its span is [last tab's right edge, suffix's left edge].
|
|
||||||
let drag_overlay = match (
|
let drag_overlay = match (
|
||||||
self.title_bar_bounds.get(),
|
self.title_bar_bounds.get(),
|
||||||
self.title_bar_strip_bounds.get(),
|
self.title_bar_strip_bounds.get(),
|
||||||
|
|||||||
@@ -101,13 +101,11 @@ impl SignedTilesSkin {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The trailing controls of a tile's title bar.
|
/// The trailing controls of a tile's title bar: zoom, close and the
|
||||||
///
|
/// ellipsis menu. They use click handlers rather than the
|
||||||
/// A tile has no tab bar to hang a toolbar off, so this is where its zoom,
|
/// [`ToggleZoom`](crate::ToggleZoom)/[`ClosePanel`](crate::ClosePanel)
|
||||||
/// close and ellipsis menu live. The entries use click handlers rather
|
/// actions, which are dispatched to a focused tab group — a tile is
|
||||||
/// than the [`ToggleZoom`](crate::ToggleZoom) and
|
/// not one.
|
||||||
/// [`ClosePanel`](crate::ClosePanel) actions: those are dispatched to a
|
|
||||||
/// focused tab group, and a tile is not one.
|
|
||||||
fn render_tile_controls(
|
fn render_tile_controls(
|
||||||
&self,
|
&self,
|
||||||
tile: &TileContext,
|
tile: &TileContext,
|
||||||
@@ -388,12 +386,9 @@ impl TilesRenderer for SignedTilesSkin {
|
|||||||
.size_full()
|
.size_full()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The canvas scrollbar.
|
/// The canvas scrollbar. It must be an overlay: the frame is the scroll
|
||||||
///
|
/// container and base appends the tiles after it, so a scrollbar placed
|
||||||
/// It has to be an overlay rather than one of the frame's own children:
|
/// inside would paint and hit-test underneath every tile.
|
||||||
/// the frame is the scroll container and base appends the tiles after
|
|
||||||
/// whatever the frame carries, so a scrollbar placed there would paint and
|
|
||||||
/// hit-test underneath every tile.
|
|
||||||
fn render_overlay(
|
fn render_overlay(
|
||||||
&self,
|
&self,
|
||||||
content: Size<Pixels>,
|
content: Size<Pixels>,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use nostr::prelude::*;
|
use nostr::prelude::*;
|
||||||
|
|
||||||
use crate::RepoAddr;
|
use crate::RepoAddr;
|
||||||
@@ -40,8 +42,10 @@ pub fn activity(addr: &RepoAddr) -> Filter {
|
|||||||
Filter::new().kinds(ACTIVITY_KINDS).coordinate(addr)
|
Filter::new().kinds(ACTIVITY_KINDS).coordinate(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Status events (`1630..=1633`) referencing a specific root event (`#e` tag).
|
/// Status events (`1630..=1633`) referencing any of the given root events
|
||||||
pub fn statuses_for(root: EventId) -> Filter {
|
/// (`#e` tag). Batched: one filter covers all roots, so a negentropy sync
|
||||||
|
/// reconciles them in a single session instead of one per root.
|
||||||
|
pub fn statuses_for(roots: impl IntoIterator<Item = EventId>) -> Filter {
|
||||||
Filter::new()
|
Filter::new()
|
||||||
.kinds([
|
.kinds([
|
||||||
Kind::GitStatusOpen,
|
Kind::GitStatusOpen,
|
||||||
@@ -49,16 +53,17 @@ pub fn statuses_for(root: EventId) -> Filter {
|
|||||||
Kind::GitStatusClosed,
|
Kind::GitStatusClosed,
|
||||||
Kind::GitStatusDraft,
|
Kind::GitStatusDraft,
|
||||||
])
|
])
|
||||||
.event(root)
|
.events(roots)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cover notes (kind 1624) and NIP-32 label events (kind 1985) referencing a
|
/// Cover notes (kind 1624) and NIP-32 label events (kind 1985) referencing
|
||||||
/// specific root event (`#e` tag), fetched per root like comments and
|
/// any of the given root events (`#e` tag), fetched per root like comments
|
||||||
/// statuses because they carry no repository `a` tag.
|
/// and statuses because they carry no repository `a` tag. Batched, like
|
||||||
pub fn annotations_for(root: EventId) -> Filter {
|
/// [`statuses_for`].
|
||||||
|
pub fn annotations_for(roots: impl IntoIterator<Item = EventId>) -> Filter {
|
||||||
Filter::new()
|
Filter::new()
|
||||||
.kinds([crate::COVER_NOTE_KIND, Kind::Label])
|
.kinds([crate::COVER_NOTE_KIND, Kind::Label])
|
||||||
.event(root)
|
.events(roots)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A user's grasp list (kind `10317`).
|
/// A user's grasp list (kind `10317`).
|
||||||
@@ -71,11 +76,10 @@ pub fn grasp_list(public_key: PublicKey) -> Filter {
|
|||||||
/// NIP-22 comments (kind `1111`) referencing any of the given root events
|
/// NIP-22 comments (kind `1111`) referencing any of the given root events
|
||||||
/// (issues, patches, PRs).
|
/// (issues, patches, PRs).
|
||||||
///
|
///
|
||||||
/// Comments are not addressed to the repository — they carry no `a` tag with
|
/// Comments carry no repository `a` tag, so they must be fetched by their
|
||||||
/// the repo coordinate — so they must be fetched by their root reference
|
/// root reference. NIP-22 defines the uppercase `E` tag as the thread root
|
||||||
/// instead. NIP-22 defines the uppercase `E` tag as the root of the thread
|
/// (used by ngit), but some clients (including Signed) use a lowercase `e`
|
||||||
/// (used by ngit) while some clients (including Signed itself) reference the
|
/// tag, so both are matched.
|
||||||
/// root with a lowercase `e` tag, so both are matched.
|
|
||||||
///
|
///
|
||||||
/// Returns two filters because `#E` and `#e` conditions would be ANDed if
|
/// Returns two filters because `#E` and `#e` conditions would be ANDed if
|
||||||
/// combined into one.
|
/// combined into one.
|
||||||
@@ -110,12 +114,28 @@ pub fn all_announcements() -> Filter {
|
|||||||
Filter::new().kind(Kind::GitRepoAnnouncement)
|
Filter::new().kind(Kind::GitRepoAnnouncement)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All deletion-related events (NIP-09 kind `5`, NIP-62 kind `62`).
|
/// How far back deletion requests are fetched and stored.
|
||||||
///
|
///
|
||||||
/// Unbounded, like [`all_announcements`]: deletion requests must be known
|
/// A deletion request can only target events created before it, and NIP-34
|
||||||
/// before any other event can be shown.
|
/// events are all far younger than this window, so older requests can never
|
||||||
|
/// match anything shown. Bounding the window keeps the kind-5/62 set (one of
|
||||||
|
/// the largest on public relays) from being fully reconciled on every sync.
|
||||||
|
const DELETIONS_LOOKBACK: Duration = Duration::from_secs(3 * 365 * 86_400);
|
||||||
|
|
||||||
|
/// `now` minus [`DELETIONS_LOOKBACK`], quantized to whole days so identical
|
||||||
|
/// filters hash the same and the backend's sync dedup can match them.
|
||||||
|
fn deletions_since() -> Timestamp {
|
||||||
|
let now = Timestamp::now().as_secs();
|
||||||
|
Timestamp::from_secs(now - now % 86_400) - DELETIONS_LOOKBACK
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All deletion-related events (NIP-09 kind `5`, NIP-62 kind `62`) within
|
||||||
|
/// [`DELETIONS_LOOKBACK`]. Deletion requests must be known before any other
|
||||||
|
/// event can be shown.
|
||||||
pub fn deletions() -> Filter {
|
pub fn deletions() -> Filter {
|
||||||
Filter::new().kinds([Kind::EventDeletion, Kind::RequestToVanish])
|
Filter::new()
|
||||||
|
.kinds([Kind::EventDeletion, Kind::RequestToVanish])
|
||||||
|
.since(deletions_since())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletion events relevant to a single repository: requests authored by
|
/// Deletion events relevant to a single repository: requests authored by
|
||||||
|
|||||||
@@ -70,10 +70,9 @@ const SCAN_SKIPPED_DIRS: [&str; 1] = ["node_modules"];
|
|||||||
/// Walk `root` recursively and collect the paths of git repositories
|
/// Walk `root` recursively and collect the paths of git repositories
|
||||||
/// (directories containing a `.git` entry) below it.
|
/// (directories containing a `.git` entry) below it.
|
||||||
///
|
///
|
||||||
/// Hidden entries and symlinks are skipped, and directories that are
|
/// Hidden entries and symlinks are skipped; repositories are not descended
|
||||||
/// themselves repositories are not descended into (so nested repositories,
|
/// into, so nested ones (e.g. submodule worktrees) are not reported.
|
||||||
/// like submodule worktrees, are not reported). Results are canonicalized,
|
/// Results are canonicalized, deduplicated and sorted.
|
||||||
/// deduplicated and sorted by path.
|
|
||||||
pub fn find_git_repos(root: &Path) -> Vec<PathBuf> {
|
pub fn find_git_repos(root: &Path) -> Vec<PathBuf> {
|
||||||
let mut repos = Vec::new();
|
let mut repos = Vec::new();
|
||||||
if !root.is_dir() {
|
if !root.is_dir() {
|
||||||
@@ -121,11 +120,9 @@ pub fn find_git_repos(root: &Path) -> Vec<PathBuf> {
|
|||||||
/// Clone a repository into `path` from the first working URL in
|
/// Clone a repository into `path` from the first working URL in
|
||||||
/// `clone_urls` (the announcement's `clone` tag), then fetch the
|
/// `clone_urls` (the announcement's `clone` tag), then fetch the
|
||||||
/// `refs/nostr/*` PR refs like the cache clone does. The destination must
|
/// `refs/nostr/*` PR refs like the cache clone does. The destination must
|
||||||
/// not exist yet; it is created by the clone. The first URL that works
|
/// not exist yet. When no URL works, the last error is returned.
|
||||||
/// wins; when none do, the error of the last failing URL is returned.
|
|
||||||
///
|
///
|
||||||
/// Unlike [`GitCache::ensure_clone`], the clone is not kept in any cache;
|
/// Unlike [`GitCache::ensure_clone`], the clone is not kept in any cache.
|
||||||
/// callers open it themselves if they need a [`gix::Repository`].
|
|
||||||
pub fn clone_repo(clone_urls: &[String], path: &Path) -> Result<()> {
|
pub fn clone_repo(clone_urls: &[String], path: &Path) -> Result<()> {
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
bail!("destination {} already exists", path.display());
|
bail!("destination {} already exists", path.display());
|
||||||
@@ -218,8 +215,8 @@ fn clone(url: &str, path: &Path) -> Result<gix::Repository> {
|
|||||||
/// `README.md` derived from `name`/`description`, and create the initial
|
/// `README.md` derived from `name`/`description`, and create the initial
|
||||||
/// commit. Returns the initial commit id.
|
/// commit. Returns the initial commit id.
|
||||||
///
|
///
|
||||||
/// Uses the git CLI (like [`apply_patch`]) because it handles the plumbing
|
/// Uses the git CLI (like [`apply_patch`]), which handles index writes,
|
||||||
/// (index writes, ref updates, default branch selection) natively.
|
/// ref updates and default branch selection natively.
|
||||||
pub fn init_repository(path: &Path, name: &str, description: &str) -> Result<String> {
|
pub fn init_repository(path: &Path, name: &str, description: &str) -> Result<String> {
|
||||||
std::fs::create_dir_all(path)
|
std::fs::create_dir_all(path)
|
||||||
.with_context(|| format!("failed to create {}", path.display()))?;
|
.with_context(|| format!("failed to create {}", path.display()))?;
|
||||||
@@ -371,11 +368,8 @@ fn git_in(dir: &Path, args: &[&str]) -> Result<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Map an untrusted repository id (or display name) to a safe single path
|
/// Map an untrusted repository id (or display name) to a safe single path
|
||||||
/// component.
|
/// component: everything outside `[A-Za-z0-9._-]` becomes `_`, and the
|
||||||
///
|
/// special components `.` and `..` are rejected.
|
||||||
/// Replaces everything outside `[A-Za-z0-9._-]` with `_`, and rejects the
|
|
||||||
/// special components `.` and `..` so the id can't escape a directory it is
|
|
||||||
/// joined onto.
|
|
||||||
pub fn sanitize_path_component(id: &str) -> String {
|
pub fn sanitize_path_component(id: &str) -> String {
|
||||||
let sanitized: String = id
|
let sanitized: String = id
|
||||||
.chars()
|
.chars()
|
||||||
@@ -517,12 +511,9 @@ fn file_commit(commit: &gix::Commit<'_>, include_description: bool) -> Result<Fi
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Find the most recent commit that changed `rel` (a path relative to the
|
/// Find the most recent commit that changed `rel` (a path relative to the
|
||||||
/// worktree), like `git log -1 -- <rel>` does for non-merge commits.
|
/// worktree), like `git log -1 -- <rel>`: the first commit, walking from
|
||||||
///
|
/// `HEAD` newest-first, whose tree entry for `rel` differs from its first
|
||||||
/// Walks history from `HEAD` newest-first and returns the first commit whose
|
/// parent's. `Ok(None)` when no commit touched the file (e.g. untracked).
|
||||||
/// tree entry for `rel` differs from its first parent's; a merge that only
|
|
||||||
/// changed the file through its second parent is therefore not reported.
|
|
||||||
/// Returns `Ok(None)` if no commit touched the file (e.g. untracked files).
|
|
||||||
pub fn last_commit(repo: &gix::Repository, rel: &Path) -> Result<Option<FileCommit>> {
|
pub fn last_commit(repo: &gix::Repository, rel: &Path) -> Result<Option<FileCommit>> {
|
||||||
let rel = rel.to_path_buf();
|
let rel = rel.to_path_buf();
|
||||||
Ok(last_commits(repo, std::slice::from_ref(&rel))?
|
Ok(last_commits(repo, std::slice::from_ref(&rel))?
|
||||||
@@ -727,10 +718,8 @@ pub struct CommitDiff {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The changes of the commit `id` (short or full) in the repository at
|
/// The changes of the commit `id` (short or full) in the repository at
|
||||||
/// `workdir`, compared against its first parent (the empty tree for the root
|
/// `workdir`, compared against its first parent (the empty tree for the
|
||||||
/// commit), like `git show`. Directory entries and submodules are skipped;
|
/// root commit), like `git show`. Files are sorted by path.
|
||||||
/// their contents are reported as individual file changes. Files are sorted
|
|
||||||
/// by path.
|
|
||||||
pub fn worktree_commit_diff(workdir: &Path, id: &str) -> Result<CommitDiff> {
|
pub fn worktree_commit_diff(workdir: &Path, id: &str) -> Result<CommitDiff> {
|
||||||
commit_diff(&open_with_cache(workdir)?, id)
|
commit_diff(&open_with_cache(workdir)?, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ bitcoin_hashes = "1"
|
|||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use anyhow::{Error, anyhow, bail};
|
use anyhow::{Error, anyhow, bail};
|
||||||
use bitcoin_hashes::sha1::Hash as Sha1Hash;
|
use bitcoin_hashes::sha1::Hash as Sha1Hash;
|
||||||
@@ -32,6 +34,12 @@ pub const BOOTSTRAP_RELAYS: [&str; 4] = [
|
|||||||
/// Relays used for indexing user's relay list (NIP-65).
|
/// Relays used for indexing user's relay list (NIP-65).
|
||||||
pub const INDEXER_RELAYS: [&str; 2] = ["wss://indexer.coracle.social", "wss://user.kindpag.es"];
|
pub const INDEXER_RELAYS: [&str; 2] = ["wss://indexer.coracle.social", "wss://user.kindpag.es"];
|
||||||
|
|
||||||
|
/// How long an identical fetch/sync request is suppressed after it started.
|
||||||
|
/// A second panel for the same repository (or the global and per-author
|
||||||
|
/// list stores at login) doesn't duplicate a sync that just ran; after the
|
||||||
|
/// window, re-fetching is allowed again so data stays fresh.
|
||||||
|
const FETCH_DEDUP_WINDOW: Duration = Duration::from_secs(5 * 60);
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum BackendEvent {
|
pub enum BackendEvent {
|
||||||
/// User has no signer configured.
|
/// User has no signer configured.
|
||||||
@@ -84,6 +92,10 @@ pub struct Backend {
|
|||||||
/// Whether the stored credential is NIP-49 encrypted and a passphrase
|
/// Whether the stored credential is NIP-49 encrypted and a passphrase
|
||||||
/// is still needed to resume the session.
|
/// is still needed to resume the session.
|
||||||
passphrase_required: bool,
|
passphrase_required: bool,
|
||||||
|
/// Fingerprints of recently started fetches/syncs (relay + filter set),
|
||||||
|
/// so duplicate requests within [`FETCH_DEDUP_WINDOW`] collapse into
|
||||||
|
/// one. Entries are pruned lazily on the next request.
|
||||||
|
recent_fetches: HashMap<u64, Instant>,
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +146,7 @@ impl Backend {
|
|||||||
connected: false,
|
connected: false,
|
||||||
sync_progress: None,
|
sync_progress: None,
|
||||||
passphrase_required: false,
|
passphrase_required: false,
|
||||||
|
recent_fetches: HashMap::new(),
|
||||||
tasks: vec![pump],
|
tasks: vec![pump],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -245,9 +258,8 @@ impl Backend {
|
|||||||
/// Decrypt the NIP-49 encrypted credential stored in the keyring with
|
/// Decrypt the NIP-49 encrypted credential stored in the keyring with
|
||||||
/// the given passphrase and resume the session.
|
/// the given passphrase and resume the session.
|
||||||
///
|
///
|
||||||
/// The scrypt decryption runs off the UI thread. The returned task
|
/// The scrypt decryption runs off the UI thread. The task yields the
|
||||||
/// yields the public key on success, or the failure reason (e.g. wrong
|
/// public key, or the failure reason (e.g. wrong passphrase).
|
||||||
/// passphrase), so callers can render inline errors.
|
|
||||||
pub fn restore_with_passphrase(
|
pub fn restore_with_passphrase(
|
||||||
&mut self,
|
&mut self,
|
||||||
password: &str,
|
password: &str,
|
||||||
@@ -286,9 +298,8 @@ impl Backend {
|
|||||||
/// passphrase (NIP-49) and persist it in the keyring, then publish the
|
/// passphrase (NIP-49) and persist it in the keyring, then publish the
|
||||||
/// user's NIP-65 relay list, metadata and grasp list.
|
/// user's NIP-65 relay list, metadata and grasp list.
|
||||||
///
|
///
|
||||||
/// The heavy encryption runs off the UI thread. The returned task yields
|
/// The encryption runs off the UI thread; the task yields the new
|
||||||
/// the new public key on success, or the failure reason, so callers can
|
/// public key.
|
||||||
/// render progress and inline errors.
|
|
||||||
pub fn create_identity(
|
pub fn create_identity(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: &str,
|
name: &str,
|
||||||
@@ -386,11 +397,8 @@ impl Backend {
|
|||||||
/// a push for a not-yet-existing repository while that authorization is
|
/// a push for a not-yet-existing repository while that authorization is
|
||||||
/// pending (it expires after 30 minutes), like gitworkshop and ngit.
|
/// pending (it expires after 30 minutes), like gitworkshop and ngit.
|
||||||
///
|
///
|
||||||
/// The git work (init, commit, push) runs on background threads. The
|
/// The git work runs on background threads; the task yields the
|
||||||
/// returned task yields the published announcement on success, so
|
/// published announcement.
|
||||||
/// callers can open the new repository right away. The announcement's
|
|
||||||
/// `relays` tag carries the grasp servers, which are also added to the
|
|
||||||
/// relay pool so the published events reach them.
|
|
||||||
pub fn create_repository(
|
pub fn create_repository(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: &str,
|
name: &str,
|
||||||
@@ -435,8 +443,7 @@ impl Backend {
|
|||||||
let servers = grasp_servers.clone();
|
let servers = grasp_servers.clone();
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
// 1. Initialize the local clone (main branch + README + initial
|
// Initialize the local clone (main branch + README + initial commit).
|
||||||
// commit) on a background thread.
|
|
||||||
let work = cx.background_spawn({
|
let work = cx.background_spawn({
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
let name = name.clone();
|
let name = name.clone();
|
||||||
@@ -467,16 +474,14 @@ impl Backend {
|
|||||||
let commit_sha =
|
let commit_sha =
|
||||||
Sha1Hash::from_str(&commit).map_err(|_| anyhow!("invalid initial commit id"))?;
|
Sha1Hash::from_str(&commit).map_err(|_| anyhow!("invalid initial commit id"))?;
|
||||||
|
|
||||||
// 2. Ensure the grasp servers are in the relay pool; the nostr
|
// The nostr client queues events until each relay is connected.
|
||||||
// client queues events until each relay is connected.
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
let urls: Vec<String> = servers.iter().map(ToString::to_string).collect();
|
let urls: Vec<String> = servers.iter().map(ToString::to_string).collect();
|
||||||
this.add_relays(urls, cx);
|
this.add_relays(urls, cx);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// 3. Publish the announcement, then the state event, to the
|
// The state event is the push authorization ("purgatory"), so
|
||||||
// grasp relays. The state event is the push authorization
|
// it must be accepted before the push below.
|
||||||
// ("purgatory"), so it must be accepted before step 4.
|
|
||||||
let announcement = GitRepositoryAnnouncement {
|
let announcement = GitRepositoryAnnouncement {
|
||||||
id: repo_id.clone(),
|
id: repo_id.clone(),
|
||||||
name: Some(name.clone()),
|
name: Some(name.clone()),
|
||||||
@@ -522,9 +527,8 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. Push the initial commit to every grasp server. A server
|
// Push to every grasp server; creation only fails when no
|
||||||
// that fails to accept the push is logged, but the creation
|
// server accepted it.
|
||||||
// only fails when no server accepted it.
|
|
||||||
let push = cx.background_spawn({
|
let push = cx.background_spawn({
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
let owner = owner.clone();
|
let owner = owner.clone();
|
||||||
@@ -555,14 +559,8 @@ impl Backend {
|
|||||||
/// state to the grasp relays, then push every branch and tag to each
|
/// state to the grasp relays, then push every branch and tag to each
|
||||||
/// grasp server. Also points `origin` at the first grasp server.
|
/// grasp server. Also points `origin` at the first grasp server.
|
||||||
///
|
///
|
||||||
/// The events must reach the grasp servers *before* the push, like
|
/// Same ordering constraint as [`Self::create_repository`]: the state
|
||||||
/// [`Self::create_repository`]: GRASP servers hold the signed state
|
/// event ("purgatory") must be accepted before the push.
|
||||||
/// event in "purgatory" and only accept a push while that
|
|
||||||
/// authorization is pending.
|
|
||||||
///
|
|
||||||
/// The git work (ref listing, push) runs on background threads. The
|
|
||||||
/// returned task yields the published announcement on success, so
|
|
||||||
/// callers can switch the repository into its NIP-34 mode.
|
|
||||||
pub fn publish_local_repo(
|
pub fn publish_local_repo(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
@@ -586,9 +584,8 @@ impl Backend {
|
|||||||
return Task::ready(Err(anyhow!("Sign in to publish a repository")));
|
return Task::ready(Err(anyhow!("Sign in to publish a repository")));
|
||||||
};
|
};
|
||||||
|
|
||||||
// The repository identifier is derived from the name, like
|
// The repository identifier is derived from the name as in
|
||||||
// [`Self::create_repository`]: spaces become hyphens, other
|
// [`Self::create_repository`].
|
||||||
// non-alphanumeric characters (except `/`) become hyphens.
|
|
||||||
let repo_id = identifier_from_name(&name);
|
let repo_id = identifier_from_name(&name);
|
||||||
|
|
||||||
if repo_id.is_empty() || repo_id.len() > 100 {
|
if repo_id.is_empty() || repo_id.len() > 100 {
|
||||||
@@ -607,8 +604,6 @@ impl Backend {
|
|||||||
let servers = grasp_servers.clone();
|
let servers = grasp_servers.clone();
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
// 1. Read the local repository's refs (branches, tags, HEAD)
|
|
||||||
// and its root commit on a background thread.
|
|
||||||
let work = cx.background_spawn({
|
let work = cx.background_spawn({
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
async move {
|
async move {
|
||||||
@@ -619,16 +614,14 @@ impl Backend {
|
|||||||
});
|
});
|
||||||
let (state, euc) = work.await?;
|
let (state, euc) = work.await?;
|
||||||
|
|
||||||
// 2. Ensure the grasp servers are in the relay pool; the nostr
|
// The nostr client queues events until each relay is connected.
|
||||||
// client queues events until each relay is connected.
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
let urls: Vec<String> = servers.iter().map(ToString::to_string).collect();
|
let urls: Vec<String> = servers.iter().map(ToString::to_string).collect();
|
||||||
this.add_relays(urls, cx);
|
this.add_relays(urls, cx);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// 3. Publish the announcement, then the state event, to the
|
// The state event is the push authorization ("purgatory"), so
|
||||||
// grasp relays. The state event is the push authorization
|
// it must be accepted before the push below.
|
||||||
// ("purgatory"), so it must be accepted before step 4.
|
|
||||||
let announcement = GitRepositoryAnnouncement {
|
let announcement = GitRepositoryAnnouncement {
|
||||||
id: repo_id.clone(),
|
id: repo_id.clone(),
|
||||||
name: Some(name.clone()),
|
name: Some(name.clone()),
|
||||||
@@ -672,10 +665,9 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. Push every branch and tag to each grasp server. A server
|
// Push every branch and tag to each grasp server; the init
|
||||||
// that fails to accept the push is logged, but the init only
|
// only fails when no server accepted it. An empty repository
|
||||||
// fails when no server accepted it. An empty repository
|
// has nothing to push.
|
||||||
// (no refs yet) has nothing to push.
|
|
||||||
if !refs.is_empty() {
|
if !refs.is_empty() {
|
||||||
let push = cx.background_spawn({
|
let push = cx.background_spawn({
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
@@ -697,8 +689,8 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Point `origin` at the first grasp server so later pushes
|
// Point `origin` at the first grasp server so later pushes
|
||||||
// have a target, like the create flow.
|
// have a target.
|
||||||
if let Some(base) = servers.first().and_then(grasp_base_url) {
|
if let Some(base) = servers.first().and_then(grasp_base_url) {
|
||||||
let url = format!("{base}/{owner}/{repo_id}.git");
|
let url = format!("{base}/{owner}/{repo_id}.git");
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
@@ -732,15 +724,13 @@ impl Backend {
|
|||||||
let relays = announcement.relays.clone();
|
let relays = announcement.relays.clone();
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
// 1. Read the current refs of the local clone.
|
|
||||||
let work = cx.background_spawn({
|
let work = cx.background_spawn({
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
async move { signed_git::worktree_ref_state(&path) }
|
async move { signed_git::worktree_ref_state(&path) }
|
||||||
});
|
});
|
||||||
let state = work.await?;
|
let state = work.await?;
|
||||||
|
|
||||||
// 2. Publish a fresh state event; grasp servers authorize a
|
// Grasp servers authorize a push by the state they have seen.
|
||||||
// push by the state they have seen.
|
|
||||||
let refs = state.refs.clone();
|
let refs = state.refs.clone();
|
||||||
let head = state.head.clone();
|
let head = state.head.clone();
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
@@ -749,7 +739,6 @@ impl Backend {
|
|||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// 3. Push every branch and tag to the announced grasp servers.
|
|
||||||
if !refs.is_empty() {
|
if !refs.is_empty() {
|
||||||
let push = cx.background_spawn({
|
let push = cx.background_spawn({
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
@@ -1106,25 +1095,53 @@ impl Backend {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether an identical fetch was started within [`FETCH_DEDUP_WINDOW`]
|
||||||
|
/// and is still recent enough to suppress a duplicate. Records the
|
||||||
|
/// fingerprint (after pruning expired entries) when returning `false`.
|
||||||
|
fn fetch_recently_started(&mut self, fingerprint: u64) -> bool {
|
||||||
|
self.recent_fetches
|
||||||
|
.retain(|_, started| started.elapsed() < FETCH_DEDUP_WINDOW);
|
||||||
|
if self.recent_fetches.contains_key(&fingerprint) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
self.recent_fetches.insert(fingerprint, Instant::now());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Connect to relays announced by a repository (NIP-34 `relays` tag) and
|
/// Connect to relays announced by a repository (NIP-34 `relays` tag) and
|
||||||
/// fetch its events from them: a one-shot auto-closing subscription for
|
/// fetch its events from them: a one-shot auto-closing subscription for
|
||||||
/// `filters`, plus a negentropy sync so issues, patches and PRs stored
|
/// `filters`, plus a negentropy sync so issues, patches and PRs stored
|
||||||
/// only on those relays are not missed.
|
/// only on those relays are not missed.
|
||||||
///
|
///
|
||||||
/// Best-effort: failures are logged, not surfaced, because the bootstrap
|
/// Deduplicated: an identical request (same relays and filters) started
|
||||||
/// relays already cover the repository. The relays stay in the pool, so
|
/// within [`FETCH_DEDUP_WINDOW`] is skipped, so a second panel for the
|
||||||
/// events the user publishes for this repository also reach them.
|
/// same repository doesn't re-run the fetch.
|
||||||
|
///
|
||||||
|
/// Best-effort: failures are logged, not surfaced. The relays stay in
|
||||||
|
/// the pool, so later publishes for this repository also reach them.
|
||||||
pub fn connect_repo_relays(
|
pub fn connect_repo_relays(
|
||||||
&mut self,
|
&mut self,
|
||||||
relays: Vec<RelayUrl>,
|
relays: Vec<RelayUrl>,
|
||||||
filters: Vec<Filter>,
|
filters: Vec<Filter>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
let relay_strs: Vec<&str> = relays.iter().map(|url| url.as_str()).collect();
|
||||||
|
let fingerprint = fetch_fingerprint(&relay_strs, &filters);
|
||||||
|
if self.fetch_recently_started(fingerprint) {
|
||||||
|
log::debug!("skipping duplicate repo relay fetch");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |_this, _cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
if let Err(e) = connect_repo_relays_only(&client, relays, filters).await {
|
if let Err(e) = connect_repo_relays_only(&client, relays, filters).await {
|
||||||
log::warn!("repo relay fetch failed: {e}");
|
log::warn!("repo relay fetch failed: {e}");
|
||||||
|
// Allow an immediate retry after a failure.
|
||||||
|
this.update(cx, |this, _cx| {
|
||||||
|
this.recent_fetches.remove(&fingerprint);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
@@ -1152,7 +1169,17 @@ impl Backend {
|
|||||||
/// reconciles the local database with the relays in both directions.
|
/// reconciles the local database with the relays in both directions.
|
||||||
/// Emits [`BackendEvent::SyncProgress`] while running (throttled to
|
/// Emits [`BackendEvent::SyncProgress`] while running (throttled to
|
||||||
/// whole-percent changes) and [`BackendEvent::Synced`] on completion.
|
/// whole-percent changes) and [`BackendEvent::Synced`] on completion.
|
||||||
|
///
|
||||||
|
/// Deduplicated: an identical sync started within
|
||||||
|
/// [`FETCH_DEDUP_WINDOW`] is skipped. Observers still see the original
|
||||||
|
/// sync's progress and completion events.
|
||||||
pub fn sync_bootstrap(&mut self, filter: Filter, cx: &mut Context<Self>) {
|
pub fn sync_bootstrap(&mut self, filter: Filter, cx: &mut Context<Self>) {
|
||||||
|
let fingerprint = fetch_fingerprint(&BOOTSTRAP_RELAYS, std::slice::from_ref(&filter));
|
||||||
|
if self.fetch_recently_started(fingerprint) {
|
||||||
|
log::debug!("skipping duplicate bootstrap sync");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
|
|
||||||
self.sync_progress = Some((0, 0));
|
self.sync_progress = Some((0, 0));
|
||||||
@@ -1210,6 +1237,8 @@ impl Backend {
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.sync_progress = None;
|
this.sync_progress = None;
|
||||||
|
// Allow an immediate retry after a failure.
|
||||||
|
this.recent_fetches.remove(&fingerprint);
|
||||||
cx.emit(BackendEvent::error(e.to_string()))
|
cx.emit(BackendEvent::error(e.to_string()))
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
@@ -1221,10 +1250,9 @@ impl Backend {
|
|||||||
/// Sign, broadcast and locally store an event. Emits
|
/// Sign, broadcast and locally store an event. Emits
|
||||||
/// [`BackendEvent::Published`] on success so stores can refresh.
|
/// [`BackendEvent::Published`] on success so stores can refresh.
|
||||||
///
|
///
|
||||||
/// The returned task yields the outcome of this specific action, so
|
/// The task yields the outcome of this specific action (for inline
|
||||||
/// callers can show inline progress/errors instead of relying on
|
/// progress/errors) and is owned by the caller; dropping it cancels
|
||||||
/// the global [`BackendEvent::Error`]. The task is owned by the caller;
|
/// the publish.
|
||||||
/// dropping it cancels the publish.
|
|
||||||
pub fn send(
|
pub fn send(
|
||||||
&mut self,
|
&mut self,
|
||||||
builder: EventBuilder,
|
builder: EventBuilder,
|
||||||
@@ -1329,6 +1357,20 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fingerprint of a relay + filter set, for fetch dedup. Relays and
|
||||||
|
/// filters are sorted first so the fingerprint is order-independent.
|
||||||
|
fn fetch_fingerprint(relays: &[&str], filters: &[Filter]) -> u64 {
|
||||||
|
let mut relays: Vec<&str> = relays.to_vec();
|
||||||
|
relays.sort_unstable();
|
||||||
|
let mut filters: Vec<&Filter> = filters.iter().collect();
|
||||||
|
filters.sort_unstable();
|
||||||
|
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
relays.hash(&mut hasher);
|
||||||
|
filters.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
/// Add the given relays, connect to them, and fetch the filters: a one-shot
|
/// Add the given relays, connect to them, and fetch the filters: a one-shot
|
||||||
/// subscription (auto-closing after EOSE) plus a negentropy sync per filter
|
/// subscription (auto-closing after EOSE) plus a negentropy sync per filter
|
||||||
/// as a second pass, so events that race with the subscription or relays
|
/// as a second pass, so events that race with the subscription or relays
|
||||||
@@ -1343,10 +1385,15 @@ async fn connect_repo_relays_only(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut added = false;
|
||||||
for url in &relays {
|
for url in &relays {
|
||||||
client.add_relay(url).await?;
|
added |= client.add_relay(url).await?;
|
||||||
}
|
}
|
||||||
|
// Connecting is only needed when the pool grew; connected relays no-op,
|
||||||
|
// but the call still iterates every relay in the pool.
|
||||||
|
if added {
|
||||||
client.connect().await;
|
client.connect().await;
|
||||||
|
}
|
||||||
|
|
||||||
let opts = SubscribeAutoCloseOptions::default()
|
let opts = SubscribeAutoCloseOptions::default()
|
||||||
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
||||||
@@ -1358,17 +1405,21 @@ async fn connect_repo_relays_only(
|
|||||||
.collect();
|
.collect();
|
||||||
client.subscribe(target).close_on(opts).await?;
|
client.subscribe(target).close_on(opts).await?;
|
||||||
|
|
||||||
for filter in filters {
|
// Sync the filters concurrently: each reconciles against every relay
|
||||||
|
// either way, and a relay without NEG-XX support otherwise serializes
|
||||||
|
// its initial timeout behind every other filter.
|
||||||
let sync_opts = SyncOptions::default().initial_timeout(Duration::from_secs(5));
|
let sync_opts = SyncOptions::default().initial_timeout(Duration::from_secs(5));
|
||||||
if let Err(e) = client
|
let syncs = filters.into_iter().map(|filter| {
|
||||||
.sync(filter)
|
let client = &client;
|
||||||
.with(relays.iter())
|
let relays = &relays;
|
||||||
.opts(sync_opts)
|
let sync_opts = sync_opts.clone();
|
||||||
.await
|
async move {
|
||||||
{
|
if let Err(e) = client.sync(filter).with(relays.iter()).opts(sync_opts).await {
|
||||||
log::warn!("repo relay negentropy sync failed: {e}");
|
log::warn!("repo relay negentropy sync failed: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
futures::future::join_all(syncs).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ pub fn init(db_path: impl AsRef<Path>, cx: &mut App) -> Entity<Backend> {
|
|||||||
.install_default()
|
.install_default()
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
// Initialize the nostr client and universal signer.
|
|
||||||
let (client, signer) = cx.foreground_executor().block_on(async move {
|
let (client, signer) = cx.foreground_executor().block_on(async move {
|
||||||
let path = db_path.as_ref().to_path_buf();
|
let path = db_path.as_ref().to_path_buf();
|
||||||
new_backend(path)
|
new_backend(path)
|
||||||
@@ -42,24 +41,18 @@ pub fn init(db_path: impl AsRef<Path>, cx: &mut App) -> Entity<Backend> {
|
|||||||
.expect("failed to initialize nostr backend")
|
.expect("failed to initialize nostr backend")
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize the backend and stores.
|
|
||||||
let entity = cx.new(|cx| Backend::new(client, signer, cx));
|
let entity = cx.new(|cx| Backend::new(client, signer, cx));
|
||||||
Backend::set_global(entity.clone(), cx);
|
Backend::set_global(entity.clone(), cx);
|
||||||
|
|
||||||
// Initialize the profile store.
|
|
||||||
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
|
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
|
||||||
|
|
||||||
// Start the explore list from the local database before
|
// Seed the explore list from the local database; relay syncs continue
|
||||||
// the first window opens, relay syncs continue in the background,
|
// in the background.
|
||||||
// so the list never waits for them.
|
|
||||||
RepoListStore::set_global(cx.new(|cx| RepoListStore::new(None, cx)), cx);
|
RepoListStore::set_global(cx.new(|cx| RepoListStore::new(None, cx)), cx);
|
||||||
|
|
||||||
// The clone cache is only meaningful on native platforms,
|
// The clone cache is native-only; wasm registers an empty store so
|
||||||
// the wasm build registers an empty store so `GitStore::global` still works.
|
// `GitStore::global` still works.
|
||||||
GitStore::set_global(PathBuf::new(), cx);
|
GitStore::set_global(PathBuf::new(), cx);
|
||||||
|
|
||||||
// Scan the default directories (Desktop, Documents) for local git
|
|
||||||
// repositories; the sidebar lists them next to the user's NIP-34 repos.
|
|
||||||
LocalReposStore::set_global(
|
LocalReposStore::set_global(
|
||||||
cx.new(|cx| LocalReposStore::new(default_scan_paths(), cx)),
|
cx.new(|cx| LocalReposStore::new(default_scan_paths(), cx)),
|
||||||
cx,
|
cx,
|
||||||
@@ -71,26 +64,13 @@ pub fn init(db_path: impl AsRef<Path>, cx: &mut App) -> Entity<Backend> {
|
|||||||
/// Initialize the backend with an in-memory database on wasm.
|
/// Initialize the backend with an in-memory database on wasm.
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub fn init(cx: &mut App) -> Entity<Backend> {
|
pub fn init(cx: &mut App) -> Entity<Backend> {
|
||||||
// Initialize the nostr client and universal signer.
|
|
||||||
let (client, signer) = new_backend().expect("failed to initialize nostr backend");
|
let (client, signer) = new_backend().expect("failed to initialize nostr backend");
|
||||||
|
|
||||||
// Initialize the backend and stores.
|
|
||||||
let entity = cx.new(|cx| Backend::new(client, signer, cx));
|
let entity = cx.new(|cx| Backend::new(client, signer, cx));
|
||||||
Backend::set_global(entity.clone(), cx);
|
Backend::set_global(entity.clone(), cx);
|
||||||
|
|
||||||
// Initialize the profile store.
|
|
||||||
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
|
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
|
||||||
|
|
||||||
// Start the explore list from the local database before
|
|
||||||
// the first window opens, relay syncs continue in the background,
|
|
||||||
// so the list never waits for them.
|
|
||||||
RepoListStore::set_global(cx.new(|cx| RepoListStore::new(None, cx)), cx);
|
RepoListStore::set_global(cx.new(|cx| RepoListStore::new(None, cx)), cx);
|
||||||
|
|
||||||
// The clone cache is only meaningful on native platforms,
|
|
||||||
// the wasm build registers an empty store so `GitStore::global` still works.
|
|
||||||
GitStore::set_global(PathBuf::new(), cx);
|
GitStore::set_global(PathBuf::new(), cx);
|
||||||
|
|
||||||
// No filesystem scan on wasm: there are no local git repositories.
|
|
||||||
LocalReposStore::set_global(cx.new(|cx| LocalReposStore::new(Vec::new(), cx)), cx);
|
LocalReposStore::set_global(cx.new(|cx| LocalReposStore::new(Vec::new(), cx)), cx);
|
||||||
|
|
||||||
entity
|
entity
|
||||||
|
|||||||
+134
-66
@@ -1,5 +1,5 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
@@ -33,11 +33,21 @@ pub struct RepoStore {
|
|||||||
pub pull_requests: Vec<Event>,
|
pub pull_requests: Vec<Event>,
|
||||||
/// Comments on issues / PRs, oldest first.
|
/// Comments on issues / PRs, oldest first.
|
||||||
pub comments: Vec<Event>,
|
pub comments: Vec<Event>,
|
||||||
statuses: Vec<Event>,
|
/// Resolved status per root event (issue / patch / PR), recomputed on
|
||||||
|
/// every refresh so render paths are HashMap lookups instead of
|
||||||
|
/// scanning all status events per root.
|
||||||
|
status_by_root: HashMap<EventId, RepoStatus>,
|
||||||
|
/// Open issue / root PR counts, computed with [`Self::status_by_root`]
|
||||||
|
/// on every refresh.
|
||||||
|
open_issue_count: usize,
|
||||||
|
open_pr_count: usize,
|
||||||
/// Kind-1624 cover notes and kind-1985 label events referencing this
|
/// Kind-1624 cover notes and kind-1985 label events referencing this
|
||||||
/// repository's roots (ngit / GitWorkshop extensions).
|
/// repository's roots (ngit / GitWorkshop extensions).
|
||||||
cover_notes: Vec<Event>,
|
cover_notes: Vec<Event>,
|
||||||
labels: Vec<Event>,
|
labels: Vec<Event>,
|
||||||
|
/// Incremented on every applied refresh; views key their derived-data
|
||||||
|
/// caches to it instead of recomputing on every render.
|
||||||
|
version: u64,
|
||||||
/// Error of the last action initiated from this store, if any.
|
/// Error of the last action initiated from this store, if any.
|
||||||
pub last_error: Option<String>,
|
pub last_error: Option<String>,
|
||||||
/// Relays announced by this repository (NIP-34 `relays` tag) that we
|
/// Relays announced by this repository (NIP-34 `relays` tag) that we
|
||||||
@@ -111,9 +121,12 @@ impl RepoStore {
|
|||||||
patches: Vec::new(),
|
patches: Vec::new(),
|
||||||
pull_requests: Vec::new(),
|
pull_requests: Vec::new(),
|
||||||
comments: Vec::new(),
|
comments: Vec::new(),
|
||||||
statuses: Vec::new(),
|
status_by_root: HashMap::new(),
|
||||||
|
open_issue_count: 0,
|
||||||
|
open_pr_count: 0,
|
||||||
cover_notes: Vec::new(),
|
cover_notes: Vec::new(),
|
||||||
labels: Vec::new(),
|
labels: Vec::new(),
|
||||||
|
version: 0,
|
||||||
last_error: None,
|
last_error: None,
|
||||||
repo_relays: HashSet::new(),
|
repo_relays: HashSet::new(),
|
||||||
root_fetches: HashSet::new(),
|
root_fetches: HashSet::new(),
|
||||||
@@ -141,8 +154,13 @@ impl RepoStore {
|
|||||||
/// deletions targeting it.
|
/// deletions targeting it.
|
||||||
fn repo_filters(addr: &RepoAddr) -> Vec<Filter> {
|
fn repo_filters(addr: &RepoAddr) -> Vec<Filter> {
|
||||||
let mut filters = vec![
|
let mut filters = vec![
|
||||||
filters::announcement(addr),
|
// Announcement and state share author and identifier, so they
|
||||||
filters::state(addr),
|
// combine into one filter: one fewer negentropy reconciliation
|
||||||
|
// per relay when fetching from the repo's announced relays.
|
||||||
|
Filter::new()
|
||||||
|
.kinds([Kind::GitRepoAnnouncement, Kind::RepoState])
|
||||||
|
.author(addr.public_key)
|
||||||
|
.identifier(addr.identifier.clone()),
|
||||||
filters::activity(addr),
|
filters::activity(addr),
|
||||||
];
|
];
|
||||||
// Deletion requests (NIP-09/62) must be known before any event of
|
// Deletion requests (NIP-09/62) must be known before any event of
|
||||||
@@ -293,7 +311,7 @@ impl RepoStore {
|
|||||||
.chain(&pull_requests)
|
.chain(&pull_requests)
|
||||||
.map(|e| e.id);
|
.map(|e| e.id);
|
||||||
for root in roots {
|
for root in roots {
|
||||||
for event in db.query(filters::statuses_for(root)).await? {
|
for event in db.query(filters::statuses_for([root])).await? {
|
||||||
if seen_statuses.insert(event.id) {
|
if seen_statuses.insert(event.id) {
|
||||||
statuses.push(event);
|
statuses.push(event);
|
||||||
}
|
}
|
||||||
@@ -312,7 +330,7 @@ impl RepoStore {
|
|||||||
.chain(&pull_requests)
|
.chain(&pull_requests)
|
||||||
.map(|e| e.id);
|
.map(|e| e.id);
|
||||||
for root in roots {
|
for root in roots {
|
||||||
for event in db.query(filters::annotations_for(root)).await? {
|
for event in db.query(filters::annotations_for([root])).await? {
|
||||||
if deletions.is_deleted(&event) {
|
if deletions.is_deleted(&event) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -331,13 +349,36 @@ impl RepoStore {
|
|||||||
sort_newest_first(&mut cover_notes);
|
sort_newest_first(&mut cover_notes);
|
||||||
sort_newest_first(&mut labels);
|
sort_newest_first(&mut labels);
|
||||||
|
|
||||||
|
// Resolve every root's status once here; render paths do
|
||||||
|
// HashMap lookups instead of scanning all status events per
|
||||||
|
// root (quadratic, with an allocation per pair).
|
||||||
|
let maintainers = announcement
|
||||||
|
.as_ref()
|
||||||
|
.map(Announcement::effective_maintainers)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let status_by_root =
|
||||||
|
resolve_statuses(&issues, &patches, &pull_requests, &statuses, &maintainers);
|
||||||
|
let open_issue_count = issues
|
||||||
|
.iter()
|
||||||
|
.filter(|issue| status_of(&status_by_root, issue) == RepoStatus::Open)
|
||||||
|
.count();
|
||||||
|
let open_pr_count = pull_requests
|
||||||
|
.iter()
|
||||||
|
.filter(|pr| {
|
||||||
|
pr.kind == Kind::GitPullRequest
|
||||||
|
&& status_of(&status_by_root, pr) == RepoStatus::Open
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
|
||||||
Ok::<_, Error>((
|
Ok::<_, Error>((
|
||||||
announcement,
|
announcement,
|
||||||
state,
|
state,
|
||||||
issues,
|
issues,
|
||||||
patches,
|
patches,
|
||||||
pull_requests,
|
pull_requests,
|
||||||
statuses,
|
status_by_root,
|
||||||
|
open_issue_count,
|
||||||
|
open_pr_count,
|
||||||
comments,
|
comments,
|
||||||
cover_notes,
|
cover_notes,
|
||||||
labels,
|
labels,
|
||||||
@@ -353,7 +394,9 @@ impl RepoStore {
|
|||||||
issues,
|
issues,
|
||||||
patches,
|
patches,
|
||||||
pull_requests,
|
pull_requests,
|
||||||
statuses,
|
status_by_root,
|
||||||
|
open_issue_count,
|
||||||
|
open_pr_count,
|
||||||
comments,
|
comments,
|
||||||
cover_notes,
|
cover_notes,
|
||||||
labels,
|
labels,
|
||||||
@@ -389,9 +432,12 @@ impl RepoStore {
|
|||||||
this.patches = patches;
|
this.patches = patches;
|
||||||
this.pull_requests = pull_requests;
|
this.pull_requests = pull_requests;
|
||||||
this.comments = comments;
|
this.comments = comments;
|
||||||
this.statuses = statuses;
|
this.status_by_root = status_by_root;
|
||||||
|
this.open_issue_count = open_issue_count;
|
||||||
|
this.open_pr_count = open_pr_count;
|
||||||
this.cover_notes = cover_notes;
|
this.cover_notes = cover_notes;
|
||||||
this.labels = labels;
|
this.labels = labels;
|
||||||
|
this.version = this.version.wrapping_add(1);
|
||||||
|
|
||||||
// Comments, statuses without an `a` tag, cover notes and
|
// Comments, statuses without an `a` tag, cover notes and
|
||||||
// labels are not addressed to the repository, so fetch them
|
// labels are not addressed to the repository, so fetch them
|
||||||
@@ -411,25 +457,18 @@ impl RepoStore {
|
|||||||
.collect();
|
.collect();
|
||||||
if !new_roots.is_empty() {
|
if !new_roots.is_empty() {
|
||||||
this.root_fetches.extend(new_roots.iter().copied());
|
this.root_fetches.extend(new_roots.iter().copied());
|
||||||
let comment_filters = filters::comments_for(new_roots.clone());
|
// Batch the per-root filters: one statuses filter and one
|
||||||
let status_filters: Vec<Filter> = new_roots
|
// annotations filter covering all new roots, instead of
|
||||||
.iter()
|
// one filter per root (each filter is a separate
|
||||||
.copied()
|
// negentropy reconciliation per relay).
|
||||||
.map(filters::statuses_for)
|
let mut root_filters = filters::comments_for(new_roots.clone());
|
||||||
.collect();
|
root_filters.push(filters::statuses_for(new_roots.iter().copied()));
|
||||||
let annotation_filters: Vec<Filter> = new_roots
|
root_filters.push(filters::annotations_for(new_roots));
|
||||||
.into_iter()
|
|
||||||
.map(filters::annotations_for)
|
|
||||||
.collect();
|
|
||||||
let announced: Vec<RelayUrl> = this.repo_relays.iter().cloned().collect();
|
let announced: Vec<RelayUrl> = this.repo_relays.iter().cloned().collect();
|
||||||
let backend = Backend::global(cx);
|
let backend = Backend::global(cx);
|
||||||
backend.update(cx, |backend, cx| {
|
backend.update(cx, |backend, cx| {
|
||||||
backend.subscribe_bootstrap(comment_filters.clone(), cx);
|
backend.subscribe_bootstrap(root_filters.clone(), cx);
|
||||||
backend.connect_repo_relays(announced.clone(), comment_filters, cx);
|
backend.connect_repo_relays(announced, root_filters, cx);
|
||||||
backend.subscribe_bootstrap(status_filters.clone(), cx);
|
|
||||||
backend.connect_repo_relays(announced.clone(), status_filters, cx);
|
|
||||||
backend.subscribe_bootstrap(annotation_filters.clone(), cx);
|
|
||||||
backend.connect_repo_relays(announced, annotation_filters, cx);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,20 +493,17 @@ impl RepoStore {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the status of a root event (issue / patch / PR) per NIP-34.
|
/// Resolve the status of a root event (issue / patch / PR) per NIP-34:
|
||||||
|
/// a lookup into the map built on the last refresh.
|
||||||
pub fn status_of(&self, root: &Event) -> RepoStatus {
|
pub fn status_of(&self, root: &Event) -> RepoStatus {
|
||||||
let maintainers = self
|
status_of(&self.status_by_root, root)
|
||||||
.announcement
|
}
|
||||||
.as_ref()
|
|
||||||
.map(Announcement::effective_maintainers)
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let events = self
|
/// Refresh generation, incremented on every applied refresh. Views use
|
||||||
.statuses
|
/// it to key their derived-data caches (filtered lists, counts) so
|
||||||
.iter()
|
/// renders that change nothing stay O(1).
|
||||||
.filter(|e| signed_core::references_root(e, &root.id));
|
pub fn version(&self) -> u64 {
|
||||||
|
self.version
|
||||||
signed_core::resolve_status(events, &root.pubkey, &maintainers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The effective cover note of `root` (kind 1624), if any: the latest
|
/// The effective cover note of `root` (kind 1624), if any: the latest
|
||||||
@@ -509,21 +545,16 @@ impl RepoStore {
|
|||||||
|
|
||||||
/// Number of open issues: issues whose resolved status is
|
/// Number of open issues: issues whose resolved status is
|
||||||
/// [`RepoStatus::Open`] (issues without status events default to open).
|
/// [`RepoStatus::Open`] (issues without status events default to open).
|
||||||
|
/// Cached on the last refresh.
|
||||||
pub fn issue_count(&self) -> usize {
|
pub fn issue_count(&self) -> usize {
|
||||||
self.issues
|
self.open_issue_count
|
||||||
.iter()
|
|
||||||
.filter(|issue| self.status_of(issue) == RepoStatus::Open)
|
|
||||||
.count()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Number of open pull requests: root PR events (not PR updates, whose
|
/// Number of open pull requests: root PR events (not PR updates, whose
|
||||||
/// status is carried by the root) with a resolved status of
|
/// status is carried by the root) with a resolved status of
|
||||||
/// [`RepoStatus::Open`].
|
/// [`RepoStatus::Open`]. Cached on the last refresh.
|
||||||
pub fn pull_request_count(&self) -> usize {
|
pub fn pull_request_count(&self) -> usize {
|
||||||
self.pull_requests
|
self.open_pr_count
|
||||||
.iter()
|
|
||||||
.filter(|pr| pr.kind == Kind::GitPullRequest && self.status_of(pr) == RepoStatus::Open)
|
|
||||||
.count()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether `user` is the author (owner) of this repository: the public
|
/// Whether `user` is the author (owner) of this repository: the public
|
||||||
@@ -586,15 +617,12 @@ impl RepoStore {
|
|||||||
/// (kind 1617) carrying the `git format-patch` output, which the PR
|
/// (kind 1617) carrying the `git format-patch` output, which the PR
|
||||||
/// references via an `e` tag (NIP-34).
|
/// references via an `e` tag (NIP-34).
|
||||||
///
|
///
|
||||||
/// The patch is published first and the PR is sent once the patch
|
/// The patch is published first so the PR can reference its id. The
|
||||||
/// event's id is known, so the two always arrive together. The proposed
|
/// proposed commit is parsed from the patch's `From <commit>` header;
|
||||||
/// commit is parsed from the patch's `From <commit>` header; publishing
|
/// without one publishing is refused, because the PR's `c` tag must
|
||||||
/// without one is refused, because the PR's `c` tag (and the patch's
|
/// carry a real commit id for other NIP-34 clients to verify and apply
|
||||||
/// `commit`/`r` tags) must carry a real commit id for other NIP-34
|
/// the proposal. The `clone` tag carries the announced mirror URLs; the
|
||||||
/// clients to verify and apply the proposal. The PR's `clone` tag
|
/// linked patch is the source of truth until the commit is pushed there.
|
||||||
/// carries the repository's announced mirror URLs (the commit may not be
|
|
||||||
/// pushed there yet; the linked patch is the source of truth until a
|
|
||||||
/// push backend exists).
|
|
||||||
pub fn open_pull_request(
|
pub fn open_pull_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
subject: Option<String>,
|
subject: Option<String>,
|
||||||
@@ -783,10 +811,9 @@ impl RepoStore {
|
|||||||
/// the merged status.
|
/// the merged status.
|
||||||
///
|
///
|
||||||
/// Only the repository author may merge. The clone is created on demand
|
/// Only the repository author may merge. The clone is created on demand
|
||||||
/// from the announcement's clone URLs when the repository hasn't been
|
/// from the announcement's clone URLs when needed. Patch application
|
||||||
/// mirrored locally yet. Patch application runs on a background thread
|
/// (`git am`) runs on a background thread; failures (e.g. a patch that
|
||||||
/// (`git am`); failures (e.g. a patch that no longer applies) surface in
|
/// no longer applies) surface in [`Self::last_error`].
|
||||||
/// [`Self::last_error`] and no status is sent.
|
|
||||||
pub fn merge_pull_request(&mut self, root: &Event, cx: &mut Context<Self>) {
|
pub fn merge_pull_request(&mut self, root: &Event, cx: &mut Context<Self>) {
|
||||||
self.last_error = None;
|
self.last_error = None;
|
||||||
|
|
||||||
@@ -860,6 +887,49 @@ where
|
|||||||
events.into_iter().max_by_key(|e| e.created_at)
|
events.into_iter().max_by_key(|e| e.created_at)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Status of `root` from the precomputed map; roots without status events
|
||||||
|
/// default to [`RepoStatus::Open`], like [`signed_core::resolve_status`].
|
||||||
|
fn status_of(status_by_root: &HashMap<EventId, RepoStatus>, root: &Event) -> RepoStatus {
|
||||||
|
status_by_root
|
||||||
|
.get(&root.id)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(RepoStatus::Open)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the status of every root event in one pass: status events are
|
||||||
|
/// indexed by the root they reference (`e`/`E` tag), then each root
|
||||||
|
/// resolves against its own slice. O(roots + statuses) instead of the
|
||||||
|
/// O(roots × statuses) of resolving per root on demand.
|
||||||
|
fn resolve_statuses(
|
||||||
|
issues: &[Event],
|
||||||
|
patches: &[Event],
|
||||||
|
pull_requests: &[Event],
|
||||||
|
statuses: &[Event],
|
||||||
|
maintainers: &[PublicKey],
|
||||||
|
) -> HashMap<EventId, RepoStatus> {
|
||||||
|
let mut by_root: HashMap<EventId, Vec<&Event>> = HashMap::new();
|
||||||
|
for event in statuses {
|
||||||
|
for tag in event.tags.iter() {
|
||||||
|
if matches!(tag.kind(), "e" | "E")
|
||||||
|
&& let Some(id) = tag.content().and_then(|hex| EventId::from_hex(hex).ok())
|
||||||
|
{
|
||||||
|
by_root.entry(id).or_default().push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
issues
|
||||||
|
.iter()
|
||||||
|
.chain(patches)
|
||||||
|
.chain(pull_requests)
|
||||||
|
.map(|root| {
|
||||||
|
let events = by_root.get(&root.id).map(Vec::as_slice).unwrap_or(&[]);
|
||||||
|
let status = signed_core::resolve_status(events.iter().copied(), &root.pubkey, maintainers);
|
||||||
|
(root.id, status)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn sort_newest_first(events: &mut [Event]) {
|
fn sort_newest_first(events: &mut [Event]) {
|
||||||
events.sort_by_key(|e| std::cmp::Reverse(e.created_at));
|
events.sort_by_key(|e| std::cmp::Reverse(e.created_at));
|
||||||
}
|
}
|
||||||
@@ -876,12 +946,10 @@ fn patch_current_commit(patch: &str) -> Option<&str> {
|
|||||||
hex.split_whitespace().next().filter(|hex| hex.len() == 40)
|
hex.split_whitespace().next().filter(|hex| hex.len() == 40)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a NIP-22 kind-1111 comment using the SDK's [`CommentBuilder`]:
|
/// Build a NIP-22 kind-1111 comment: uppercase `E`/`K`/`P` tags scope the
|
||||||
/// uppercase `E`/`K`/`P` tags scope the thread root, lowercase `e`/`k`/`p`
|
/// thread root, lowercase `e`/`k`/`p` the direct parent (or the root for a
|
||||||
/// tags the direct parent (`parent`, or the root itself for a top-level
|
/// top-level comment). An `a` tag with the repository coordinate (not part
|
||||||
/// comment). An `a` tag with the repository coordinate is added so Signed's
|
/// of NIP-22) is added so Signed's own activity subscriptions also match.
|
||||||
/// own activity subscriptions also match the comment (it is not part of
|
|
||||||
/// NIP-22).
|
|
||||||
fn comment_builder(
|
fn comment_builder(
|
||||||
root: &Event,
|
root: &Event,
|
||||||
parent: Option<&Event>,
|
parent: Option<&Event>,
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ impl Global for GlobalRepoListStore {}
|
|||||||
|
|
||||||
/// Counts of NIP-34 activity events per repository, used to rank the
|
/// Counts of NIP-34 activity events per repository, used to rank the
|
||||||
/// explore list by popularity. Each patch event is a pushed commit (or a
|
/// explore list by popularity. Each patch event is a pushed commit (or a
|
||||||
/// small commit series), which is the closest cross-repository proxy for
|
/// small series), the closest proxy for commit count in the event data.
|
||||||
/// commit count available from event data alone.
|
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
pub struct RepoActivityCounts {
|
pub struct RepoActivityCounts {
|
||||||
/// Root `30611` issue events addressed to the repository.
|
/// Root `30611` issue events addressed to the repository.
|
||||||
|
|||||||
@@ -39,12 +39,9 @@ pub(super) enum FileContent {
|
|||||||
|
|
||||||
/// A markdown document loaded into a persistent [`TextViewState`].
|
/// A markdown document loaded into a persistent [`TextViewState`].
|
||||||
///
|
///
|
||||||
/// The state is owned by the view rather than created per render (as the
|
/// The state is owned by the view rather than created per render: GPUI
|
||||||
/// stateless `text::markdown` helper does), so it survives branch switches
|
/// drops keyed element state after one absent frame, which would re-parse
|
||||||
/// in the content pane. GPUI's keyed element state is dropped as soon as the
|
/// the whole document on every pane switch (README / file / spinner).
|
||||||
/// element is absent for a single frame, which would otherwise re-parse the
|
|
||||||
/// whole document on the main thread every time the pane switches between
|
|
||||||
/// the README, a file preview, and the loading spinner.
|
|
||||||
pub(super) struct MarkdownView {
|
pub(super) struct MarkdownView {
|
||||||
/// Source path; `None` means the repository README.
|
/// Source path; `None` means the repository README.
|
||||||
pub(super) path: Option<SharedString>,
|
pub(super) path: Option<SharedString>,
|
||||||
@@ -53,11 +50,7 @@ pub(super) struct MarkdownView {
|
|||||||
|
|
||||||
/// A code file loaded into a persistent [`InputState`], rendered as a
|
/// A code file loaded into a persistent [`InputState`], rendered as a
|
||||||
/// disabled (read-only) code editor with syntax highlighting, line numbers
|
/// disabled (read-only) code editor with syntax highlighting, line numbers
|
||||||
/// and search.
|
/// and search. Persistent for the same reason as [`MarkdownView`].
|
||||||
///
|
|
||||||
/// Same persistence rationale as [`MarkdownView`]: the state lives as long
|
|
||||||
/// as this view, so re-viewing the same file does not re-parse it, and
|
|
||||||
/// parsing happens on a background task inside the editor.
|
|
||||||
pub(super) struct CodeView {
|
pub(super) struct CodeView {
|
||||||
/// Source path, relative to the worktree root.
|
/// Source path, relative to the worktree root.
|
||||||
pub(super) path: SharedString,
|
pub(super) path: SharedString,
|
||||||
@@ -230,9 +223,7 @@ impl RepoDetailView {
|
|||||||
/// Load `text` into the persistent markdown TextView state.
|
/// Load `text` into the persistent markdown TextView state.
|
||||||
///
|
///
|
||||||
/// The state is created empty and fed via `push_str`, which parses on a
|
/// The state is created empty and fed via `push_str`, which parses on a
|
||||||
/// background task: switching files never blocks the main thread, and
|
/// background task, so switching files never blocks the main thread.
|
||||||
/// the state lives as long as this view, so re-viewing the same document
|
|
||||||
/// does not re-parse it.
|
|
||||||
pub(super) fn set_markdown(
|
pub(super) fn set_markdown(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: Option<SharedString>,
|
path: Option<SharedString>,
|
||||||
@@ -269,10 +260,8 @@ impl RepoDetailView {
|
|||||||
/// Load `text` into the persistent code editor state for `path`.
|
/// Load `text` into the persistent code editor state for `path`.
|
||||||
///
|
///
|
||||||
/// The state is created in code editor mode so the Input renders it as
|
/// The state is created in code editor mode so the Input renders it as
|
||||||
/// a syntax-highlighted, read-only editor. Like [`set_markdown`], the
|
/// a syntax-highlighted, read-only editor; the tree-sitter parse runs
|
||||||
/// state lives as long as this view, so re-viewing the same file does
|
/// on a background task like [`set_markdown`]'s.
|
||||||
/// not re-parse it; the tree-sitter parse runs on a background task
|
|
||||||
/// inside the editor instead of blocking the main thread.
|
|
||||||
pub(super) fn set_code(
|
pub(super) fn set_code(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: SharedString,
|
path: SharedString,
|
||||||
|
|||||||
@@ -276,11 +276,8 @@ pub(super) fn status_badge(status: RepoStatus, cx: &App) -> AnyElement {
|
|||||||
/// A split dropdown button built on `gpui_base::Popover`: an action element
|
/// A split dropdown button built on `gpui_base::Popover`: an action element
|
||||||
/// with a separate caret trigger that opens a [`PopupMenu`].
|
/// with a separate caret trigger that opens a [`PopupMenu`].
|
||||||
///
|
///
|
||||||
/// The action and the caret are ordinary elements supplied by the caller, so
|
/// The action and the caret are caller-supplied elements, so the look stays
|
||||||
/// the look — icons, borders, hover states, sizes — stays fully in the
|
/// in the application; this component only owns the popover wiring.
|
||||||
/// application. The component only owns the popover wiring: opening on caret
|
|
||||||
/// click, Escape/outside dismissal, focus movement into the menu, and the
|
|
||||||
/// menu entity's lifecycle.
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub(super) struct BaseDropdownButton {
|
pub(super) struct BaseDropdownButton {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
@@ -493,11 +490,10 @@ impl ShareTargets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One row of the share menu: a small title on top of the compact label,
|
/// One row of the share menu: a small title above the compact label, with
|
||||||
/// with a copy button that flips to a check while the value is on the
|
/// a copy button that flips to a check while the value is on the clipboard.
|
||||||
/// clipboard. Clicking the row text copies and dismisses the menu; the copy
|
/// Clicking the row copies and dismisses the menu; the copy button stops
|
||||||
/// button stops propagation, so the menu stays open for further copies.
|
/// propagation so the menu stays open. Both copy `copy`, never the label.
|
||||||
/// Both copy `copy`, never the truncated label.
|
|
||||||
pub(super) fn share_menu_row(
|
pub(super) fn share_menu_row(
|
||||||
id: &'static str,
|
id: &'static str,
|
||||||
title: &'static str,
|
title: &'static str,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use assets::CustomIconName;
|
use assets::CustomIconName;
|
||||||
use dock::{BasePanel, Panel, PanelEvent};
|
use dock::{BasePanel, Panel, PanelEvent};
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
@@ -27,6 +29,10 @@ pub struct IssueDetailView {
|
|||||||
issue_id: EventId,
|
issue_id: EventId,
|
||||||
/// Input state of the "leave a comment" textarea.
|
/// Input state of the "leave a comment" textarea.
|
||||||
comment_input: Entity<TextareaState>,
|
comment_input: Entity<TextareaState>,
|
||||||
|
/// Issue/comment bodies as shared strings, keyed by event ID, so
|
||||||
|
/// re-renders don't clone full contents again (events are immutable,
|
||||||
|
/// so the cache never needs invalidation).
|
||||||
|
contents: HashMap<EventId, SharedString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IssueDetailView {
|
impl IssueDetailView {
|
||||||
@@ -44,6 +50,7 @@ impl IssueDetailView {
|
|||||||
store,
|
store,
|
||||||
issue_id,
|
issue_id,
|
||||||
comment_input,
|
comment_input,
|
||||||
|
contents: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +149,13 @@ impl IssueDetailView {
|
|||||||
let author = profile.name();
|
let author = profile.name();
|
||||||
let picture = profile.picture();
|
let picture = profile.picture();
|
||||||
let age = relative_time(comment.created_at);
|
let age = relative_time(comment.created_at);
|
||||||
|
// Comment bodies are cloned into shared strings once per
|
||||||
|
// comment, not on every render.
|
||||||
|
let content = self
|
||||||
|
.contents
|
||||||
|
.entry(comment.id)
|
||||||
|
.or_insert_with(|| SharedString::from(comment.content.clone()))
|
||||||
|
.clone();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
@@ -176,11 +190,7 @@ impl IssueDetailView {
|
|||||||
.child(SharedString::from(age)),
|
.child(SharedString::from(age)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(div().text_sm().child(content))
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.child(SharedString::from(comment.content.clone())),
|
|
||||||
)
|
|
||||||
}))
|
}))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
@@ -284,6 +294,11 @@ impl Render for IssueDetailView {
|
|||||||
let (title, author, picture, status, age, issue_id, content) = {
|
let (title, author, picture, status, age, issue_id, content) = {
|
||||||
let profile_store = ProfileStore::global(cx);
|
let profile_store = ProfileStore::global(cx);
|
||||||
let profile = profile_store.read(cx).get(&issue.pubkey);
|
let profile = profile_store.read(cx).get(&issue.pubkey);
|
||||||
|
let content = self
|
||||||
|
.contents
|
||||||
|
.entry(issue.id)
|
||||||
|
.or_insert_with(|| SharedString::from(issue.content.clone()))
|
||||||
|
.clone();
|
||||||
|
|
||||||
(
|
(
|
||||||
activity_subject(issue),
|
activity_subject(issue),
|
||||||
@@ -292,7 +307,7 @@ impl Render for IssueDetailView {
|
|||||||
store.status_of(issue),
|
store.status_of(issue),
|
||||||
relative_time(issue.created_at),
|
relative_time(issue.created_at),
|
||||||
issue.id,
|
issue.id,
|
||||||
issue.content.clone(),
|
content,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -358,7 +373,7 @@ impl Render for IssueDetailView {
|
|||||||
.child(SharedString::from(age)),
|
.child(SharedString::from(age)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(div().text_sm().child(SharedString::from(&content))),
|
.child(div().text_sm().child(content)),
|
||||||
)
|
)
|
||||||
.child(self.render_comments(&issue_id, cx))
|
.child(self.render_comments(&issue_id, cx))
|
||||||
.child(self.render_form(&issue_id, cx)),
|
.child(self.render_form(&issue_id, cx)),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use gpui_component::scroll::Scrollbar;
|
|||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
ActiveTheme, Icon, Sizable, VirtualListScrollHandle, WindowExt, h_flex, v_flex, v_virtual_list,
|
ActiveTheme, Icon, Sizable, VirtualListScrollHandle, WindowExt, h_flex, v_flex, v_virtual_list,
|
||||||
};
|
};
|
||||||
use nostr::prelude::{Event, EventId};
|
use nostr::prelude::EventId;
|
||||||
use signed_core::{RepoStatus, activity_subject};
|
use signed_core::{RepoStatus, activity_subject};
|
||||||
use signed_state::{ProfileStore, RepoStore};
|
use signed_state::{ProfileStore, RepoStore};
|
||||||
use utils::relative_time;
|
use utils::relative_time;
|
||||||
@@ -26,10 +26,8 @@ use super::helpers::{placeholder, status_badge};
|
|||||||
use super::issue_detail::IssueDetailView;
|
use super::issue_detail::IssueDetailView;
|
||||||
use crate::image_cache::{MAX_IMAGES, image_cache};
|
use crate::image_cache::{MAX_IMAGES, image_cache};
|
||||||
|
|
||||||
/// Height of one issue row in the virtual list: 8px vertical padding
|
/// Height of one issue row in the virtual list: `py_2` padding, a 32px
|
||||||
/// (`py_2`) on top and bottom, a 32px title line (`h_8`) and a 24px meta
|
/// title line (`h_8`), a 24px meta line (`h_6`) and the 1px bottom border.
|
||||||
/// line (`h_6`), plus the 1px bottom border; the row totals 73px. The
|
|
||||||
/// status chip (`size_7`, 28px) is shorter than the content.
|
|
||||||
const ISSUE_ROW_HEIGHT: f32 = 73.;
|
const ISSUE_ROW_HEIGHT: f32 = 73.;
|
||||||
|
|
||||||
/// Status filter of the issues list, chosen via the header's filter buttons.
|
/// Status filter of the issues list, chosen via the header's filter buttons.
|
||||||
@@ -39,21 +37,18 @@ enum IssueFilter {
|
|||||||
All,
|
All,
|
||||||
/// Issues whose resolved status is [`RepoStatus::Open`].
|
/// Issues whose resolved status is [`RepoStatus::Open`].
|
||||||
Open,
|
Open,
|
||||||
/// Issues whose resolved status is
|
/// Issues whose resolved status is [`RepoStatus::Closed`] or
|
||||||
/// [`RepoStatus::Closed`] or [`RepoStatus::Applied`] (both are "done" states).
|
/// [`RepoStatus::Applied`] (both are "done" states).
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IssueFilter {
|
impl IssueFilter {
|
||||||
/// Whether `issue` (of `store`) is included by this filter.
|
/// Whether an issue with `status` is included by this filter.
|
||||||
fn matches(self, store: &RepoStore, issue: &Event) -> bool {
|
fn matches(self, status: RepoStatus) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::All => true,
|
Self::All => true,
|
||||||
Self::Open => store.status_of(issue) == RepoStatus::Open,
|
Self::Open => status == RepoStatus::Open,
|
||||||
Self::Closed => matches!(
|
Self::Closed => matches!(status, RepoStatus::Closed | RepoStatus::Applied),
|
||||||
store.status_of(issue),
|
|
||||||
RepoStatus::Closed | RepoStatus::Applied
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,9 +67,15 @@ pub struct IssuesView {
|
|||||||
item_sizes: Rc<Vec<Size<Pixels>>>,
|
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||||
/// Number of rows [`Self::item_sizes`] was built for (the filtered issue count).
|
/// Number of rows [`Self::item_sizes`] was built for (the filtered issue count).
|
||||||
issue_len: usize,
|
issue_len: usize,
|
||||||
/// Indices into the store's `issues` matching [`Self::filter`], rebuilt
|
/// Indices into the store's `issues` matching [`Self::filter`]; the
|
||||||
/// every render; the virtual list renders this slice.
|
/// virtual list renders this slice. Rebuilt only when the store
|
||||||
|
/// version or the filter changes, keyed by [`Self::cache_key`].
|
||||||
visible_issues: Vec<usize>,
|
visible_issues: Vec<usize>,
|
||||||
|
/// Header counts `(total, open, closed)`, rebuilt with
|
||||||
|
/// [`Self::visible_issues`].
|
||||||
|
counts: (usize, usize, usize),
|
||||||
|
/// Store version and filter the cached rows/counts were built from.
|
||||||
|
cache_key: Option<(u64, IssueFilter)>,
|
||||||
/// Virtual list state of the issues list.
|
/// Virtual list state of the issues list.
|
||||||
scroll_handle: VirtualListScrollHandle,
|
scroll_handle: VirtualListScrollHandle,
|
||||||
}
|
}
|
||||||
@@ -96,6 +97,8 @@ impl IssuesView {
|
|||||||
item_sizes: Rc::new(Vec::new()),
|
item_sizes: Rc::new(Vec::new()),
|
||||||
issue_len: 0,
|
issue_len: 0,
|
||||||
visible_issues: Vec::new(),
|
visible_issues: Vec::new(),
|
||||||
|
counts: (0, 0, 0),
|
||||||
|
cache_key: None,
|
||||||
scroll_handle: VirtualListScrollHandle::new(),
|
scroll_handle: VirtualListScrollHandle::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,19 +191,9 @@ impl IssuesView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let store = self.store.read(cx);
|
// Counts of the last list rebuild (`render` rebuilds first when the
|
||||||
let (total, open, closed) =
|
// store version or filter changed, so this is never stale).
|
||||||
store
|
let (total, open, closed) = self.counts;
|
||||||
.issues
|
|
||||||
.iter()
|
|
||||||
.fold(
|
|
||||||
(0usize, 0usize, 0usize),
|
|
||||||
|(total, open, closed), issue| match store.status_of(issue) {
|
|
||||||
RepoStatus::Open => (total + 1, open + 1, closed),
|
|
||||||
RepoStatus::Closed => (total + 1, open, closed + 1),
|
|
||||||
RepoStatus::Draft | RepoStatus::Applied => (total + 1, open, closed),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.px_4()
|
.px_4()
|
||||||
@@ -433,18 +426,30 @@ impl Render for IssuesView {
|
|||||||
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 filter = self.filter;
|
let filter = self.filter;
|
||||||
|
|
||||||
// Indices of the issues matching the active filter; the virtual
|
// Rebuild the filtered rows and header counts only when the store
|
||||||
// list renders this filtered slice.
|
// refreshed or the filter changed; other renders reuse the cache.
|
||||||
self.visible_issues = {
|
let version = self.store.read(cx).version();
|
||||||
|
if self.cache_key != Some((version, filter)) {
|
||||||
let store = self.store.read(cx);
|
let store = self.store.read(cx);
|
||||||
store
|
let mut counts = (0usize, 0usize, 0usize);
|
||||||
|
self.visible_issues = store
|
||||||
.issues
|
.issues
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(_, issue)| filter.matches(store, issue))
|
.filter_map(|(ix, issue)| {
|
||||||
.map(|(ix, _)| ix)
|
let status = store.status_of(issue);
|
||||||
.collect()
|
counts.0 += 1;
|
||||||
};
|
match status {
|
||||||
|
RepoStatus::Open => counts.1 += 1,
|
||||||
|
RepoStatus::Closed => counts.2 += 1,
|
||||||
|
RepoStatus::Draft | RepoStatus::Applied => {}
|
||||||
|
}
|
||||||
|
filter.matches(status).then_some(ix)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
self.counts = counts;
|
||||||
|
self.cache_key = Some((version, filter));
|
||||||
|
}
|
||||||
|
|
||||||
let count = self.visible_issues.len();
|
let count = self.visible_issues.len();
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ use gpui_component::{
|
|||||||
ActiveTheme, Colorize, Icon, IconName, Sizable, StyledExt, ThemeStyled,
|
ActiveTheme, Colorize, Icon, IconName, Sizable, StyledExt, ThemeStyled,
|
||||||
VirtualListScrollHandle, h_flex, v_flex,
|
VirtualListScrollHandle, h_flex, v_flex,
|
||||||
};
|
};
|
||||||
use nostr::prelude::{RelayUrl, ToBech32};
|
use nostr::prelude::{EventId, RelayUrl, ToBech32};
|
||||||
use signed_core::Announcement;
|
use signed_core::Announcement;
|
||||||
use signed_git::{CommitList, FileCommit};
|
use signed_git::{CommitList, FileCommit};
|
||||||
use signed_state::{Backend, GitStore, LocalReposStore, ProfileStore, RepoStore};
|
use signed_state::{Backend, GitStore, LocalReposStore, ProfileStore, RepoStore};
|
||||||
@@ -98,6 +98,20 @@ struct RepoData {
|
|||||||
head_commit: Option<FileCommit>,
|
head_commit: Option<FileCommit>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Derived NIP-34 header data, cached so renders don't re-encode bech32
|
||||||
|
/// share targets and rebuild clone command strings on every frame.
|
||||||
|
struct HeaderCache {
|
||||||
|
/// Announcement event ID and owner NIP-05 this cache was built from;
|
||||||
|
/// rebuilt when either changes (a new announcement version, or the
|
||||||
|
/// owner's profile arriving with a NIP-05 identifier).
|
||||||
|
key: (EventId, Option<String>),
|
||||||
|
announcement: Rc<Announcement>,
|
||||||
|
share: Rc<ShareTargets>,
|
||||||
|
ngit_command: SharedString,
|
||||||
|
nak_command: SharedString,
|
||||||
|
git_commands: Rc<Vec<SharedString>>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Detail view of a repository: header, stats, a file explorer with README
|
/// Detail view of a repository: header, stats, a file explorer with README
|
||||||
/// preview (cloned from the announcement's `clone` URLs), and metadata.
|
/// preview (cloned from the announcement's `clone` URLs), and metadata.
|
||||||
pub struct RepoDetailView {
|
pub struct RepoDetailView {
|
||||||
@@ -170,6 +184,10 @@ pub struct RepoDetailView {
|
|||||||
/// Bumped on every branch/tag switch; in-flight loads tagged with an
|
/// Bumped on every branch/tag switch; in-flight loads tagged with an
|
||||||
/// older generation are discarded when they complete.
|
/// older generation are discarded when they complete.
|
||||||
ref_generation: u64,
|
ref_generation: u64,
|
||||||
|
/// Derived NIP-34 header data (share targets, clone commands),
|
||||||
|
/// rebuilt only when the announcement or the owner's NIP-05 changes
|
||||||
|
/// instead of on every render.
|
||||||
|
header_cache: Option<HeaderCache>,
|
||||||
/// In-flight tasks; finished tasks are pruned on every push, so the vec
|
/// In-flight tasks; finished tasks are pruned on every push, so the vec
|
||||||
/// stays bounded by the number of concurrent loads.
|
/// stays bounded by the number of concurrent loads.
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
@@ -298,6 +316,7 @@ impl RepoDetailView {
|
|||||||
tag_select,
|
tag_select,
|
||||||
switching_ref: false,
|
switching_ref: false,
|
||||||
ref_generation: 0,
|
ref_generation: 0,
|
||||||
|
header_cache: None,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
tasks: Vec::new(),
|
tasks: Vec::new(),
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
@@ -308,9 +327,7 @@ impl RepoDetailView {
|
|||||||
/// (not yet published) repository is opened straight from disk. An
|
/// (not yet published) repository is opened straight from disk. An
|
||||||
/// announced repository's local clone (if any) is loaded first without
|
/// announced repository's local clone (if any) is loaded first without
|
||||||
/// touching the network, so an unreachable server can't block the
|
/// touching the network, so an unreachable server can't block the
|
||||||
/// panel; a background fetch then refreshes the refs and commit list
|
/// panel; a background fetch then refreshes the refs and commit list.
|
||||||
/// (a fetch never changes the checked-out files, so the tree and
|
|
||||||
/// previews are left alone).
|
|
||||||
fn load_repo(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn load_repo(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.loading = true;
|
self.loading = true;
|
||||||
self.error = None;
|
self.error = None;
|
||||||
@@ -427,9 +444,9 @@ impl RepoDetailView {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Ok(Some((branches, tags, current_branch, head_commit))) = refresh {
|
if let Ok(Some((branches, tags, current_branch, head_commit))) = refresh {
|
||||||
let branches: Vec<SharedString> =
|
let branches: Vec<SharedString> = branches.iter().map(Into::into).collect();
|
||||||
branches.into_iter().map(Into::into).collect();
|
let tags: Vec<SharedString> = tags.iter().map(Into::into).collect();
|
||||||
let tags: Vec<SharedString> = tags.into_iter().map(Into::into).collect();
|
|
||||||
this.branch_select.update(cx, |state, cx| {
|
this.branch_select.update(cx, |state, cx| {
|
||||||
state.set_items(SearchableVec::from(branches), window, cx);
|
state.set_items(SearchableVec::from(branches), window, cx);
|
||||||
if let Some(branch) = current_branch {
|
if let Some(branch) = current_branch {
|
||||||
@@ -437,14 +454,22 @@ impl RepoDetailView {
|
|||||||
state.set_selected_values(&[branch], window, cx);
|
state.set_selected_values(&[branch], window, cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.tag_select.update(cx, |state, cx| {
|
this.tag_select.update(cx, |state, cx| {
|
||||||
state.set_items(SearchableVec::from(tags), window, cx);
|
state.set_items(SearchableVec::from(tags), window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let new_head_commit = head_commit.as_ref().map(|c| &c.id);
|
||||||
|
let current_head_commit = this.head_commit.as_ref().map(|c| &c.id);
|
||||||
|
let head_changed = new_head_commit != current_head_commit;
|
||||||
this.head_commit = head_commit;
|
this.head_commit = head_commit;
|
||||||
// The fetch may have brought new commits: reload the list.
|
|
||||||
|
if head_changed || this.all_commits.is_none() {
|
||||||
this.all_commits = None;
|
this.all_commits = None;
|
||||||
this.loading_all_commits = false;
|
this.loading_all_commits = false;
|
||||||
this.load_all_commits(cx);
|
this.load_all_commits(cx);
|
||||||
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
@@ -455,8 +480,8 @@ impl RepoDetailView {
|
|||||||
self.tasks.push(task);
|
self.tasks.push(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply the loaded repository data: explorer tree, README preview, ref
|
/// Apply the loaded repository data: explorer tree, README preview,
|
||||||
/// selectors and HEAD commit, then start the commit-list walk.
|
/// ref selectors and HEAD commit, then start the commit-list walk.
|
||||||
fn apply_repo_data(&mut self, data: RepoData, window: &mut Window, cx: &mut Context<Self>) {
|
fn apply_repo_data(&mut self, data: RepoData, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let RepoData {
|
let RepoData {
|
||||||
tree,
|
tree,
|
||||||
@@ -479,10 +504,11 @@ impl RepoDetailView {
|
|||||||
state.set_items(tree_items(tree, false), cx);
|
state.set_items(tree_items(tree, false), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Populate the branch/tag selectors with the local refs, selecting
|
// Populate the branch/tag selectors with the local refs,
|
||||||
// the branch HEAD points to.
|
// selecting the branch HEAD points to.
|
||||||
let branches: Vec<SharedString> = branches.into_iter().map(Into::into).collect();
|
let branches: Vec<SharedString> = branches.into_iter().map(Into::into).collect();
|
||||||
let tags: Vec<SharedString> = tags.into_iter().map(Into::into).collect();
|
let tags: Vec<SharedString> = tags.into_iter().map(Into::into).collect();
|
||||||
|
|
||||||
self.branch_select.update(cx, |state, cx| {
|
self.branch_select.update(cx, |state, cx| {
|
||||||
state.set_items(SearchableVec::from(branches), window, cx);
|
state.set_items(SearchableVec::from(branches), window, cx);
|
||||||
if let Some(branch) = current_branch {
|
if let Some(branch) = current_branch {
|
||||||
@@ -490,11 +516,13 @@ impl RepoDetailView {
|
|||||||
state.set_selected_values(&[branch], window, cx);
|
state.set_selected_values(&[branch], window, cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.tag_select.update(cx, |state, cx| {
|
self.tag_select.update(cx, |state, cx| {
|
||||||
state.set_items(SearchableVec::from(tags), window, cx);
|
state.set_items(SearchableVec::from(tags), window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.load_all_commits(cx);
|
self.load_all_commits(cx);
|
||||||
|
|
||||||
if let Some((path, bytes)) = readme_path.zip(readme) {
|
if let Some((path, bytes)) = readme_path.zip(readme) {
|
||||||
self.readme_name = Some(path.to_string_lossy().into());
|
self.readme_name = Some(path.to_string_lossy().into());
|
||||||
self.load_commit(&path.to_string_lossy(), cx);
|
self.load_commit(&path.to_string_lossy(), cx);
|
||||||
@@ -504,10 +532,8 @@ impl RepoDetailView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clone the repository into a folder chosen by the user (outside the
|
/// Clone the repository into a folder chosen by the user (outside the cache),
|
||||||
/// cache), then open the new clone in the system file manager. Like
|
/// then open the new clone in the system file manager.
|
||||||
/// ngit's clone, this resolves the announcement's `clone` URLs and
|
|
||||||
/// clones from the first working git server.
|
|
||||||
fn clone_to_folder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn clone_to_folder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if self.cloning {
|
if self.cloning {
|
||||||
return;
|
return;
|
||||||
@@ -722,11 +748,8 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
/// Walk history once for every queued path on a background task, and
|
/// Walk history once for every queued path on a background task, and
|
||||||
/// cache the latest commit touching each of them in [`Self::commits`]
|
/// cache the latest commit touching each of them in [`Self::commits`]
|
||||||
/// (for the file header in the content column).
|
/// (for the file header in the content column). Batching shares one
|
||||||
///
|
/// walk across all paths queued while the previous walk was in flight.
|
||||||
/// Batching shares one walk (and its object decodes) across all paths
|
|
||||||
/// queued while the previous walk was in flight, instead of walking the
|
|
||||||
/// full history per file.
|
|
||||||
fn load_commits(&mut self, cx: &mut Context<Self>) {
|
fn load_commits(&mut self, cx: &mut Context<Self>) {
|
||||||
if self.pending_commits.is_empty() || self.loading_commits {
|
if self.pending_commits.is_empty() || self.loading_commits {
|
||||||
return;
|
return;
|
||||||
@@ -1029,8 +1052,7 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
/// Trigger body for the branch/tag selectors: the kind icon, the
|
/// Trigger body for the branch/tag selectors: the kind icon, the
|
||||||
/// selection (or placeholder) and the caret. `Combobox` replaces its
|
/// selection (or placeholder) and the caret. `Combobox` replaces its
|
||||||
/// default trigger entirely, which is the only way to show an icon
|
/// default trigger entirely, the only way to show an icon inside it.
|
||||||
/// inside the trigger label.
|
|
||||||
fn render_ref_trigger(
|
fn render_ref_trigger(
|
||||||
ctx: &ComboboxTriggerContext<SearchableVec<SharedString>>,
|
ctx: &ComboboxTriggerContext<SearchableVec<SharedString>>,
|
||||||
icon: CustomIconName,
|
icon: CustomIconName,
|
||||||
@@ -1201,7 +1223,7 @@ impl RepoDetailView {
|
|||||||
/// The NIP-34 header (actions, issues/PR counts) or, for a local
|
/// The NIP-34 header (actions, issues/PR counts) or, for a local
|
||||||
/// repository that hasn't been published yet, the local header with an
|
/// repository that hasn't been published yet, the local header with an
|
||||||
/// Init button.
|
/// Init button.
|
||||||
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_header(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
if self.local_path.is_some() {
|
if self.local_path.is_some() {
|
||||||
return self.render_local_header(cx);
|
return self.render_local_header(cx);
|
||||||
}
|
}
|
||||||
@@ -1210,26 +1232,53 @@ impl RepoDetailView {
|
|||||||
return div().into_any_element();
|
return div().into_any_element();
|
||||||
};
|
};
|
||||||
let store = store_entity.read(cx);
|
let store = store_entity.read(cx);
|
||||||
let Some(announcement) = store
|
|
||||||
.announcement
|
|
||||||
.as_ref()
|
|
||||||
.or(self.initial.as_ref())
|
|
||||||
.cloned()
|
|
||||||
else {
|
|
||||||
return div().into_any_element();
|
|
||||||
};
|
|
||||||
let issue_count = SharedString::from(store.issue_count().to_string());
|
let issue_count = SharedString::from(store.issue_count().to_string());
|
||||||
let pr_count = SharedString::from(store.pull_request_count().to_string());
|
let pr_count = SharedString::from(store.pull_request_count().to_string());
|
||||||
|
|
||||||
|
let Some(source) = store.announcement.as_ref().or(self.initial.as_ref()) else {
|
||||||
|
return div().into_any_element();
|
||||||
|
};
|
||||||
|
|
||||||
|
// The header derives bech32 share targets and clone command strings
|
||||||
|
// from the announcement; rebuild them only when the announcement or
|
||||||
|
// the owner's NIP-05 changes, not on every render.
|
||||||
|
let nip05 = ProfileStore::global(cx)
|
||||||
|
.read(cx)
|
||||||
|
.get(&source.owner)
|
||||||
|
.metadata()
|
||||||
|
.nip05
|
||||||
|
.clone()
|
||||||
|
.filter(|nip05| !nip05.trim().is_empty());
|
||||||
|
let key = (source.event_id, nip05);
|
||||||
|
|
||||||
|
if self
|
||||||
|
.header_cache
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|cache| cache.key != key)
|
||||||
|
{
|
||||||
|
let announcement = source.clone();
|
||||||
|
let share = ShareTargets::from_announcement(&announcement);
|
||||||
|
let nostr_url = nostr_clone_url(&announcement, key.1.as_deref());
|
||||||
|
self.header_cache = Some(HeaderCache {
|
||||||
|
ngit_command: SharedString::from(format!("git clone {nostr_url}")),
|
||||||
|
nak_command: SharedString::from(format!("nak git clone {nostr_url}")),
|
||||||
|
git_commands: Rc::new(announcement.clone_urls()),
|
||||||
|
share: Rc::new(share),
|
||||||
|
announcement: Rc::new(announcement),
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let cache = self.header_cache.as_ref().expect("cache just built");
|
||||||
|
let announcement = cache.announcement.clone();
|
||||||
|
let share = cache.share.clone();
|
||||||
|
let ngit_command = cache.ngit_command.clone();
|
||||||
|
let nak_command = cache.nak_command.clone();
|
||||||
|
let git_commands = cache.git_commands.clone();
|
||||||
|
|
||||||
let name = self.display_name(cx);
|
let name = self.display_name(cx);
|
||||||
let description = announcement.description();
|
let description = announcement.description();
|
||||||
let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id));
|
let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id));
|
||||||
let share = ShareTargets::from_announcement(&announcement);
|
|
||||||
|
|
||||||
let nostr_url = nostr_clone_url(&announcement, cx);
|
|
||||||
let ngit_command = SharedString::from(format!("git clone {nostr_url}"));
|
|
||||||
let nak_command = SharedString::from(format!("nak git clone {nostr_url}"));
|
|
||||||
let git_commands = announcement.clone_urls();
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.on_action(
|
.on_action(
|
||||||
@@ -1923,9 +1972,7 @@ impl Render for RepoDetailView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Read the worktree state of `repo` (no network): entries, README, refs
|
/// Read the worktree state of `repo` (no network): entries, README, refs
|
||||||
/// and HEAD commit. The tree is built off the main thread; the seeds are
|
/// and HEAD commit.
|
||||||
/// plain owned strings and convert to `TreeItem`s (which hold `Rc` state)
|
|
||||||
/// on the main thread.
|
|
||||||
fn load_repo_data(repo: &Repository) -> Result<RepoData, Error> {
|
fn load_repo_data(repo: &Repository) -> Result<RepoData, Error> {
|
||||||
let entries = signed_git::worktree_entries(repo)?;
|
let entries = signed_git::worktree_entries(repo)?;
|
||||||
let tree = build_tree_items(&entries);
|
let tree = build_tree_items(&entries);
|
||||||
@@ -1961,16 +2008,11 @@ fn load_repo_data(repo: &Repository) -> Result<RepoData, Error> {
|
|||||||
|
|
||||||
/// The `nostr://...` clone URL of an announcement (NIP-34): the owner as a
|
/// The `nostr://...` clone URL of an announcement (NIP-34): the owner as a
|
||||||
/// NIP-05 identifier when known (npub otherwise), the first announced relay
|
/// NIP-05 identifier when known (npub otherwise), the first announced relay
|
||||||
/// as a hint, and the repository identifier.
|
/// as a hint, and the repository identifier. `nip05` is the owner's
|
||||||
fn nostr_clone_url(announcement: &Announcement, cx: &App) -> SharedString {
|
/// NIP-05 identifier from the profile store, already blank-filtered.
|
||||||
|
fn nostr_clone_url(announcement: &Announcement, nip05: Option<&str>) -> SharedString {
|
||||||
let owner = announcement.owner;
|
let owner = announcement.owner;
|
||||||
let user = ProfileStore::global(cx)
|
let user = nip05
|
||||||
.read(cx)
|
|
||||||
.get(&owner)
|
|
||||||
.metadata()
|
|
||||||
.nip05
|
|
||||||
.as_deref()
|
|
||||||
.filter(|nip05| !nip05.trim().is_empty())
|
|
||||||
.map(str::to_owned)
|
.map(str::to_owned)
|
||||||
.unwrap_or_else(|| owner.to_bech32().unwrap_or_else(|_| owner.to_hex()));
|
.unwrap_or_else(|| owner.to_bech32().unwrap_or_else(|_| owner.to_hex()));
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
@@ -38,6 +39,10 @@ use crate::image_cache::{MAX_IMAGES, image_cache};
|
|||||||
/// Width of the changed-files column.
|
/// Width of the changed-files column.
|
||||||
const TREE_WIDTH: f32 = 260.;
|
const TREE_WIDTH: f32 = 260.;
|
||||||
|
|
||||||
|
/// Height of one commit row in the commits tab's virtual list: a single
|
||||||
|
/// text line plus the 1px bottom border.
|
||||||
|
const PR_COMMIT_ROW_HEIGHT: f32 = 37.;
|
||||||
|
|
||||||
/// Detail panel of a single pull request.
|
/// Detail panel of a single pull request.
|
||||||
pub struct PullRequestDetailView {
|
pub struct PullRequestDetailView {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
@@ -77,6 +82,15 @@ pub struct PullRequestDetailView {
|
|||||||
item_sizes: Rc<Vec<Size<Pixels>>>,
|
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||||
/// Virtual list state of the diff rows.
|
/// Virtual list state of the diff rows.
|
||||||
scroll_handle: VirtualListScrollHandle,
|
scroll_handle: VirtualListScrollHandle,
|
||||||
|
/// Per-row heights of the commits tab's virtual list, built when the
|
||||||
|
/// patch series is loaded.
|
||||||
|
commit_item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||||
|
/// Virtual list state of the commits tab.
|
||||||
|
commit_scroll_handle: VirtualListScrollHandle,
|
||||||
|
/// Comment bodies as shared strings, keyed by comment event ID, so
|
||||||
|
/// re-renders don't clone full contents again (events are immutable,
|
||||||
|
/// so the cache never needs invalidation).
|
||||||
|
contents: HashMap<EventId, SharedString>,
|
||||||
/// In-flight tasks; finished tasks are pruned on every push, so the vec
|
/// In-flight tasks; finished tasks are pruned on every push, so the vec
|
||||||
/// stays bounded by the number of concurrent loads.
|
/// stays bounded by the number of concurrent loads.
|
||||||
tasks: Vec<Task<Result<(), anyhow::Error>>>,
|
tasks: Vec<Task<Result<(), anyhow::Error>>>,
|
||||||
@@ -137,6 +151,9 @@ impl PullRequestDetailView {
|
|||||||
rows: Vec::new(),
|
rows: Vec::new(),
|
||||||
item_sizes: Rc::new(Vec::new()),
|
item_sizes: Rc::new(Vec::new()),
|
||||||
scroll_handle: VirtualListScrollHandle::new(),
|
scroll_handle: VirtualListScrollHandle::new(),
|
||||||
|
commit_item_sizes: Rc::new(Vec::new()),
|
||||||
|
commit_scroll_handle: VirtualListScrollHandle::new(),
|
||||||
|
contents: HashMap::new(),
|
||||||
tasks: Vec::new(),
|
tasks: Vec::new(),
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
@@ -146,9 +163,8 @@ impl PullRequestDetailView {
|
|||||||
/// and commit list on a background task and populate the tree.
|
/// and commit list on a background task and populate the tree.
|
||||||
///
|
///
|
||||||
/// The changes come from the PR's patch set (NIP-34 `e`-linked patch
|
/// The changes come from the PR's patch set (NIP-34 `e`-linked patch
|
||||||
/// events) when present; otherwise they live in the git repository
|
/// events) when present; otherwise from the git repository (`c`,
|
||||||
/// (`c`, `clone` and `merge-base` tags, per NIP-34), so the clone is
|
/// `clone` and `merge-base` tags), diffing the `merge-base..tip` range.
|
||||||
/// fetched and the `merge-base..tip` range is diffed.
|
|
||||||
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.loading = true;
|
self.loading = true;
|
||||||
self.error = None;
|
self.error = None;
|
||||||
@@ -263,6 +279,8 @@ impl PullRequestDetailView {
|
|||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.worktree = worktree;
|
this.worktree = worktree;
|
||||||
this.current_commit = current_commit.map(SharedString::from);
|
this.current_commit = current_commit.map(SharedString::from);
|
||||||
|
this.commit_item_sizes =
|
||||||
|
Rc::new(vec![size(px(0.), px(PR_COMMIT_ROW_HEIGHT)); commits.len()]);
|
||||||
this.commits = commits;
|
this.commits = commits;
|
||||||
match diff {
|
match diff {
|
||||||
Ok(diff) => {
|
Ok(diff) => {
|
||||||
@@ -799,15 +817,32 @@ impl PullRequestDetailView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.relative()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.min_h_0()
|
.min_h_0()
|
||||||
.overflow_y_scrollbar()
|
.child(
|
||||||
.children(
|
v_virtual_list(
|
||||||
self.commits
|
cx.entity().clone(),
|
||||||
.iter()
|
"pr-commits",
|
||||||
.enumerate()
|
self.commit_item_sizes.clone(),
|
||||||
.map(|(ix, commit)| self.render_commit_row(ix, commit, cx)),
|
move |this, range, _window, cx| {
|
||||||
|
range
|
||||||
|
.map(|ix| this.render_commit_row(ix, &this.commits[ix], cx))
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.track_scroll(&self.commit_scroll_handle)
|
||||||
|
.size_full(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.top_0()
|
||||||
|
.left_0()
|
||||||
|
.right_0()
|
||||||
|
.bottom_0()
|
||||||
|
.child(Scrollbar::vertical(&self.commit_scroll_handle)),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
@@ -826,7 +861,7 @@ impl PullRequestDetailView {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.id(ix)
|
.id(ix)
|
||||||
.px_4()
|
.px_4()
|
||||||
.py_2()
|
.h(px(PR_COMMIT_ROW_HEIGHT))
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.items_center()
|
.items_center()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
@@ -881,6 +916,13 @@ impl PullRequestDetailView {
|
|||||||
let author = profile.name();
|
let author = profile.name();
|
||||||
let picture = profile.picture();
|
let picture = profile.picture();
|
||||||
let age = relative_time(comment.created_at);
|
let age = relative_time(comment.created_at);
|
||||||
|
// Comment bodies are cloned into shared strings once per
|
||||||
|
// comment, not on every render.
|
||||||
|
let content = self
|
||||||
|
.contents
|
||||||
|
.entry(comment.id)
|
||||||
|
.or_insert_with(|| SharedString::from(comment.content.clone()))
|
||||||
|
.clone();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
@@ -915,11 +957,7 @@ impl PullRequestDetailView {
|
|||||||
.child(SharedString::from(age)),
|
.child(SharedString::from(age)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(div().text_sm().child(content))
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.child(SharedString::from(comment.content.clone())),
|
|
||||||
)
|
|
||||||
}))
|
}))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use gpui_component::scroll::Scrollbar;
|
|||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
ActiveTheme, Icon, Sizable, VirtualListScrollHandle, WindowExt, h_flex, v_flex, v_virtual_list,
|
ActiveTheme, Icon, Sizable, VirtualListScrollHandle, WindowExt, h_flex, v_flex, v_virtual_list,
|
||||||
};
|
};
|
||||||
use nostr::prelude::{Event, EventId, Kind};
|
use nostr::prelude::{EventId, Kind};
|
||||||
use signed_core::{RepoStatus, activity_subject};
|
use signed_core::{RepoStatus, activity_subject};
|
||||||
use signed_state::{ProfileStore, RepoStore};
|
use signed_state::{ProfileStore, RepoStore};
|
||||||
use utils::relative_time;
|
use utils::relative_time;
|
||||||
@@ -26,11 +26,8 @@ use super::helpers::{placeholder, status_badge};
|
|||||||
use super::pull_request_detail::PullRequestDetailView;
|
use super::pull_request_detail::PullRequestDetailView;
|
||||||
use crate::image_cache::{MAX_IMAGES, image_cache};
|
use crate::image_cache::{MAX_IMAGES, image_cache};
|
||||||
|
|
||||||
/// Height of one pull request row in the virtual list: same layout as an
|
/// Height of one pull request row in the virtual list; same layout as an
|
||||||
/// issue row (8px vertical padding (`py_2`) on top and bottom, a 32px title
|
/// issue row.
|
||||||
/// line (`h_8`) and a 24px meta line (`h_6`), plus the 1px bottom border),
|
|
||||||
/// so the row totals 73px. The status badge (`size_7`, 28px) is shorter
|
|
||||||
/// than the content.
|
|
||||||
const PR_ROW_HEIGHT: f32 = 73.;
|
const PR_ROW_HEIGHT: f32 = 73.;
|
||||||
|
|
||||||
/// Status filter of the pull request list, chosen via the header's filter
|
/// Status filter of the pull request list, chosen via the header's filter
|
||||||
@@ -51,14 +48,14 @@ enum PullRequestFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PullRequestFilter {
|
impl PullRequestFilter {
|
||||||
/// Whether `pr` (of `store`) is included by this filter.
|
/// Whether a pull request with `status` is included by this filter.
|
||||||
fn matches(self, store: &RepoStore, pr: &Event) -> bool {
|
fn matches(self, status: RepoStatus) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::All => true,
|
Self::All => true,
|
||||||
Self::Open => store.status_of(pr) == RepoStatus::Open,
|
Self::Open => status == RepoStatus::Open,
|
||||||
Self::Closed => store.status_of(pr) == RepoStatus::Closed,
|
Self::Closed => status == RepoStatus::Closed,
|
||||||
Self::Draft => store.status_of(pr) == RepoStatus::Draft,
|
Self::Draft => status == RepoStatus::Draft,
|
||||||
Self::Merged => store.status_of(pr) == RepoStatus::Applied,
|
Self::Merged => status == RepoStatus::Applied,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,10 +76,16 @@ pub struct PullRequestsView {
|
|||||||
/// pull request count); rebuilt on change.
|
/// pull request count); rebuilt on change.
|
||||||
pr_len: usize,
|
pr_len: usize,
|
||||||
/// Indices into the store's `pull_requests` matching [`Self::filter`]
|
/// Indices into the store's `pull_requests` matching [`Self::filter`]
|
||||||
/// (root PR events only; updates are revisions of the root and are not
|
/// (root PR events only; updates are revisions of the root); the
|
||||||
/// listed separately), rebuilt every render; the virtual list renders
|
/// virtual list renders this slice. Rebuilt only when the store
|
||||||
/// this slice.
|
/// version or the filter changes, keyed by [`Self::cache_key`].
|
||||||
visible_prs: Vec<usize>,
|
visible_prs: Vec<usize>,
|
||||||
|
/// Header counts `(total, open, closed, draft, merged)` of the root
|
||||||
|
/// pull requests only (revisions are not separate PRs), rebuilt with
|
||||||
|
/// [`Self::visible_prs`].
|
||||||
|
counts: (usize, usize, usize, usize, usize),
|
||||||
|
/// Store version and filter the cached rows/counts were built from.
|
||||||
|
cache_key: Option<(u64, PullRequestFilter)>,
|
||||||
/// Virtual list state of the pull requests list.
|
/// Virtual list state of the pull requests list.
|
||||||
scroll_handle: VirtualListScrollHandle,
|
scroll_handle: VirtualListScrollHandle,
|
||||||
}
|
}
|
||||||
@@ -104,6 +107,8 @@ impl PullRequestsView {
|
|||||||
item_sizes: Rc::new(Vec::new()),
|
item_sizes: Rc::new(Vec::new()),
|
||||||
pr_len: 0,
|
pr_len: 0,
|
||||||
visible_prs: Vec::new(),
|
visible_prs: Vec::new(),
|
||||||
|
counts: (0, 0, 0, 0, 0),
|
||||||
|
cache_key: None,
|
||||||
scroll_handle: VirtualListScrollHandle::new(),
|
scroll_handle: VirtualListScrollHandle::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,16 +211,9 @@ impl PullRequestsView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let store = self.store.read(cx);
|
// Counts of the last list rebuild (`render` rebuilds first when the
|
||||||
let (total, open, closed, draft, merged) = store.pull_requests.iter().fold(
|
// store version or filter changed, so this is never stale).
|
||||||
(0usize, 0usize, 0usize, 0usize, 0usize),
|
let (total, open, closed, draft, merged) = self.counts;
|
||||||
|(total, open, closed, draft, merged), pr| match store.status_of(pr) {
|
|
||||||
RepoStatus::Open => (total + 1, open + 1, closed, draft, merged),
|
|
||||||
RepoStatus::Closed => (total + 1, open, closed + 1, draft, merged),
|
|
||||||
RepoStatus::Draft => (total + 1, open, closed, draft + 1, merged),
|
|
||||||
RepoStatus::Applied => (total + 1, open, closed, draft, merged + 1),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.px_4()
|
.px_4()
|
||||||
@@ -541,19 +539,38 @@ impl Render for PullRequestsView {
|
|||||||
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 filter = self.filter;
|
let filter = self.filter;
|
||||||
|
|
||||||
// Indices of the root pull requests matching the active filter
|
// Rebuild the filtered rows and header counts only when the store
|
||||||
// (updates are revisions of the root and are not listed
|
// refreshed or the filter changed; other renders reuse the cache.
|
||||||
// separately); the virtual list renders this filtered slice.
|
let version = self.store.read(cx).version();
|
||||||
self.visible_prs = {
|
if self.cache_key != Some((version, filter)) {
|
||||||
let store = self.store.read(cx);
|
let store = self.store.read(cx);
|
||||||
store
|
let mut counts = (0usize, 0usize, 0usize, 0usize, 0usize);
|
||||||
|
self.visible_prs = store
|
||||||
.pull_requests
|
.pull_requests
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(_, pr)| pr.kind == Kind::GitPullRequest && filter.matches(store, pr))
|
.filter_map(|(ix, pr)| {
|
||||||
.map(|(ix, _)| ix)
|
// Kind-30620 patches are revisions of a root PR (NIP-34),
|
||||||
.collect()
|
// not separate pull requests: count only root events, or
|
||||||
};
|
// the header counts inflate with every revision (which
|
||||||
|
// also default to `Open` in `status_of`).
|
||||||
|
if pr.kind != Kind::GitPullRequest {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let status = store.status_of(pr);
|
||||||
|
counts.0 += 1;
|
||||||
|
match status {
|
||||||
|
RepoStatus::Open => counts.1 += 1,
|
||||||
|
RepoStatus::Closed => counts.2 += 1,
|
||||||
|
RepoStatus::Draft => counts.3 += 1,
|
||||||
|
RepoStatus::Applied => counts.4 += 1,
|
||||||
|
}
|
||||||
|
filter.matches(status).then_some(ix)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
self.counts = counts;
|
||||||
|
self.cache_key = Some((version, filter));
|
||||||
|
}
|
||||||
|
|
||||||
let count = self.visible_prs.len();
|
let count = self.visible_prs.len();
|
||||||
|
|
||||||
|
|||||||
@@ -97,9 +97,7 @@ pub struct RepoListView {
|
|||||||
/// Number of rows [`Self::item_sizes`] was built for (the filtered repo count).
|
/// Number of rows [`Self::item_sizes`] was built for (the filtered repo count).
|
||||||
repo_len: usize,
|
repo_len: usize,
|
||||||
/// Indices into the store's `announcements` matching [`Self::filter`],
|
/// Indices into the store's `announcements` matching [`Self::filter`],
|
||||||
/// in display order; rebuilt when the store changes, the filter is
|
/// in display order; the virtual list renders this slice.
|
||||||
/// switched, or the search text changes. The virtual list renders this
|
|
||||||
/// slice.
|
|
||||||
visible: Vec<usize>,
|
visible: Vec<usize>,
|
||||||
/// Search box filtering repositories by name.
|
/// Search box filtering repositories by name.
|
||||||
search: Entity<InputState>,
|
search: Entity<InputState>,
|
||||||
@@ -153,10 +151,7 @@ impl RepoListView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Rebuild [`Self::visible`] and [`Self::item_sizes`] from the current
|
/// Rebuild [`Self::visible`] and [`Self::item_sizes`] from the current
|
||||||
/// store contents, [`Self::filter`] and the search query. Called when
|
/// store contents, [`Self::filter`] and the search query.
|
||||||
/// the view is created, when the store changes, when the filter is
|
|
||||||
/// switched, and on every search keystroke, so the list is ready before
|
|
||||||
/// the next render.
|
|
||||||
fn rebuild_rows(&mut self, cx: &mut Context<Self>) {
|
fn rebuild_rows(&mut self, cx: &mut Context<Self>) {
|
||||||
let filter = self.filter;
|
let filter = self.filter;
|
||||||
let query = self.search.read(cx).value();
|
let query = self.search.read(cx).value();
|
||||||
|
|||||||
@@ -417,9 +417,8 @@ impl SidebarPanel {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sign-in placeholder shown while logged out: the banner artwork fills the
|
/// Sign-in placeholder shown while logged out: banner artwork behind a
|
||||||
/// panel behind a scrim that ends in a solid black band, keeping the CTA
|
/// scrim so the CTA buttons stay readable in both themes.
|
||||||
/// buttons readable on a clean dark surface in both themes.
|
|
||||||
fn render_sign_in(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
|
fn render_sign_in(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
|
||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
|
|||||||
+2
-14
@@ -7,17 +7,13 @@ use gpui_component::{Theme, ThemeRegistry, theme};
|
|||||||
use gpui_platform::application;
|
use gpui_platform::application;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Initialize logging
|
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
application()
|
application()
|
||||||
.with_assets(Assets)
|
.with_assets(Assets)
|
||||||
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()))
|
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()))
|
||||||
.run(move |cx| {
|
.run(move |cx| {
|
||||||
// Initialize components
|
|
||||||
gpui_component::init(cx);
|
gpui_component::init(cx);
|
||||||
|
|
||||||
// Initialize theme
|
|
||||||
theme::init(cx);
|
theme::init(cx);
|
||||||
|
|
||||||
// Register the built-in "Signed" theme (light + dark variants)
|
// Register the built-in "Signed" theme (light + dark variants)
|
||||||
@@ -49,10 +45,9 @@ fn main() {
|
|||||||
tracing::warn!("Signed Dark theme is missing from the registry");
|
tracing::warn!("Signed Dark theme is missing from the registry");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync the theme with the system appearance
|
|
||||||
Theme::sync_system_appearance(None, cx);
|
Theme::sync_system_appearance(None, cx);
|
||||||
|
|
||||||
// Initialize backend and stores (connects relays, restores session)
|
// Connects relays and restores the session.
|
||||||
std::fs::create_dir_all(paths::nostr_dir()).ok();
|
std::fs::create_dir_all(paths::nostr_dir()).ok();
|
||||||
signed_state::init(paths::nostr_dir(), cx);
|
signed_state::init(paths::nostr_dir(), cx);
|
||||||
|
|
||||||
@@ -60,15 +55,9 @@ fn main() {
|
|||||||
std::fs::create_dir_all(paths::repos_dir()).ok();
|
std::fs::create_dir_all(paths::repos_dir()).ok();
|
||||||
signed_state::GitStore::set_global(paths::repos_dir().clone(), cx);
|
signed_state::GitStore::set_global(paths::repos_dir().clone(), cx);
|
||||||
|
|
||||||
// Set app identity
|
|
||||||
cx.set_app_identity("su.reya.signed", "Signed");
|
cx.set_app_identity("su.reya.signed", "Signed");
|
||||||
|
|
||||||
// Set up the window options
|
|
||||||
let bounds = Bounds::centered(None, size(px(1120.0), px(750.0)), cx);
|
let bounds = Bounds::centered(None, size(px(1120.0), px(750.0)), cx);
|
||||||
|
|
||||||
// The dock's tab bar acts as the window title bar: the app owns
|
|
||||||
// title-bar dragging (via `start_window_move` on the tab bar), so
|
|
||||||
// AppKit must not treat the top strip as a native drag region.
|
|
||||||
let opts = WindowOptions {
|
let opts = WindowOptions {
|
||||||
window_background: WindowBackgroundAppearance::Opaque,
|
window_background: WindowBackgroundAppearance::Opaque,
|
||||||
window_decorations: Some(WindowDecorations::Client),
|
window_decorations: Some(WindowDecorations::Client),
|
||||||
@@ -78,7 +67,7 @@ fn main() {
|
|||||||
app_id: Some("Signed".to_owned()),
|
app_id: Some("Signed".to_owned()),
|
||||||
titlebar: Some(TitlebarOptions {
|
titlebar: Some(TitlebarOptions {
|
||||||
title: Some(SharedString::new_static("Signed")),
|
title: Some(SharedString::new_static("Signed")),
|
||||||
// AppKit's traffic-light buttons are 14 pt tall; offset them so their vertical center matches the tab bar.
|
// Center the 14pt traffic-light buttons on the tab bar.
|
||||||
traffic_light_position: Some(point(
|
traffic_light_position: Some(point(
|
||||||
px(9.0),
|
px(9.0),
|
||||||
px(TAB_BAR_HEIGHT / px(2.) - 14. / 2.),
|
px(TAB_BAR_HEIGHT / px(2.) - 14. / 2.),
|
||||||
@@ -94,7 +83,6 @@ fn main() {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
// Bring the app to the foreground
|
|
||||||
cx.activate(true);
|
cx.activate(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -2,8 +2,18 @@
|
|||||||
|
|
||||||
## Local repository scan
|
## Local repository scan
|
||||||
|
|
||||||
- [ ] Allow the user to configure which directories are scanned for local git repositories (currently fixed to the Desktop and Documents folders).
|
- [ ] Make the scanned directories configurable (currently fixed to Desktop and Documents).
|
||||||
|
|
||||||
## Create repository dialog
|
## Create repository dialog
|
||||||
|
|
||||||
- [ ] Persist the user's preferred local repository folder (the one picked in the create-repository dialog, defaulting to Desktop) and use it as the default next time the dialog opens.
|
- [ ] Remember the folder picked in the create-repository dialog and default to it next time (currently defaults to Desktop).
|
||||||
|
|
||||||
|
## Performance: render path
|
||||||
|
|
||||||
|
- [ ] Virtualize issue/PR comment threads (`issue_detail.rs::render_comments`, `pull_request_detail.rs::render_comments`). Harder than the list tabs: comment cards have variable heights and live inside a scrolling page together with the body and the comment form, so this needs either measured item sizes or restructuring the whole discussion tab into one virtual list. (Comment bodies are already cached as `SharedString`, so re-renders are cheap element constructions, not byte copies.)
|
||||||
|
|
||||||
|
## Performance: relay/subscription behavior
|
||||||
|
|
||||||
|
- [ ] Narrow `RepoStore`'s `BackendEvent::NostrUpdate` relevance filter (`crates/signed_state/src/repo.rs:65-98`): any comment/status/label/deletion from anywhere wakes every open repo store; match only events referencing this repo's roots or coordinate.
|
||||||
|
- [ ] Reconsider `ban_relay_on_mismatch(true)` (`crates/signed_nostr/src/backend.rs:49`): combined with many short-lived auto-close subscriptions, a late event after EOSE can permanently ban a relay for the session.
|
||||||
|
- [ ] Relays added for a repo stay in the pool forever and grow unboundedly (`crates/signed_state/src/backend.rs`); consider removing repo relays when the last panel for that repo closes.
|
||||||
|
|||||||
Reference in New Issue
Block a user