mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 10:29:35 +03:00
WIP: remoting (#10085)
Release Notes: - Added private alpha support for remote development. Please reach out to hi@zed.dev if you'd like to be part of shaping this feature.
This commit is contained in:
parent
ea4419076e
commit
f6c85b28d5
168
Cargo.lock
generated
168
Cargo.lock
generated
@ -520,7 +520,7 @@ dependencies = [
|
|||||||
"polling 3.3.2",
|
"polling 3.3.2",
|
||||||
"rustix 0.38.32",
|
"rustix 0.38.32",
|
||||||
"slab",
|
"slab",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -861,7 +861,7 @@ dependencies = [
|
|||||||
"ring 0.17.7",
|
"ring 0.17.7",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -897,7 +897,7 @@ dependencies = [
|
|||||||
"http-body",
|
"http-body",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -926,7 +926,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"regex-lite",
|
"regex-lite",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -949,7 +949,7 @@ dependencies = [
|
|||||||
"http 0.2.9",
|
"http 0.2.9",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex-lite",
|
"regex-lite",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -971,7 +971,7 @@ dependencies = [
|
|||||||
"http 0.2.9",
|
"http 0.2.9",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex-lite",
|
"regex-lite",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -994,7 +994,7 @@ dependencies = [
|
|||||||
"http 0.2.9",
|
"http 0.2.9",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex-lite",
|
"regex-lite",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1022,7 +1022,7 @@ dependencies = [
|
|||||||
"sha2 0.10.7",
|
"sha2 0.10.7",
|
||||||
"subtle",
|
"subtle",
|
||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1055,7 +1055,7 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"sha1",
|
"sha1",
|
||||||
"sha2 0.10.7",
|
"sha2 0.10.7",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1087,7 +1087,7 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1131,7 +1131,7 @@ dependencies = [
|
|||||||
"pin-utils",
|
"pin-utils",
|
||||||
"rustls",
|
"rustls",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1146,7 +1146,7 @@ dependencies = [
|
|||||||
"http 0.2.9",
|
"http 0.2.9",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1194,7 +1194,7 @@ dependencies = [
|
|||||||
"aws-smithy-types",
|
"aws-smithy-types",
|
||||||
"http 0.2.9",
|
"http 0.2.9",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1517,7 +1517,7 @@ dependencies = [
|
|||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-lite 2.2.0",
|
"futures-lite 2.2.0",
|
||||||
"piper",
|
"piper",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1877,6 +1877,12 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "channel"
|
name = "channel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -2238,6 +2244,7 @@ dependencies = [
|
|||||||
"git",
|
"git",
|
||||||
"google_ai",
|
"google_ai",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"headless",
|
||||||
"hex",
|
"hex",
|
||||||
"indoc",
|
"indoc",
|
||||||
"language",
|
"language",
|
||||||
@ -2279,7 +2286,7 @@ dependencies = [
|
|||||||
"toml 0.8.10",
|
"toml 0.8.10",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http 0.4.4",
|
"tower-http 0.4.4",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"unindent",
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
@ -2302,6 +2309,7 @@ dependencies = [
|
|||||||
"editor",
|
"editor",
|
||||||
"emojis",
|
"emojis",
|
||||||
"extensions_ui",
|
"extensions_ui",
|
||||||
|
"feature_flags",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
@ -2966,11 +2974,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctrlc"
|
name = "ctrlc"
|
||||||
version = "3.4.2"
|
version = "3.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b"
|
checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nix 0.27.1",
|
"nix 0.28.0",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4550,7 +4558,7 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4624,6 +4632,26 @@ dependencies = [
|
|||||||
"http 0.2.9",
|
"http 0.2.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headless"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"client",
|
||||||
|
"ctrlc",
|
||||||
|
"fs",
|
||||||
|
"futures 0.3.28",
|
||||||
|
"gpui",
|
||||||
|
"language",
|
||||||
|
"log",
|
||||||
|
"node_runtime",
|
||||||
|
"postage",
|
||||||
|
"project",
|
||||||
|
"rpc",
|
||||||
|
"settings",
|
||||||
|
"util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@ -4806,7 +4834,7 @@ dependencies = [
|
|||||||
"socket2 0.4.9",
|
"socket2 0.4.9",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -5121,7 +5149,7 @@ dependencies = [
|
|||||||
"polling 2.8.0",
|
"polling 2.8.0",
|
||||||
"slab",
|
"slab",
|
||||||
"sluice",
|
"sluice",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"tracing-futures",
|
"tracing-futures",
|
||||||
"url",
|
"url",
|
||||||
"waker-fn",
|
"waker-fn",
|
||||||
@ -6100,6 +6128,18 @@ dependencies = [
|
|||||||
"memoffset",
|
"memoffset",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.4.2",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "node_runtime"
|
name = "node_runtime"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -7036,7 +7076,7 @@ dependencies = [
|
|||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustix 0.38.32",
|
"rustix 0.38.32",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -7941,7 +7981,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"strum",
|
"strum",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"util",
|
"util",
|
||||||
"zstd",
|
"zstd",
|
||||||
]
|
]
|
||||||
@ -8308,7 +8348,7 @@ dependencies = [
|
|||||||
"strum",
|
"strum",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
@ -9059,7 +9099,7 @@ dependencies = [
|
|||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
@ -9146,7 +9186,7 @@ dependencies = [
|
|||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"uuid",
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
@ -9191,7 +9231,7 @@ dependencies = [
|
|||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"uuid",
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
@ -9216,7 +9256,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
@ -10039,7 +10079,7 @@ dependencies = [
|
|||||||
"futures-sink",
|
"futures-sink",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -10134,7 +10174,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -10171,7 +10211,7 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -10188,22 +10228,30 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.37"
|
version = "0.1.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
|
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
|
||||||
"log",
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing"
|
||||||
|
version = "0.1.40"
|
||||||
|
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-lite",
|
||||||
|
"tracing-core 0.1.32 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-attributes"
|
name = "tracing-attributes"
|
||||||
version = "0.1.26"
|
version = "0.1.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
|
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -10212,9 +10260,17 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.31"
|
version = "0.1.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
|
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-core"
|
||||||
|
version = "0.1.32"
|
||||||
|
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"valuable",
|
"valuable",
|
||||||
@ -10227,35 +10283,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
|
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pin-project",
|
"pin-project",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-log"
|
name = "tracing-log"
|
||||||
version = "0.1.3"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
|
||||||
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
|
||||||
"log",
|
"log",
|
||||||
"tracing-core",
|
"once_cell",
|
||||||
|
"tracing-core 0.1.32 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-serde"
|
name = "tracing-serde"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
|
||||||
checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"tracing-core",
|
"tracing-core 0.1.32 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-subscriber"
|
name = "tracing-subscriber"
|
||||||
version = "0.3.17"
|
version = "0.3.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
|
||||||
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"matchers",
|
"matchers",
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
@ -10266,8 +10319,8 @@ dependencies = [
|
|||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
"tracing",
|
"tracing 0.1.40 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
|
||||||
"tracing-core",
|
"tracing-core 0.1.32 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
"tracing-serde",
|
"tracing-serde",
|
||||||
]
|
]
|
||||||
@ -11157,7 +11210,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"wasmtime",
|
"wasmtime",
|
||||||
"wasmtime-c-api-macros",
|
"wasmtime-c-api-macros",
|
||||||
]
|
]
|
||||||
@ -11369,7 +11422,7 @@ dependencies = [
|
|||||||
"system-interface",
|
"system-interface",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"url",
|
"url",
|
||||||
"wasmtime",
|
"wasmtime",
|
||||||
"wiggle",
|
"wiggle",
|
||||||
@ -11606,7 +11659,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"bitflags 2.4.2",
|
"bitflags 2.4.2",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"wasmtime",
|
"wasmtime",
|
||||||
"wiggle-macro",
|
"wiggle-macro",
|
||||||
]
|
]
|
||||||
@ -12362,7 +12415,7 @@ dependencies = [
|
|||||||
"serde_repr",
|
"serde_repr",
|
||||||
"sha1",
|
"sha1",
|
||||||
"static_assertions",
|
"static_assertions",
|
||||||
"tracing",
|
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"uds_windows",
|
"uds_windows",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
"xdg-home",
|
"xdg-home",
|
||||||
@ -12434,6 +12487,7 @@ dependencies = [
|
|||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"go_to_line",
|
"go_to_line",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"headless",
|
||||||
"image_viewer",
|
"image_viewer",
|
||||||
"install_cli",
|
"install_cli",
|
||||||
"isahc",
|
"isahc",
|
||||||
|
@ -38,6 +38,7 @@ members = [
|
|||||||
"crates/google_ai",
|
"crates/google_ai",
|
||||||
"crates/gpui",
|
"crates/gpui",
|
||||||
"crates/gpui_macros",
|
"crates/gpui_macros",
|
||||||
|
"crates/headless",
|
||||||
"crates/image_viewer",
|
"crates/image_viewer",
|
||||||
"crates/install_cli",
|
"crates/install_cli",
|
||||||
"crates/journal",
|
"crates/journal",
|
||||||
@ -164,6 +165,7 @@ go_to_line = { path = "crates/go_to_line" }
|
|||||||
google_ai = { path = "crates/google_ai" }
|
google_ai = { path = "crates/google_ai" }
|
||||||
gpui = { path = "crates/gpui" }
|
gpui = { path = "crates/gpui" }
|
||||||
gpui_macros = { path = "crates/gpui_macros" }
|
gpui_macros = { path = "crates/gpui_macros" }
|
||||||
|
headless = { path = "crates/headless" }
|
||||||
install_cli = { path = "crates/install_cli" }
|
install_cli = { path = "crates/install_cli" }
|
||||||
image_viewer = { path = "crates/image_viewer" }
|
image_viewer = { path = "crates/image_viewer" }
|
||||||
journal = { path = "crates/journal" }
|
journal = { path = "crates/journal" }
|
||||||
@ -242,6 +244,7 @@ chrono = { version = "0.4", features = ["serde"] }
|
|||||||
clap = { version = "4.4", features = ["derive"] }
|
clap = { version = "4.4", features = ["derive"] }
|
||||||
clickhouse = { version = "0.11.6" }
|
clickhouse = { version = "0.11.6" }
|
||||||
ctor = "0.2.6"
|
ctor = "0.2.6"
|
||||||
|
ctrlc = "3.4.4"
|
||||||
core-foundation = { version = "0.9.3" }
|
core-foundation = { version = "0.9.3" }
|
||||||
core-foundation-sys = "0.8.6"
|
core-foundation-sys = "0.8.6"
|
||||||
derive_more = "0.99.17"
|
derive_more = "0.99.17"
|
||||||
|
5
assets/icons/server.svg
Normal file
5
assets/icons/server.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.99993 6.85713C11.1558 6.85713 13.7142 5.83379 13.7142 4.57142C13.7142 3.30905 11.1558 2.28571 7.99993 2.28571C4.84402 2.28571 2.28564 3.30905 2.28564 4.57142C2.28564 5.83379 4.84402 6.85713 7.99993 6.85713Z" fill="black" stroke="black" stroke-width="1.5"/>
|
||||||
|
<path d="M13.7142 4.57141V11.4286C13.7142 12.691 11.1558 13.7143 7.99993 13.7143C4.84402 13.7143 2.28564 12.691 2.28564 11.4286V4.57141" stroke="black" stroke-width="1.5"/>
|
||||||
|
<path d="M13.7142 8C13.7142 9.26237 11.1558 10.2857 7.99993 10.2857C4.84402 10.2857 2.28564 9.26237 2.28564 8" stroke="black" stroke-width="1.5"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 692 B |
1
assets/icons/trash.svg
Normal file
1
assets/icons/trash.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
|
After Width: | Height: | Size: 409 B |
@ -1182,7 +1182,7 @@ impl Room {
|
|||||||
cx.emit(Event::RemoteProjectJoined { project_id: id });
|
cx.emit(Event::RemoteProjectJoined { project_id: id });
|
||||||
cx.spawn(move |this, mut cx| async move {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
let project =
|
let project =
|
||||||
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
|
Project::in_room(id, client, user_store, language_registry, fs, cx.clone()).await?;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.joined_projects.retain(|project| {
|
this.joined_projects.retain(|project| {
|
||||||
|
@ -11,7 +11,9 @@ pub use channel_chat::{
|
|||||||
mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId,
|
mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId,
|
||||||
MessageParams,
|
MessageParams,
|
||||||
};
|
};
|
||||||
pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore};
|
pub use channel_store::{
|
||||||
|
Channel, ChannelEvent, ChannelMembership, ChannelStore, DevServer, RemoteProject,
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod channel_store_tests;
|
mod channel_store_tests;
|
||||||
|
@ -3,7 +3,10 @@ mod channel_index;
|
|||||||
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
|
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use channel_index::ChannelIndex;
|
use channel_index::ChannelIndex;
|
||||||
use client::{ChannelId, Client, ClientSettings, ProjectId, Subscription, User, UserId, UserStore};
|
use client::{
|
||||||
|
ChannelId, Client, ClientSettings, DevServerId, ProjectId, RemoteProjectId, Subscription, User,
|
||||||
|
UserId, UserStore,
|
||||||
|
};
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
use collections::{hash_map, HashMap, HashSet};
|
||||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@ -12,7 +15,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use language::Capability;
|
use language::Capability;
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{self, ChannelRole, ChannelVisibility},
|
proto::{self, ChannelRole, ChannelVisibility, DevServerStatus},
|
||||||
TypedEnvelope,
|
TypedEnvelope,
|
||||||
};
|
};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
@ -40,7 +43,6 @@ pub struct HostedProject {
|
|||||||
name: SharedString,
|
name: SharedString,
|
||||||
_visibility: proto::ChannelVisibility,
|
_visibility: proto::ChannelVisibility,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<proto::HostedProject> for HostedProject {
|
impl From<proto::HostedProject> for HostedProject {
|
||||||
fn from(project: proto::HostedProject) -> Self {
|
fn from(project: proto::HostedProject) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -52,12 +54,56 @@ impl From<proto::HostedProject> for HostedProject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RemoteProject {
|
||||||
|
pub id: RemoteProjectId,
|
||||||
|
pub project_id: Option<ProjectId>,
|
||||||
|
pub channel_id: ChannelId,
|
||||||
|
pub name: SharedString,
|
||||||
|
pub path: SharedString,
|
||||||
|
pub dev_server_id: DevServerId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<proto::RemoteProject> for RemoteProject {
|
||||||
|
fn from(project: proto::RemoteProject) -> Self {
|
||||||
|
Self {
|
||||||
|
id: RemoteProjectId(project.id),
|
||||||
|
project_id: project.project_id.map(|id| ProjectId(id)),
|
||||||
|
channel_id: ChannelId(project.channel_id),
|
||||||
|
name: project.name.into(),
|
||||||
|
path: project.path.into(),
|
||||||
|
dev_server_id: DevServerId(project.dev_server_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DevServer {
|
||||||
|
pub id: DevServerId,
|
||||||
|
pub channel_id: ChannelId,
|
||||||
|
pub name: SharedString,
|
||||||
|
pub status: DevServerStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<proto::DevServer> for DevServer {
|
||||||
|
fn from(dev_server: proto::DevServer) -> Self {
|
||||||
|
Self {
|
||||||
|
id: DevServerId(dev_server.dev_server_id),
|
||||||
|
channel_id: ChannelId(dev_server.channel_id),
|
||||||
|
status: dev_server.status(),
|
||||||
|
name: dev_server.name.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ChannelStore {
|
pub struct ChannelStore {
|
||||||
pub channel_index: ChannelIndex,
|
pub channel_index: ChannelIndex,
|
||||||
channel_invitations: Vec<Arc<Channel>>,
|
channel_invitations: Vec<Arc<Channel>>,
|
||||||
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
||||||
channel_states: HashMap<ChannelId, ChannelState>,
|
channel_states: HashMap<ChannelId, ChannelState>,
|
||||||
hosted_projects: HashMap<ProjectId, HostedProject>,
|
hosted_projects: HashMap<ProjectId, HostedProject>,
|
||||||
|
remote_projects: HashMap<RemoteProjectId, RemoteProject>,
|
||||||
|
dev_servers: HashMap<DevServerId, DevServer>,
|
||||||
|
|
||||||
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
||||||
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
||||||
@ -87,6 +133,8 @@ pub struct ChannelState {
|
|||||||
observed_chat_message: Option<u64>,
|
observed_chat_message: Option<u64>,
|
||||||
role: Option<ChannelRole>,
|
role: Option<ChannelRole>,
|
||||||
projects: HashSet<ProjectId>,
|
projects: HashSet<ProjectId>,
|
||||||
|
dev_servers: HashSet<DevServerId>,
|
||||||
|
remote_projects: HashSet<RemoteProjectId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
@ -217,6 +265,8 @@ impl ChannelStore {
|
|||||||
channel_index: ChannelIndex::default(),
|
channel_index: ChannelIndex::default(),
|
||||||
channel_participants: Default::default(),
|
channel_participants: Default::default(),
|
||||||
hosted_projects: Default::default(),
|
hosted_projects: Default::default(),
|
||||||
|
remote_projects: Default::default(),
|
||||||
|
dev_servers: Default::default(),
|
||||||
outgoing_invites: Default::default(),
|
outgoing_invites: Default::default(),
|
||||||
opened_buffers: Default::default(),
|
opened_buffers: Default::default(),
|
||||||
opened_chats: Default::default(),
|
opened_chats: Default::default(),
|
||||||
@ -316,6 +366,40 @@ impl ChannelStore {
|
|||||||
projects
|
projects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dev_servers_for_id(&self, channel_id: ChannelId) -> Vec<DevServer> {
|
||||||
|
let mut dev_servers: Vec<DevServer> = self
|
||||||
|
.channel_states
|
||||||
|
.get(&channel_id)
|
||||||
|
.map(|state| state.dev_servers.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|id| self.dev_servers.get(&id).cloned())
|
||||||
|
.collect();
|
||||||
|
dev_servers.sort_by_key(|s| (s.name.clone(), s.id));
|
||||||
|
dev_servers
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_dev_server_by_id(&self, id: DevServerId) -> Option<&DevServer> {
|
||||||
|
self.dev_servers.get(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_remote_project_by_id(&self, id: RemoteProjectId) -> Option<&RemoteProject> {
|
||||||
|
self.remote_projects.get(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remote_projects_for_id(&self, channel_id: ChannelId) -> Vec<RemoteProject> {
|
||||||
|
let mut remote_projects: Vec<RemoteProject> = self
|
||||||
|
.channel_states
|
||||||
|
.get(&channel_id)
|
||||||
|
.map(|state| state.remote_projects.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|id| self.remote_projects.get(&id).cloned())
|
||||||
|
.collect();
|
||||||
|
remote_projects.sort_by_key(|p| (p.name.clone(), p.id));
|
||||||
|
remote_projects
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool {
|
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool {
|
||||||
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
|
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
|
||||||
if let OpenedModelHandle::Open(buffer) = buffer {
|
if let OpenedModelHandle::Open(buffer) = buffer {
|
||||||
@ -818,6 +902,45 @@ impl ChannelStore {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_remote_project(
|
||||||
|
&mut self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
dev_server_id: DevServerId,
|
||||||
|
name: String,
|
||||||
|
path: String,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<proto::CreateRemoteProjectResponse>> {
|
||||||
|
let client = self.client.clone();
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
client
|
||||||
|
.request(proto::CreateRemoteProject {
|
||||||
|
channel_id: channel_id.0,
|
||||||
|
dev_server_id: dev_server_id.0,
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_dev_server(
|
||||||
|
&mut self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
name: String,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<proto::CreateDevServerResponse>> {
|
||||||
|
let client = self.client.clone();
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
let result = client
|
||||||
|
.request(proto::CreateDevServer {
|
||||||
|
channel_id: channel_id.0,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_channel_member_details(
|
pub fn get_channel_member_details(
|
||||||
&self,
|
&self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
@ -1098,7 +1221,11 @@ impl ChannelStore {
|
|||||||
|| !payload.latest_channel_message_ids.is_empty()
|
|| !payload.latest_channel_message_ids.is_empty()
|
||||||
|| !payload.latest_channel_buffer_versions.is_empty()
|
|| !payload.latest_channel_buffer_versions.is_empty()
|
||||||
|| !payload.hosted_projects.is_empty()
|
|| !payload.hosted_projects.is_empty()
|
||||||
|| !payload.deleted_hosted_projects.is_empty();
|
|| !payload.deleted_hosted_projects.is_empty()
|
||||||
|
|| !payload.dev_servers.is_empty()
|
||||||
|
|| !payload.deleted_dev_servers.is_empty()
|
||||||
|
|| !payload.remote_projects.is_empty()
|
||||||
|
|| !payload.deleted_remote_projects.is_empty();
|
||||||
|
|
||||||
if channels_changed {
|
if channels_changed {
|
||||||
if !payload.delete_channels.is_empty() {
|
if !payload.delete_channels.is_empty() {
|
||||||
@ -1186,6 +1313,60 @@ impl ChannelStore {
|
|||||||
.remove_hosted_project(old_project.project_id);
|
.remove_hosted_project(old_project.project_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for remote_project in payload.remote_projects {
|
||||||
|
let remote_project: RemoteProject = remote_project.into();
|
||||||
|
if let Some(old_remote_project) = self
|
||||||
|
.remote_projects
|
||||||
|
.insert(remote_project.id, remote_project.clone())
|
||||||
|
{
|
||||||
|
self.channel_states
|
||||||
|
.entry(old_remote_project.channel_id)
|
||||||
|
.or_default()
|
||||||
|
.remove_remote_project(old_remote_project.id);
|
||||||
|
}
|
||||||
|
self.channel_states
|
||||||
|
.entry(remote_project.channel_id)
|
||||||
|
.or_default()
|
||||||
|
.add_remote_project(remote_project.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for remote_project_id in payload.deleted_remote_projects {
|
||||||
|
let remote_project_id = RemoteProjectId(remote_project_id);
|
||||||
|
|
||||||
|
if let Some(old_project) = self.remote_projects.remove(&remote_project_id) {
|
||||||
|
self.channel_states
|
||||||
|
.entry(old_project.channel_id)
|
||||||
|
.or_default()
|
||||||
|
.remove_remote_project(old_project.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for dev_server in payload.dev_servers {
|
||||||
|
let dev_server: DevServer = dev_server.into();
|
||||||
|
if let Some(old_server) = self.dev_servers.insert(dev_server.id, dev_server.clone())
|
||||||
|
{
|
||||||
|
self.channel_states
|
||||||
|
.entry(old_server.channel_id)
|
||||||
|
.or_default()
|
||||||
|
.remove_dev_server(old_server.id);
|
||||||
|
}
|
||||||
|
self.channel_states
|
||||||
|
.entry(dev_server.channel_id)
|
||||||
|
.or_default()
|
||||||
|
.add_dev_server(dev_server.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for dev_server_id in payload.deleted_dev_servers {
|
||||||
|
let dev_server_id = DevServerId(dev_server_id);
|
||||||
|
|
||||||
|
if let Some(old_server) = self.dev_servers.remove(&dev_server_id) {
|
||||||
|
self.channel_states
|
||||||
|
.entry(old_server.channel_id)
|
||||||
|
.or_default()
|
||||||
|
.remove_dev_server(old_server.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@ -1300,4 +1481,20 @@ impl ChannelState {
|
|||||||
fn remove_hosted_project(&mut self, project_id: ProjectId) {
|
fn remove_hosted_project(&mut self, project_id: ProjectId) {
|
||||||
self.projects.remove(&project_id);
|
self.projects.remove(&project_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_remote_project(&mut self, remote_project_id: RemoteProjectId) {
|
||||||
|
self.remote_projects.insert(remote_project_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_remote_project(&mut self, remote_project_id: RemoteProjectId) {
|
||||||
|
self.remote_projects.remove(&remote_project_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_dev_server(&mut self, dev_server_id: DevServerId) {
|
||||||
|
self.dev_servers.insert(dev_server_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_dev_server(&mut self, dev_server_id: DevServerId) {
|
||||||
|
self.dev_servers.remove(&dev_server_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -759,8 +759,9 @@ impl Client {
|
|||||||
read_credentials_from_keychain(cx).await.is_some()
|
read_credentials_from_keychain(cx).await.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_dev_server_token(&self, token: DevServerToken) {
|
pub fn set_dev_server_token(&self, token: DevServerToken) -> &Self {
|
||||||
self.state.write().credentials = Some(Credentials::DevServer { token });
|
self.state.write().credentials = Some(Credentials::DevServer { token });
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_recursion(?Send)]
|
#[async_recursion(?Send)]
|
||||||
|
@ -27,6 +27,12 @@ impl std::fmt::Display for ChannelId {
|
|||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
pub struct ProjectId(pub u64);
|
pub struct ProjectId(pub u64);
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
|
pub struct DevServerId(pub u64);
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
|
pub struct RemoteProjectId(pub u64);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct ParticipantIndex(pub u32);
|
pub struct ParticipantIndex(pub u32);
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
DATABASE_URL = "postgres://postgres@localhost/zed"
|
DATABASE_URL = "postgres://postgres@localhost/zed"
|
||||||
# DATABASE_URL = "sqlite:////home/zed/.config/zed/db.sqlite3?mode=rwc"
|
# DATABASE_URL = "sqlite:////root/0/zed/db.sqlite3?mode=rwc"
|
||||||
DATABASE_MAX_CONNECTIONS = 5
|
DATABASE_MAX_CONNECTIONS = 5
|
||||||
HTTP_PORT = 8080
|
HTTP_PORT = 8080
|
||||||
API_TOKEN = "secret"
|
API_TOKEN = "secret"
|
||||||
|
@ -63,8 +63,8 @@ tokio.workspace = true
|
|||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
tower = "0.4"
|
tower = "0.4"
|
||||||
tower-http = { workspace = true, features = ["trace"] }
|
tower-http = { workspace = true, features = ["trace"] }
|
||||||
tracing = "0.1.34"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json", "registry", "tracing-log"] }
|
tracing-subscriber = { git = "https://github.com/tokio-rs/tracing", rev = "tracing-subscriber-0.3.18", features = ["env-filter", "json", "registry", "tracing-log"] } # workaround for https://github.com/tokio-rs/tracing/issues/2927
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
|
||||||
@ -102,3 +102,4 @@ theme.workspace = true
|
|||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace = { workspace = true, features = ["test-support"] }
|
workspace = { workspace = true, features = ["test-support"] }
|
||||||
|
headless.workspace = true
|
||||||
|
@ -45,12 +45,13 @@ CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
|
|||||||
|
|
||||||
CREATE TABLE "projects" (
|
CREATE TABLE "projects" (
|
||||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
"room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL,
|
"room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE,
|
||||||
"host_user_id" INTEGER REFERENCES users (id),
|
"host_user_id" INTEGER REFERENCES users (id),
|
||||||
"host_connection_id" INTEGER,
|
"host_connection_id" INTEGER,
|
||||||
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
|
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
|
||||||
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE,
|
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
"hosted_project_id" INTEGER REFERENCES hosted_projects (id)
|
"hosted_project_id" INTEGER REFERENCES hosted_projects (id),
|
||||||
|
"remote_project_id" INTEGER REFERENCES remote_projects(id)
|
||||||
);
|
);
|
||||||
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
|
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
|
||||||
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
|
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
|
||||||
@ -397,7 +398,9 @@ CREATE TABLE hosted_projects (
|
|||||||
channel_id INTEGER NOT NULL REFERENCES channels(id),
|
channel_id INTEGER NOT NULL REFERENCES channels(id),
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
visibility TEXT NOT NULL,
|
visibility TEXT NOT NULL,
|
||||||
deleted_at TIMESTAMP NULL
|
deleted_at TIMESTAMP NULL,
|
||||||
|
dev_server_id INTEGER REFERENCES dev_servers(id),
|
||||||
|
dev_server_path TEXT
|
||||||
);
|
);
|
||||||
CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id);
|
CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id);
|
||||||
CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL);
|
CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL);
|
||||||
@ -409,3 +412,13 @@ CREATE TABLE dev_servers (
|
|||||||
hashed_token TEXT NOT NULL
|
hashed_token TEXT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX idx_dev_servers_on_channel_id ON dev_servers (channel_id);
|
CREATE INDEX idx_dev_servers_on_channel_id ON dev_servers (channel_id);
|
||||||
|
|
||||||
|
CREATE TABLE remote_projects (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
channel_id INTEGER NOT NULL REFERENCES channels(id),
|
||||||
|
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE hosted_projects ADD COLUMN remote_project_id INTEGER REFERENCES remote_projects(id);
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE remote_projects (
|
||||||
|
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||||
|
channel_id INT NOT NULL REFERENCES channels(id),
|
||||||
|
dev_server_id INT NOT NULL REFERENCES dev_servers(id),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE projects ADD COLUMN remote_project_id INTEGER REFERENCES remote_projects(id);
|
@ -10,6 +10,7 @@ use axum::{
|
|||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use prometheus::{exponential_buckets, register_histogram, Histogram};
|
use prometheus::{exponential_buckets, register_histogram, Histogram};
|
||||||
|
pub use rpc::auth::random_token;
|
||||||
use scrypt::{
|
use scrypt::{
|
||||||
password_hash::{PasswordHash, PasswordVerifier},
|
password_hash::{PasswordHash, PasswordVerifier},
|
||||||
Scrypt,
|
Scrypt,
|
||||||
@ -152,7 +153,7 @@ pub async fn create_access_token(
|
|||||||
/// Hashing prevents anyone with access to the database being able to login.
|
/// Hashing prevents anyone with access to the database being able to login.
|
||||||
/// As the token is randomly generated, we don't need to worry about scrypt-style
|
/// As the token is randomly generated, we don't need to worry about scrypt-style
|
||||||
/// protection.
|
/// protection.
|
||||||
fn hash_access_token(token: &str) -> String {
|
pub fn hash_access_token(token: &str) -> String {
|
||||||
let digest = sha2::Sha256::digest(token);
|
let digest = sha2::Sha256::digest(token);
|
||||||
format!(
|
format!(
|
||||||
"$sha256${}",
|
"$sha256${}",
|
||||||
@ -230,18 +231,15 @@ pub async fn verify_access_token(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// a dev_server_token has the format <id>.<base64>. This is to make them
|
pub fn generate_dev_server_token(id: usize, access_token: String) -> String {
|
||||||
// relatively easy to copy/paste around.
|
format!("{}.{}", id, access_token)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn verify_dev_server_token(
|
pub async fn verify_dev_server_token(
|
||||||
dev_server_token: &str,
|
dev_server_token: &str,
|
||||||
db: &Arc<Database>,
|
db: &Arc<Database>,
|
||||||
) -> anyhow::Result<dev_server::Model> {
|
) -> anyhow::Result<dev_server::Model> {
|
||||||
let mut parts = dev_server_token.splitn(2, '.');
|
let (id, token) = split_dev_server_token(dev_server_token)?;
|
||||||
let id = DevServerId(parts.next().unwrap_or_default().parse()?);
|
|
||||||
let token = parts
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| anyhow!("invalid dev server token format"))?;
|
|
||||||
|
|
||||||
let token_hash = hash_access_token(&token);
|
let token_hash = hash_access_token(&token);
|
||||||
let server = db.get_dev_server(id).await?;
|
let server = db.get_dev_server(id).await?;
|
||||||
|
|
||||||
@ -257,6 +255,17 @@ pub async fn verify_dev_server_token(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// a dev_server_token has the format <id>.<base64>. This is to make them
|
||||||
|
// relatively easy to copy/paste around.
|
||||||
|
pub fn split_dev_server_token(dev_server_token: &str) -> anyhow::Result<(DevServerId, &str)> {
|
||||||
|
let mut parts = dev_server_token.splitn(2, '.');
|
||||||
|
let id = DevServerId(parts.next().unwrap_or_default().parse()?);
|
||||||
|
let token = parts
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow!("invalid dev server token format"))?;
|
||||||
|
Ok((id, token))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
|
@ -56,6 +56,7 @@ pub struct Database {
|
|||||||
options: ConnectOptions,
|
options: ConnectOptions,
|
||||||
pool: DatabaseConnection,
|
pool: DatabaseConnection,
|
||||||
rooms: DashMap<RoomId, Arc<Mutex<()>>>,
|
rooms: DashMap<RoomId, Arc<Mutex<()>>>,
|
||||||
|
projects: DashMap<ProjectId, Arc<Mutex<()>>>,
|
||||||
rng: Mutex<StdRng>,
|
rng: Mutex<StdRng>,
|
||||||
executor: Executor,
|
executor: Executor,
|
||||||
notification_kinds_by_id: HashMap<NotificationKindId, &'static str>,
|
notification_kinds_by_id: HashMap<NotificationKindId, &'static str>,
|
||||||
@ -74,6 +75,7 @@ impl Database {
|
|||||||
options: options.clone(),
|
options: options.clone(),
|
||||||
pool: sea_orm::Database::connect(options).await?,
|
pool: sea_orm::Database::connect(options).await?,
|
||||||
rooms: DashMap::with_capacity(16384),
|
rooms: DashMap::with_capacity(16384),
|
||||||
|
projects: DashMap::with_capacity(16384),
|
||||||
rng: Mutex::new(StdRng::seed_from_u64(0)),
|
rng: Mutex::new(StdRng::seed_from_u64(0)),
|
||||||
notification_kinds_by_id: HashMap::default(),
|
notification_kinds_by_id: HashMap::default(),
|
||||||
notification_kinds_by_name: HashMap::default(),
|
notification_kinds_by_name: HashMap::default(),
|
||||||
@ -86,6 +88,7 @@ impl Database {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn reset(&self) {
|
pub fn reset(&self) {
|
||||||
self.rooms.clear();
|
self.rooms.clear();
|
||||||
|
self.projects.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs the database migrations.
|
/// Runs the database migrations.
|
||||||
@ -190,7 +193,10 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The same as room_transaction, but if you need to only optionally return a Room.
|
/// The same as room_transaction, but if you need to only optionally return a Room.
|
||||||
async fn optional_room_transaction<F, Fut, T>(&self, f: F) -> Result<Option<RoomGuard<T>>>
|
async fn optional_room_transaction<F, Fut, T>(
|
||||||
|
&self,
|
||||||
|
f: F,
|
||||||
|
) -> Result<Option<TransactionGuard<T>>>
|
||||||
where
|
where
|
||||||
F: Send + Fn(TransactionHandle) -> Fut,
|
F: Send + Fn(TransactionHandle) -> Fut,
|
||||||
Fut: Send + Future<Output = Result<Option<(RoomId, T)>>>,
|
Fut: Send + Future<Output = Result<Option<(RoomId, T)>>>,
|
||||||
@ -205,7 +211,7 @@ impl Database {
|
|||||||
let _guard = lock.lock_owned().await;
|
let _guard = lock.lock_owned().await;
|
||||||
match tx.commit().await.map_err(Into::into) {
|
match tx.commit().await.map_err(Into::into) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
return Ok(Some(RoomGuard {
|
return Ok(Some(TransactionGuard {
|
||||||
data,
|
data,
|
||||||
_guard,
|
_guard,
|
||||||
_not_send: PhantomData,
|
_not_send: PhantomData,
|
||||||
@ -240,10 +246,63 @@ impl Database {
|
|||||||
self.run(body).await
|
self.run(body).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn project_transaction<F, Fut, T>(
|
||||||
|
&self,
|
||||||
|
project_id: ProjectId,
|
||||||
|
f: F,
|
||||||
|
) -> Result<TransactionGuard<T>>
|
||||||
|
where
|
||||||
|
F: Send + Fn(TransactionHandle) -> Fut,
|
||||||
|
Fut: Send + Future<Output = Result<T>>,
|
||||||
|
{
|
||||||
|
let room_id = Database::room_id_for_project(&self, project_id).await?;
|
||||||
|
let body = async {
|
||||||
|
let mut i = 0;
|
||||||
|
loop {
|
||||||
|
let lock = if let Some(room_id) = room_id {
|
||||||
|
self.rooms.entry(room_id).or_default().clone()
|
||||||
|
} else {
|
||||||
|
self.projects.entry(project_id).or_default().clone()
|
||||||
|
};
|
||||||
|
let _guard = lock.lock_owned().await;
|
||||||
|
let (tx, result) = self.with_transaction(&f).await?;
|
||||||
|
match result {
|
||||||
|
Ok(data) => match tx.commit().await.map_err(Into::into) {
|
||||||
|
Ok(()) => {
|
||||||
|
return Ok(TransactionGuard {
|
||||||
|
data,
|
||||||
|
_guard,
|
||||||
|
_not_send: PhantomData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
if !self.retry_on_serialization_error(&error, i).await {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
tx.rollback().await?;
|
||||||
|
if !self.retry_on_serialization_error(&error, i).await {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.run(body).await
|
||||||
|
}
|
||||||
|
|
||||||
/// room_transaction runs the block in a transaction. It returns a RoomGuard, that keeps
|
/// room_transaction runs the block in a transaction. It returns a RoomGuard, that keeps
|
||||||
/// the database locked until it is dropped. This ensures that updates sent to clients are
|
/// the database locked until it is dropped. This ensures that updates sent to clients are
|
||||||
/// properly serialized with respect to database changes.
|
/// properly serialized with respect to database changes.
|
||||||
async fn room_transaction<F, Fut, T>(&self, room_id: RoomId, f: F) -> Result<RoomGuard<T>>
|
async fn room_transaction<F, Fut, T>(
|
||||||
|
&self,
|
||||||
|
room_id: RoomId,
|
||||||
|
f: F,
|
||||||
|
) -> Result<TransactionGuard<T>>
|
||||||
where
|
where
|
||||||
F: Send + Fn(TransactionHandle) -> Fut,
|
F: Send + Fn(TransactionHandle) -> Fut,
|
||||||
Fut: Send + Future<Output = Result<T>>,
|
Fut: Send + Future<Output = Result<T>>,
|
||||||
@ -257,7 +316,7 @@ impl Database {
|
|||||||
match result {
|
match result {
|
||||||
Ok(data) => match tx.commit().await.map_err(Into::into) {
|
Ok(data) => match tx.commit().await.map_err(Into::into) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
return Ok(RoomGuard {
|
return Ok(TransactionGuard {
|
||||||
data,
|
data,
|
||||||
_guard,
|
_guard,
|
||||||
_not_send: PhantomData,
|
_not_send: PhantomData,
|
||||||
@ -399,15 +458,16 @@ impl Deref for TransactionHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`RoomGuard`] keeps a database transaction alive until it is dropped.
|
/// [`TransactionGuard`] keeps a database transaction alive until it is dropped.
|
||||||
/// so that updates to rooms are serialized.
|
/// It wraps data that depends on the state of the database and prevents an additional
|
||||||
pub struct RoomGuard<T> {
|
/// transaction from starting that would invalidate that data.
|
||||||
|
pub struct TransactionGuard<T> {
|
||||||
data: T,
|
data: T,
|
||||||
_guard: OwnedMutexGuard<()>,
|
_guard: OwnedMutexGuard<()>,
|
||||||
_not_send: PhantomData<Rc<()>>,
|
_not_send: PhantomData<Rc<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Deref for RoomGuard<T> {
|
impl<T> Deref for TransactionGuard<T> {
|
||||||
type Target = T;
|
type Target = T;
|
||||||
|
|
||||||
fn deref(&self) -> &T {
|
fn deref(&self) -> &T {
|
||||||
@ -415,13 +475,13 @@ impl<T> Deref for RoomGuard<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> DerefMut for RoomGuard<T> {
|
impl<T> DerefMut for TransactionGuard<T> {
|
||||||
fn deref_mut(&mut self) -> &mut T {
|
fn deref_mut(&mut self) -> &mut T {
|
||||||
&mut self.data
|
&mut self.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> RoomGuard<T> {
|
impl<T> TransactionGuard<T> {
|
||||||
/// Returns the inner value of the guard.
|
/// Returns the inner value of the guard.
|
||||||
pub fn into_inner(self) -> T {
|
pub fn into_inner(self) -> T {
|
||||||
self.data
|
self.data
|
||||||
@ -518,6 +578,7 @@ pub struct MembershipUpdated {
|
|||||||
|
|
||||||
/// The result of setting a member's role.
|
/// The result of setting a member's role.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum SetMemberRoleResult {
|
pub enum SetMemberRoleResult {
|
||||||
InviteUpdated(Channel),
|
InviteUpdated(Channel),
|
||||||
MembershipUpdated(MembershipUpdated),
|
MembershipUpdated(MembershipUpdated),
|
||||||
@ -594,6 +655,8 @@ pub struct ChannelsForUser {
|
|||||||
pub channel_memberships: Vec<channel_member::Model>,
|
pub channel_memberships: Vec<channel_member::Model>,
|
||||||
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
||||||
pub hosted_projects: Vec<proto::HostedProject>,
|
pub hosted_projects: Vec<proto::HostedProject>,
|
||||||
|
pub dev_servers: Vec<dev_server::Model>,
|
||||||
|
pub remote_projects: Vec<proto::RemoteProject>,
|
||||||
|
|
||||||
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
|
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
|
||||||
pub observed_channel_messages: Vec<proto::ChannelMessageId>,
|
pub observed_channel_messages: Vec<proto::ChannelMessageId>,
|
||||||
@ -635,6 +698,30 @@ pub struct RejoinedProject {
|
|||||||
pub language_servers: Vec<proto::LanguageServer>,
|
pub language_servers: Vec<proto::LanguageServer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RejoinedProject {
|
||||||
|
pub fn to_proto(&self) -> proto::RejoinedProject {
|
||||||
|
proto::RejoinedProject {
|
||||||
|
id: self.id.to_proto(),
|
||||||
|
worktrees: self
|
||||||
|
.worktrees
|
||||||
|
.iter()
|
||||||
|
.map(|worktree| proto::WorktreeMetadata {
|
||||||
|
id: worktree.id,
|
||||||
|
root_name: worktree.root_name.clone(),
|
||||||
|
visible: worktree.visible,
|
||||||
|
abs_path: worktree.abs_path.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
collaborators: self
|
||||||
|
.collaborators
|
||||||
|
.iter()
|
||||||
|
.map(|collaborator| collaborator.to_proto())
|
||||||
|
.collect(),
|
||||||
|
language_servers: self.language_servers.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RejoinedWorktree {
|
pub struct RejoinedWorktree {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
|
@ -84,6 +84,7 @@ id_type!(NotificationId);
|
|||||||
id_type!(NotificationKindId);
|
id_type!(NotificationKindId);
|
||||||
id_type!(ProjectCollaboratorId);
|
id_type!(ProjectCollaboratorId);
|
||||||
id_type!(ProjectId);
|
id_type!(ProjectId);
|
||||||
|
id_type!(RemoteProjectId);
|
||||||
id_type!(ReplicaId);
|
id_type!(ReplicaId);
|
||||||
id_type!(RoomId);
|
id_type!(RoomId);
|
||||||
id_type!(RoomParticipantId);
|
id_type!(RoomParticipantId);
|
||||||
@ -270,3 +271,18 @@ impl Into<i32> for ChannelVisibility {
|
|||||||
proto.into()
|
proto.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, PartialEq)]
|
||||||
|
pub enum PrincipalId {
|
||||||
|
UserId(UserId),
|
||||||
|
DevServerId(DevServerId),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indicate whether a [Buffer] has permissions to edit.
|
||||||
|
#[derive(PartialEq, Clone, Copy, Debug)]
|
||||||
|
pub enum Capability {
|
||||||
|
/// The buffer is a mutable replica.
|
||||||
|
ReadWrite,
|
||||||
|
/// The buffer is a read-only replica.
|
||||||
|
ReadOnly,
|
||||||
|
}
|
||||||
|
@ -12,6 +12,7 @@ pub mod messages;
|
|||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
pub mod projects;
|
pub mod projects;
|
||||||
pub mod rate_buckets;
|
pub mod rate_buckets;
|
||||||
|
pub mod remote_projects;
|
||||||
pub mod rooms;
|
pub mod rooms;
|
||||||
pub mod servers;
|
pub mod servers;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
@ -640,10 +640,15 @@ impl Database {
|
|||||||
.get_hosted_projects(&channel_ids, &roles_by_channel_id, tx)
|
.get_hosted_projects(&channel_ids, &roles_by_channel_id, tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let dev_servers = self.get_dev_servers(&channel_ids, tx).await?;
|
||||||
|
let remote_projects = self.get_remote_projects(&channel_ids, tx).await?;
|
||||||
|
|
||||||
Ok(ChannelsForUser {
|
Ok(ChannelsForUser {
|
||||||
channel_memberships,
|
channel_memberships,
|
||||||
channels,
|
channels,
|
||||||
hosted_projects,
|
hosted_projects,
|
||||||
|
dev_servers,
|
||||||
|
remote_projects,
|
||||||
channel_participants,
|
channel_participants,
|
||||||
latest_buffer_versions,
|
latest_buffer_versions,
|
||||||
latest_channel_messages,
|
latest_channel_messages,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use sea_orm::EntityTrait;
|
use sea_orm::{ActiveValue, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter};
|
||||||
|
|
||||||
use super::{dev_server, Database, DevServerId};
|
use super::{channel, dev_server, ChannelId, Database, DevServerId, UserId};
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub async fn get_dev_server(
|
pub async fn get_dev_server(
|
||||||
@ -15,4 +15,42 @@ impl Database {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_dev_servers(
|
||||||
|
&self,
|
||||||
|
channel_ids: &Vec<ChannelId>,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
) -> crate::Result<Vec<dev_server::Model>> {
|
||||||
|
let servers = dev_server::Entity::find()
|
||||||
|
.filter(dev_server::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
|
||||||
|
.all(tx)
|
||||||
|
.await?;
|
||||||
|
Ok(servers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_dev_server(
|
||||||
|
&self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
name: &str,
|
||||||
|
hashed_access_token: &str,
|
||||||
|
user_id: UserId,
|
||||||
|
) -> crate::Result<(channel::Model, dev_server::Model)> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||||
|
self.check_user_is_channel_admin(&channel, user_id, &tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let dev_server = dev_server::Entity::insert(dev_server::ActiveModel {
|
||||||
|
id: ActiveValue::NotSet,
|
||||||
|
hashed_token: ActiveValue::Set(hashed_access_token.to_string()),
|
||||||
|
channel_id: ActiveValue::Set(channel_id),
|
||||||
|
name: ActiveValue::Set(name.to_string()),
|
||||||
|
})
|
||||||
|
.exec_with_returning(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((channel, dev_server))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
@ -28,7 +30,7 @@ impl Database {
|
|||||||
room_id: RoomId,
|
room_id: RoomId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
worktrees: &[proto::WorktreeMetadata],
|
worktrees: &[proto::WorktreeMetadata],
|
||||||
) -> Result<RoomGuard<(ProjectId, proto::Room)>> {
|
) -> Result<TransactionGuard<(ProjectId, proto::Room)>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
let participant = room_participant::Entity::find()
|
let participant = room_participant::Entity::find()
|
||||||
.filter(
|
.filter(
|
||||||
@ -65,6 +67,7 @@ impl Database {
|
|||||||
))),
|
))),
|
||||||
id: ActiveValue::NotSet,
|
id: ActiveValue::NotSet,
|
||||||
hosted_project_id: ActiveValue::Set(None),
|
hosted_project_id: ActiveValue::Set(None),
|
||||||
|
remote_project_id: ActiveValue::Set(None),
|
||||||
}
|
}
|
||||||
.insert(&*tx)
|
.insert(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
@ -108,20 +111,22 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
|
) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
self.project_transaction(project_id, |tx| async move {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
|
||||||
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||||
|
|
||||||
let project = project::Entity::find_by_id(project_id)
|
let project = project::Entity::find_by_id(project_id)
|
||||||
.one(&*tx)
|
.one(&*tx)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("project not found"))?;
|
.ok_or_else(|| anyhow!("project not found"))?;
|
||||||
if project.host_connection()? == connection {
|
if project.host_connection()? == connection {
|
||||||
|
let room = if let Some(room_id) = project.room_id {
|
||||||
|
Some(self.get_room(room_id, &tx).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
project::Entity::delete(project.into_active_model())
|
project::Entity::delete(project.into_active_model())
|
||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
let room = self.get_room(room_id, &tx).await?;
|
|
||||||
Ok((room, guest_connection_ids))
|
Ok((room, guest_connection_ids))
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("cannot unshare a project hosted by another user"))?
|
Err(anyhow!("cannot unshare a project hosted by another user"))?
|
||||||
@ -136,9 +141,8 @@ impl Database {
|
|||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
worktrees: &[proto::WorktreeMetadata],
|
worktrees: &[proto::WorktreeMetadata],
|
||||||
) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
|
) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
self.project_transaction(project_id, |tx| async move {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
|
||||||
let project = project::Entity::find_by_id(project_id)
|
let project = project::Entity::find_by_id(project_id)
|
||||||
.filter(
|
.filter(
|
||||||
Condition::all()
|
Condition::all()
|
||||||
@ -154,12 +158,14 @@ impl Database {
|
|||||||
self.update_project_worktrees(project.id, worktrees, &tx)
|
self.update_project_worktrees(project.id, worktrees, &tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let room_id = project
|
|
||||||
.room_id
|
|
||||||
.ok_or_else(|| anyhow!("project not in a room"))?;
|
|
||||||
|
|
||||||
let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
|
let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
|
||||||
let room = self.get_room(room_id, &tx).await?;
|
|
||||||
|
let room = if let Some(room_id) = project.room_id {
|
||||||
|
Some(self.get_room(room_id, &tx).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Ok((room, guest_connection_ids))
|
Ok((room, guest_connection_ids))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@ -204,11 +210,10 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
update: &proto::UpdateWorktree,
|
update: &proto::UpdateWorktree,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> Result<RoomGuard<Vec<ConnectionId>>> {
|
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
|
||||||
let project_id = ProjectId::from_proto(update.project_id);
|
let project_id = ProjectId::from_proto(update.project_id);
|
||||||
let worktree_id = update.worktree_id as i64;
|
let worktree_id = update.worktree_id as i64;
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
self.project_transaction(project_id, |tx| async move {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
|
||||||
// Ensure the update comes from the host.
|
// Ensure the update comes from the host.
|
||||||
let _project = project::Entity::find_by_id(project_id)
|
let _project = project::Entity::find_by_id(project_id)
|
||||||
.filter(
|
.filter(
|
||||||
@ -360,11 +365,10 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
update: &proto::UpdateDiagnosticSummary,
|
update: &proto::UpdateDiagnosticSummary,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> Result<RoomGuard<Vec<ConnectionId>>> {
|
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
|
||||||
let project_id = ProjectId::from_proto(update.project_id);
|
let project_id = ProjectId::from_proto(update.project_id);
|
||||||
let worktree_id = update.worktree_id as i64;
|
let worktree_id = update.worktree_id as i64;
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
self.project_transaction(project_id, |tx| async move {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
|
||||||
let summary = update
|
let summary = update
|
||||||
.summary
|
.summary
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@ -415,10 +419,9 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
update: &proto::StartLanguageServer,
|
update: &proto::StartLanguageServer,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> Result<RoomGuard<Vec<ConnectionId>>> {
|
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
|
||||||
let project_id = ProjectId::from_proto(update.project_id);
|
let project_id = ProjectId::from_proto(update.project_id);
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
self.project_transaction(project_id, |tx| async move {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
|
||||||
let server = update
|
let server = update
|
||||||
.server
|
.server
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@ -461,10 +464,9 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
update: &proto::UpdateWorktreeSettings,
|
update: &proto::UpdateWorktreeSettings,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> Result<RoomGuard<Vec<ConnectionId>>> {
|
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
|
||||||
let project_id = ProjectId::from_proto(update.project_id);
|
let project_id = ProjectId::from_proto(update.project_id);
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
self.project_transaction(project_id, |tx| async move {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
|
||||||
// Ensure the update comes from the host.
|
// Ensure the update comes from the host.
|
||||||
let project = project::Entity::find_by_id(project_id)
|
let project = project::Entity::find_by_id(project_id)
|
||||||
.one(&*tx)
|
.one(&*tx)
|
||||||
@ -542,46 +544,36 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_project(&self, id: ProjectId) -> Result<project::Model> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
Ok(project::Entity::find_by_id(id)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no such project"))?)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Adds the given connection to the specified project
|
/// Adds the given connection to the specified project
|
||||||
/// in the current room.
|
/// in the current room.
|
||||||
pub async fn join_project_in_room(
|
pub async fn join_project(
|
||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> Result<RoomGuard<(Project, ReplicaId)>> {
|
user_id: UserId,
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
) -> Result<TransactionGuard<(Project, ReplicaId)>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.project_transaction(project_id, |tx| async move {
|
||||||
let participant = room_participant::Entity::find()
|
let (project, role) = self
|
||||||
.filter(
|
.access_project(
|
||||||
Condition::all()
|
project_id,
|
||||||
.add(
|
connection,
|
||||||
room_participant::Column::AnsweringConnectionId
|
PrincipalId::UserId(user_id),
|
||||||
.eq(connection.id as i32),
|
Capability::ReadOnly,
|
||||||
)
|
&tx,
|
||||||
.add(
|
|
||||||
room_participant::Column::AnsweringConnectionServerId
|
|
||||||
.eq(connection.owner_id as i32),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.one(&*tx)
|
.await?;
|
||||||
.await?
|
self.join_project_internal(project, user_id, connection, role, &tx)
|
||||||
.ok_or_else(|| anyhow!("must join a room first"))?;
|
.await
|
||||||
|
|
||||||
let project = project::Entity::find_by_id(project_id)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("no such project"))?;
|
|
||||||
if project.room_id != Some(participant.room_id) {
|
|
||||||
return Err(anyhow!("no such project"))?;
|
|
||||||
}
|
|
||||||
self.join_project_internal(
|
|
||||||
project,
|
|
||||||
participant.user_id,
|
|
||||||
connection,
|
|
||||||
participant.role.unwrap_or(ChannelRole::Member),
|
|
||||||
&tx,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@ -814,9 +806,8 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> Result<RoomGuard<(proto::Room, LeftProject)>> {
|
) -> Result<TransactionGuard<(Option<proto::Room>, LeftProject)>> {
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
self.project_transaction(project_id, |tx| async move {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
|
||||||
let result = project_collaborator::Entity::delete_many()
|
let result = project_collaborator::Entity::delete_many()
|
||||||
.filter(
|
.filter(
|
||||||
Condition::all()
|
Condition::all()
|
||||||
@ -871,7 +862,12 @@ impl Database {
|
|||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let room = self.get_room(room_id, &tx).await?;
|
let room = if let Some(room_id) = project.room_id {
|
||||||
|
Some(self.get_room(room_id, &tx).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let left_project = LeftProject {
|
let left_project = LeftProject {
|
||||||
id: project_id,
|
id: project_id,
|
||||||
host_user_id: project.host_user_id,
|
host_user_id: project.host_user_id,
|
||||||
@ -888,17 +884,15 @@ impl Database {
|
|||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection_id: ConnectionId,
|
connection_id: ConnectionId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
self.project_transaction(project_id, |tx| async move {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
project::Entity::find()
|
||||||
project_collaborator::Entity::find()
|
|
||||||
.filter(
|
.filter(
|
||||||
Condition::all()
|
Condition::all()
|
||||||
.add(project_collaborator::Column::ProjectId.eq(project_id))
|
.add(project::Column::Id.eq(project_id))
|
||||||
.add(project_collaborator::Column::IsHost.eq(true))
|
.add(project::Column::HostConnectionId.eq(Some(connection_id.id as i32)))
|
||||||
.add(project_collaborator::Column::ConnectionId.eq(connection_id.id))
|
|
||||||
.add(
|
.add(
|
||||||
project_collaborator::Column::ConnectionServerId
|
project::Column::HostConnectionServerId
|
||||||
.eq(connection_id.owner_id),
|
.eq(Some(connection_id.owner_id as i32)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.one(&*tx)
|
.one(&*tx)
|
||||||
@ -911,39 +905,90 @@ impl Database {
|
|||||||
.map(|guard| guard.into_inner())
|
.map(|guard| guard.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the current project if the given user is authorized to access it with the specified capability.
|
||||||
|
pub async fn access_project(
|
||||||
|
&self,
|
||||||
|
project_id: ProjectId,
|
||||||
|
connection_id: ConnectionId,
|
||||||
|
principal_id: PrincipalId,
|
||||||
|
capability: Capability,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
) -> Result<(project::Model, ChannelRole)> {
|
||||||
|
let (project, remote_project) = project::Entity::find_by_id(project_id)
|
||||||
|
.find_also_related(remote_project::Entity)
|
||||||
|
.one(tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no such project"))?;
|
||||||
|
|
||||||
|
let user_id = match principal_id {
|
||||||
|
PrincipalId::DevServerId(_) => {
|
||||||
|
if project
|
||||||
|
.host_connection()
|
||||||
|
.is_ok_and(|connection| connection == connection_id)
|
||||||
|
{
|
||||||
|
return Ok((project, ChannelRole::Admin));
|
||||||
|
}
|
||||||
|
return Err(anyhow!("not the project host"))?;
|
||||||
|
}
|
||||||
|
PrincipalId::UserId(user_id) => user_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let role = if let Some(remote_project) = remote_project {
|
||||||
|
let channel = channel::Entity::find_by_id(remote_project.channel_id)
|
||||||
|
.one(tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no such channel"))?;
|
||||||
|
|
||||||
|
self.check_user_is_channel_participant(&channel, user_id, &tx)
|
||||||
|
.await?
|
||||||
|
} else if let Some(room_id) = project.room_id {
|
||||||
|
// what's the users role?
|
||||||
|
let current_participant = room_participant::Entity::find()
|
||||||
|
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||||
|
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
|
||||||
|
.one(tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no such room"))?;
|
||||||
|
|
||||||
|
current_participant.role.unwrap_or(ChannelRole::Guest)
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!("not authorized to read projects"))?;
|
||||||
|
};
|
||||||
|
|
||||||
|
match capability {
|
||||||
|
Capability::ReadWrite => {
|
||||||
|
if !role.can_edit_projects() {
|
||||||
|
return Err(anyhow!("not authorized to edit projects"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Capability::ReadOnly => {
|
||||||
|
if !role.can_read_projects() {
|
||||||
|
return Err(anyhow!("not authorized to read projects"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((project, role))
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the host connection for a read-only request to join a shared project.
|
/// Returns the host connection for a read-only request to join a shared project.
|
||||||
pub async fn host_for_read_only_project_request(
|
pub async fn host_for_read_only_project_request(
|
||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection_id: ConnectionId,
|
connection_id: ConnectionId,
|
||||||
|
user_id: UserId,
|
||||||
) -> Result<ConnectionId> {
|
) -> Result<ConnectionId> {
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
self.project_transaction(project_id, |tx| async move {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
let (project, _) = self
|
||||||
let current_participant = room_participant::Entity::find()
|
.access_project(
|
||||||
.filter(room_participant::Column::RoomId.eq(room_id))
|
project_id,
|
||||||
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
|
connection_id,
|
||||||
.one(&*tx)
|
PrincipalId::UserId(user_id),
|
||||||
.await?
|
Capability::ReadOnly,
|
||||||
.ok_or_else(|| anyhow!("no such room"))?;
|
&tx,
|
||||||
|
|
||||||
if !current_participant
|
|
||||||
.role
|
|
||||||
.map_or(false, |role| role.can_read_projects())
|
|
||||||
{
|
|
||||||
Err(anyhow!("not authorized to read projects"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let host = project_collaborator::Entity::find()
|
|
||||||
.filter(
|
|
||||||
project_collaborator::Column::ProjectId
|
|
||||||
.eq(project_id)
|
|
||||||
.and(project_collaborator::Column::IsHost.eq(true)),
|
|
||||||
)
|
)
|
||||||
.one(&*tx)
|
.await?;
|
||||||
.await?
|
project.host_connection()
|
||||||
.ok_or_else(|| anyhow!("failed to read project host"))?;
|
|
||||||
|
|
||||||
Ok(host.connection())
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map(|guard| guard.into_inner())
|
.map(|guard| guard.into_inner())
|
||||||
@ -954,83 +999,56 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection_id: ConnectionId,
|
connection_id: ConnectionId,
|
||||||
|
user_id: UserId,
|
||||||
) -> Result<ConnectionId> {
|
) -> Result<ConnectionId> {
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
self.project_transaction(project_id, |tx| async move {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
let (project, _) = self
|
||||||
let current_participant = room_participant::Entity::find()
|
.access_project(
|
||||||
.filter(room_participant::Column::RoomId.eq(room_id))
|
project_id,
|
||||||
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
|
connection_id,
|
||||||
.one(&*tx)
|
PrincipalId::UserId(user_id),
|
||||||
.await?
|
Capability::ReadWrite,
|
||||||
.ok_or_else(|| anyhow!("no such room"))?;
|
&tx,
|
||||||
|
|
||||||
if !current_participant
|
|
||||||
.role
|
|
||||||
.map_or(false, |role| role.can_edit_projects())
|
|
||||||
{
|
|
||||||
Err(anyhow!("not authorized to edit projects"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let host = project_collaborator::Entity::find()
|
|
||||||
.filter(
|
|
||||||
project_collaborator::Column::ProjectId
|
|
||||||
.eq(project_id)
|
|
||||||
.and(project_collaborator::Column::IsHost.eq(true)),
|
|
||||||
)
|
)
|
||||||
.one(&*tx)
|
.await?;
|
||||||
.await?
|
project.host_connection()
|
||||||
.ok_or_else(|| anyhow!("failed to read project host"))?;
|
|
||||||
|
|
||||||
Ok(host.connection())
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map(|guard| guard.into_inner())
|
.map(|guard| guard.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn project_collaborators_for_buffer_update(
|
pub async fn connections_for_buffer_update(
|
||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
|
principal_id: PrincipalId,
|
||||||
connection_id: ConnectionId,
|
connection_id: ConnectionId,
|
||||||
requires_write: bool,
|
capability: Capability,
|
||||||
) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
|
) -> Result<TransactionGuard<(ConnectionId, Vec<ConnectionId>)>> {
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
self.project_transaction(project_id, |tx| async move {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
// Authorize
|
||||||
let current_participant = room_participant::Entity::find()
|
let (project, _) = self
|
||||||
.filter(room_participant::Column::RoomId.eq(room_id))
|
.access_project(project_id, connection_id, principal_id, capability, &tx)
|
||||||
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
|
.await?;
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("no such room"))?;
|
|
||||||
|
|
||||||
if requires_write
|
let host_connection_id = project.host_connection()?;
|
||||||
&& !current_participant
|
|
||||||
.role
|
|
||||||
.map_or(false, |role| role.can_edit_projects())
|
|
||||||
{
|
|
||||||
Err(anyhow!("not authorized to edit projects"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let collaborators = project_collaborator::Entity::find()
|
let collaborators = project_collaborator::Entity::find()
|
||||||
.filter(project_collaborator::Column::ProjectId.eq(project_id))
|
.filter(project_collaborator::Column::ProjectId.eq(project_id))
|
||||||
.all(&*tx)
|
.all(&*tx)
|
||||||
.await?
|
.await?;
|
||||||
.into_iter()
|
|
||||||
.map(|collaborator| ProjectCollaborator {
|
|
||||||
connection_id: collaborator.connection(),
|
|
||||||
user_id: collaborator.user_id,
|
|
||||||
replica_id: collaborator.replica_id,
|
|
||||||
is_host: collaborator.is_host,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if collaborators
|
let guest_connection_ids = collaborators
|
||||||
.iter()
|
.into_iter()
|
||||||
.any(|collaborator| collaborator.connection_id == connection_id)
|
.filter_map(|collaborator| {
|
||||||
{
|
if collaborator.is_host {
|
||||||
Ok(collaborators)
|
None
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("no such project"))?
|
Some(collaborator.connection())
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok((host_connection_id, guest_connection_ids))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@ -1043,24 +1061,39 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection_id: ConnectionId,
|
connection_id: ConnectionId,
|
||||||
) -> Result<RoomGuard<HashSet<ConnectionId>>> {
|
exclude_dev_server: bool,
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
) -> Result<TransactionGuard<HashSet<ConnectionId>>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.project_transaction(project_id, |tx| async move {
|
||||||
|
let project = project::Entity::find_by_id(project_id)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no such project"))?;
|
||||||
|
|
||||||
let mut collaborators = project_collaborator::Entity::find()
|
let mut collaborators = project_collaborator::Entity::find()
|
||||||
.filter(project_collaborator::Column::ProjectId.eq(project_id))
|
.filter(project_collaborator::Column::ProjectId.eq(project_id))
|
||||||
.stream(&*tx)
|
.stream(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut connection_ids = HashSet::default();
|
let mut connection_ids = HashSet::default();
|
||||||
|
if let Some(host_connection) = project.host_connection().log_err() {
|
||||||
|
if !exclude_dev_server {
|
||||||
|
connection_ids.insert(host_connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while let Some(collaborator) = collaborators.next().await {
|
while let Some(collaborator) = collaborators.next().await {
|
||||||
let collaborator = collaborator?;
|
let collaborator = collaborator?;
|
||||||
connection_ids.insert(collaborator.connection());
|
connection_ids.insert(collaborator.connection());
|
||||||
}
|
}
|
||||||
|
|
||||||
if connection_ids.contains(&connection_id) {
|
if connection_ids.contains(&connection_id)
|
||||||
|
|| Some(connection_id) == project.host_connection().ok()
|
||||||
|
{
|
||||||
Ok(connection_ids)
|
Ok(connection_ids)
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("no such project"))?
|
Err(anyhow!(
|
||||||
|
"can only send project updates to a project you're in"
|
||||||
|
))?
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@ -1089,15 +1122,12 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the [`RoomId`] for the given project.
|
/// Returns the [`RoomId`] for the given project.
|
||||||
pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> {
|
pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result<Option<RoomId>> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
let project = project::Entity::find_by_id(project_id)
|
Ok(project::Entity::find_by_id(project_id)
|
||||||
.one(&*tx)
|
.one(&*tx)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("project {} not found", project_id))?;
|
.and_then(|project| project.room_id))
|
||||||
Ok(project
|
|
||||||
.room_id
|
|
||||||
.ok_or_else(|| anyhow!("project not in room"))?)
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@ -1142,7 +1172,7 @@ impl Database {
|
|||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
leader_connection: ConnectionId,
|
leader_connection: ConnectionId,
|
||||||
follower_connection: ConnectionId,
|
follower_connection: ConnectionId,
|
||||||
) -> Result<RoomGuard<proto::Room>> {
|
) -> Result<TransactionGuard<proto::Room>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
follower::ActiveModel {
|
follower::ActiveModel {
|
||||||
room_id: ActiveValue::set(room_id),
|
room_id: ActiveValue::set(room_id),
|
||||||
@ -1173,7 +1203,7 @@ impl Database {
|
|||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
leader_connection: ConnectionId,
|
leader_connection: ConnectionId,
|
||||||
follower_connection: ConnectionId,
|
follower_connection: ConnectionId,
|
||||||
) -> Result<RoomGuard<proto::Room>> {
|
) -> Result<TransactionGuard<proto::Room>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
follower::Entity::delete_many()
|
follower::Entity::delete_many()
|
||||||
.filter(
|
.filter(
|
||||||
|
261
crates/collab/src/db/queries/remote_projects.rs
Normal file
261
crates/collab/src/db/queries/remote_projects.rs
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
use anyhow::anyhow;
|
||||||
|
use rpc::{proto, ConnectionId};
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
|
||||||
|
ModelTrait, QueryFilter,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::db::ProjectId;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
channel, project, project_collaborator, remote_project, worktree, ChannelId, Database,
|
||||||
|
DevServerId, RejoinedProject, RemoteProjectId, ResharedProject, ServerId, UserId,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub async fn get_remote_project(
|
||||||
|
&self,
|
||||||
|
remote_project_id: RemoteProjectId,
|
||||||
|
) -> crate::Result<remote_project::Model> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
Ok(remote_project::Entity::find_by_id(remote_project_id)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no remote project with id {}", remote_project_id))?)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_remote_projects(
|
||||||
|
&self,
|
||||||
|
channel_ids: &Vec<ChannelId>,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
) -> crate::Result<Vec<proto::RemoteProject>> {
|
||||||
|
let servers = remote_project::Entity::find()
|
||||||
|
.filter(remote_project::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
|
||||||
|
.find_also_related(project::Entity)
|
||||||
|
.all(tx)
|
||||||
|
.await?;
|
||||||
|
Ok(servers
|
||||||
|
.into_iter()
|
||||||
|
.map(|(remote_project, project)| proto::RemoteProject {
|
||||||
|
id: remote_project.id.to_proto(),
|
||||||
|
project_id: project.map(|p| p.id.to_proto()),
|
||||||
|
channel_id: remote_project.channel_id.to_proto(),
|
||||||
|
name: remote_project.name,
|
||||||
|
dev_server_id: remote_project.dev_server_id.to_proto(),
|
||||||
|
path: remote_project.path,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_remote_projects_for_dev_server(
|
||||||
|
&self,
|
||||||
|
dev_server_id: DevServerId,
|
||||||
|
) -> crate::Result<Vec<proto::RemoteProject>> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
let servers = remote_project::Entity::find()
|
||||||
|
.filter(remote_project::Column::DevServerId.eq(dev_server_id))
|
||||||
|
.find_also_related(project::Entity)
|
||||||
|
.all(&*tx)
|
||||||
|
.await?;
|
||||||
|
Ok(servers
|
||||||
|
.into_iter()
|
||||||
|
.map(|(remote_project, project)| proto::RemoteProject {
|
||||||
|
id: remote_project.id.to_proto(),
|
||||||
|
project_id: project.map(|p| p.id.to_proto()),
|
||||||
|
channel_id: remote_project.channel_id.to_proto(),
|
||||||
|
name: remote_project.name,
|
||||||
|
dev_server_id: remote_project.dev_server_id.to_proto(),
|
||||||
|
path: remote_project.path,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_stale_dev_server_projects(
|
||||||
|
&self,
|
||||||
|
connection: ConnectionId,
|
||||||
|
) -> crate::Result<Vec<ProjectId>> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
let projects = project::Entity::find()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(project::Column::HostConnectionId.eq(connection.id))
|
||||||
|
.add(project::Column::HostConnectionServerId.eq(connection.owner_id)),
|
||||||
|
)
|
||||||
|
.all(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(projects.into_iter().map(|p| p.id).collect())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_remote_project(
|
||||||
|
&self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
dev_server_id: DevServerId,
|
||||||
|
name: &str,
|
||||||
|
path: &str,
|
||||||
|
user_id: UserId,
|
||||||
|
) -> crate::Result<(channel::Model, remote_project::Model)> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||||
|
self.check_user_is_channel_admin(&channel, user_id, &tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let project = remote_project::Entity::insert(remote_project::ActiveModel {
|
||||||
|
name: ActiveValue::Set(name.to_string()),
|
||||||
|
id: ActiveValue::NotSet,
|
||||||
|
channel_id: ActiveValue::Set(channel_id),
|
||||||
|
dev_server_id: ActiveValue::Set(dev_server_id),
|
||||||
|
path: ActiveValue::Set(path.to_string()),
|
||||||
|
})
|
||||||
|
.exec_with_returning(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((channel, project))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn share_remote_project(
|
||||||
|
&self,
|
||||||
|
remote_project_id: RemoteProjectId,
|
||||||
|
dev_server_id: DevServerId,
|
||||||
|
connection: ConnectionId,
|
||||||
|
worktrees: &[proto::WorktreeMetadata],
|
||||||
|
) -> crate::Result<proto::RemoteProject> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
let remote_project = remote_project::Entity::find_by_id(remote_project_id)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no remote project with id {}", remote_project_id))?;
|
||||||
|
|
||||||
|
if remote_project.dev_server_id != dev_server_id {
|
||||||
|
return Err(anyhow!("remote project shared from wrong server"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let project = project::ActiveModel {
|
||||||
|
room_id: ActiveValue::Set(None),
|
||||||
|
host_user_id: ActiveValue::Set(None),
|
||||||
|
host_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||||
|
host_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||||
|
connection.owner_id as i32,
|
||||||
|
))),
|
||||||
|
id: ActiveValue::NotSet,
|
||||||
|
hosted_project_id: ActiveValue::Set(None),
|
||||||
|
remote_project_id: ActiveValue::Set(Some(remote_project_id)),
|
||||||
|
}
|
||||||
|
.insert(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !worktrees.is_empty() {
|
||||||
|
worktree::Entity::insert_many(worktrees.iter().map(|worktree| {
|
||||||
|
worktree::ActiveModel {
|
||||||
|
id: ActiveValue::set(worktree.id as i64),
|
||||||
|
project_id: ActiveValue::set(project.id),
|
||||||
|
abs_path: ActiveValue::set(worktree.abs_path.clone()),
|
||||||
|
root_name: ActiveValue::set(worktree.root_name.clone()),
|
||||||
|
visible: ActiveValue::set(worktree.visible),
|
||||||
|
scan_id: ActiveValue::set(0),
|
||||||
|
completed_scan_id: ActiveValue::set(0),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(remote_project.to_proto(Some(project)))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reshare_remote_projects(
|
||||||
|
&self,
|
||||||
|
reshared_projects: &Vec<proto::UpdateProject>,
|
||||||
|
dev_server_id: DevServerId,
|
||||||
|
connection: ConnectionId,
|
||||||
|
) -> crate::Result<Vec<ResharedProject>> {
|
||||||
|
// todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
let mut ret = Vec::new();
|
||||||
|
for reshared_project in reshared_projects {
|
||||||
|
let project_id = ProjectId::from_proto(reshared_project.project_id);
|
||||||
|
let (project, remote_project) = project::Entity::find_by_id(project_id)
|
||||||
|
.find_also_related(remote_project::Entity)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("project does not exist"))?;
|
||||||
|
|
||||||
|
if remote_project.map(|rp| rp.dev_server_id) != Some(dev_server_id) {
|
||||||
|
return Err(anyhow!("remote project reshared from wrong server"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(old_connection_id) = project.host_connection() else {
|
||||||
|
return Err(anyhow!("remote project was not shared"))?;
|
||||||
|
};
|
||||||
|
|
||||||
|
project::Entity::update(project::ActiveModel {
|
||||||
|
id: ActiveValue::set(project_id),
|
||||||
|
host_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||||
|
host_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||||
|
connection.owner_id as i32,
|
||||||
|
))),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let collaborators = project
|
||||||
|
.find_related(project_collaborator::Entity)
|
||||||
|
.all(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
ret.push(super::ResharedProject {
|
||||||
|
id: project_id,
|
||||||
|
old_connection_id,
|
||||||
|
collaborators: collaborators
|
||||||
|
.iter()
|
||||||
|
.map(|collaborator| super::ProjectCollaborator {
|
||||||
|
connection_id: collaborator.connection(),
|
||||||
|
user_id: collaborator.user_id,
|
||||||
|
replica_id: collaborator.replica_id,
|
||||||
|
is_host: collaborator.is_host,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
worktrees: reshared_project.worktrees.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(ret)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn rejoin_remote_projects(
|
||||||
|
&self,
|
||||||
|
rejoined_projects: &Vec<proto::RejoinProject>,
|
||||||
|
user_id: UserId,
|
||||||
|
connection_id: ConnectionId,
|
||||||
|
) -> crate::Result<Vec<RejoinedProject>> {
|
||||||
|
// todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
let mut ret = Vec::new();
|
||||||
|
for rejoined_project in rejoined_projects {
|
||||||
|
if let Some(project) = self
|
||||||
|
.rejoin_project_internal(&tx, rejoined_project, user_id, connection_id)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
ret.push(project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ret)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
room_id: RoomId,
|
room_id: RoomId,
|
||||||
new_server_id: ServerId,
|
new_server_id: ServerId,
|
||||||
) -> Result<RoomGuard<RefreshedRoom>> {
|
) -> Result<TransactionGuard<RefreshedRoom>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
let stale_participant_filter = Condition::all()
|
let stale_participant_filter = Condition::all()
|
||||||
.add(room_participant::Column::RoomId.eq(room_id))
|
.add(room_participant::Column::RoomId.eq(room_id))
|
||||||
@ -149,7 +149,7 @@ impl Database {
|
|||||||
calling_connection: ConnectionId,
|
calling_connection: ConnectionId,
|
||||||
called_user_id: UserId,
|
called_user_id: UserId,
|
||||||
initial_project_id: Option<ProjectId>,
|
initial_project_id: Option<ProjectId>,
|
||||||
) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
|
) -> Result<TransactionGuard<(proto::Room, proto::IncomingCall)>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
let caller = room_participant::Entity::find()
|
let caller = room_participant::Entity::find()
|
||||||
.filter(
|
.filter(
|
||||||
@ -201,7 +201,7 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
room_id: RoomId,
|
room_id: RoomId,
|
||||||
called_user_id: UserId,
|
called_user_id: UserId,
|
||||||
) -> Result<RoomGuard<proto::Room>> {
|
) -> Result<TransactionGuard<proto::Room>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
room_participant::Entity::delete_many()
|
room_participant::Entity::delete_many()
|
||||||
.filter(
|
.filter(
|
||||||
@ -221,7 +221,7 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
expected_room_id: Option<RoomId>,
|
expected_room_id: Option<RoomId>,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
) -> Result<Option<RoomGuard<proto::Room>>> {
|
) -> Result<Option<TransactionGuard<proto::Room>>> {
|
||||||
self.optional_room_transaction(|tx| async move {
|
self.optional_room_transaction(|tx| async move {
|
||||||
let mut filter = Condition::all()
|
let mut filter = Condition::all()
|
||||||
.add(room_participant::Column::UserId.eq(user_id))
|
.add(room_participant::Column::UserId.eq(user_id))
|
||||||
@ -258,7 +258,7 @@ impl Database {
|
|||||||
room_id: RoomId,
|
room_id: RoomId,
|
||||||
calling_connection: ConnectionId,
|
calling_connection: ConnectionId,
|
||||||
called_user_id: UserId,
|
called_user_id: UserId,
|
||||||
) -> Result<RoomGuard<proto::Room>> {
|
) -> Result<TransactionGuard<proto::Room>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
let participant = room_participant::Entity::find()
|
let participant = room_participant::Entity::find()
|
||||||
.filter(
|
.filter(
|
||||||
@ -294,7 +294,7 @@ impl Database {
|
|||||||
room_id: RoomId,
|
room_id: RoomId,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> Result<RoomGuard<JoinRoom>> {
|
) -> Result<TransactionGuard<JoinRoom>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||||
enum QueryChannelId {
|
enum QueryChannelId {
|
||||||
@ -472,7 +472,7 @@ impl Database {
|
|||||||
rejoin_room: proto::RejoinRoom,
|
rejoin_room: proto::RejoinRoom,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> Result<RoomGuard<RejoinedRoom>> {
|
) -> Result<TransactionGuard<RejoinedRoom>> {
|
||||||
let room_id = RoomId::from_proto(rejoin_room.id);
|
let room_id = RoomId::from_proto(rejoin_room.id);
|
||||||
self.room_transaction(room_id, |tx| async {
|
self.room_transaction(room_id, |tx| async {
|
||||||
let tx = tx;
|
let tx = tx;
|
||||||
@ -572,180 +572,12 @@ impl Database {
|
|||||||
|
|
||||||
let mut rejoined_projects = Vec::new();
|
let mut rejoined_projects = Vec::new();
|
||||||
for rejoined_project in &rejoin_room.rejoined_projects {
|
for rejoined_project in &rejoin_room.rejoined_projects {
|
||||||
let project_id = ProjectId::from_proto(rejoined_project.id);
|
if let Some(rejoined_project) = self
|
||||||
let Some(project) = project::Entity::find_by_id(project_id).one(&*tx).await? else {
|
.rejoin_project_internal(&tx, rejoined_project, user_id, connection)
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut worktrees = Vec::new();
|
|
||||||
let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
|
|
||||||
for db_worktree in db_worktrees {
|
|
||||||
let mut worktree = RejoinedWorktree {
|
|
||||||
id: db_worktree.id as u64,
|
|
||||||
abs_path: db_worktree.abs_path,
|
|
||||||
root_name: db_worktree.root_name,
|
|
||||||
visible: db_worktree.visible,
|
|
||||||
updated_entries: Default::default(),
|
|
||||||
removed_entries: Default::default(),
|
|
||||||
updated_repositories: Default::default(),
|
|
||||||
removed_repositories: Default::default(),
|
|
||||||
diagnostic_summaries: Default::default(),
|
|
||||||
settings_files: Default::default(),
|
|
||||||
scan_id: db_worktree.scan_id as u64,
|
|
||||||
completed_scan_id: db_worktree.completed_scan_id as u64,
|
|
||||||
};
|
|
||||||
|
|
||||||
let rejoined_worktree = rejoined_project
|
|
||||||
.worktrees
|
|
||||||
.iter()
|
|
||||||
.find(|worktree| worktree.id == db_worktree.id as u64);
|
|
||||||
|
|
||||||
// File entries
|
|
||||||
{
|
|
||||||
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
|
|
||||||
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
|
|
||||||
} else {
|
|
||||||
worktree_entry::Column::IsDeleted.eq(false)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut db_entries = worktree_entry::Entity::find()
|
|
||||||
.filter(
|
|
||||||
Condition::all()
|
|
||||||
.add(worktree_entry::Column::ProjectId.eq(project.id))
|
|
||||||
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
|
|
||||||
.add(entry_filter),
|
|
||||||
)
|
|
||||||
.stream(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
while let Some(db_entry) = db_entries.next().await {
|
|
||||||
let db_entry = db_entry?;
|
|
||||||
if db_entry.is_deleted {
|
|
||||||
worktree.removed_entries.push(db_entry.id as u64);
|
|
||||||
} else {
|
|
||||||
worktree.updated_entries.push(proto::Entry {
|
|
||||||
id: db_entry.id as u64,
|
|
||||||
is_dir: db_entry.is_dir,
|
|
||||||
path: db_entry.path,
|
|
||||||
inode: db_entry.inode as u64,
|
|
||||||
mtime: Some(proto::Timestamp {
|
|
||||||
seconds: db_entry.mtime_seconds as u64,
|
|
||||||
nanos: db_entry.mtime_nanos as u32,
|
|
||||||
}),
|
|
||||||
is_symlink: db_entry.is_symlink,
|
|
||||||
is_ignored: db_entry.is_ignored,
|
|
||||||
is_external: db_entry.is_external,
|
|
||||||
git_status: db_entry.git_status.map(|status| status as i32),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repository Entries
|
|
||||||
{
|
|
||||||
let repository_entry_filter =
|
|
||||||
if let Some(rejoined_worktree) = rejoined_worktree {
|
|
||||||
worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
|
|
||||||
} else {
|
|
||||||
worktree_repository::Column::IsDeleted.eq(false)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut db_repositories = worktree_repository::Entity::find()
|
|
||||||
.filter(
|
|
||||||
Condition::all()
|
|
||||||
.add(worktree_repository::Column::ProjectId.eq(project.id))
|
|
||||||
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
|
|
||||||
.add(repository_entry_filter),
|
|
||||||
)
|
|
||||||
.stream(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
while let Some(db_repository) = db_repositories.next().await {
|
|
||||||
let db_repository = db_repository?;
|
|
||||||
if db_repository.is_deleted {
|
|
||||||
worktree
|
|
||||||
.removed_repositories
|
|
||||||
.push(db_repository.work_directory_id as u64);
|
|
||||||
} else {
|
|
||||||
worktree.updated_repositories.push(proto::RepositoryEntry {
|
|
||||||
work_directory_id: db_repository.work_directory_id as u64,
|
|
||||||
branch: db_repository.branch,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
worktrees.push(worktree);
|
|
||||||
}
|
|
||||||
|
|
||||||
let language_servers = project
|
|
||||||
.find_related(language_server::Entity)
|
|
||||||
.all(&*tx)
|
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
|
||||||
.map(|language_server| proto::LanguageServer {
|
|
||||||
id: language_server.id as u64,
|
|
||||||
name: language_server.name,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut db_settings_files = worktree_settings_file::Entity::find()
|
rejoined_projects.push(rejoined_project);
|
||||||
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
|
|
||||||
.stream(&*tx)
|
|
||||||
.await?;
|
|
||||||
while let Some(db_settings_file) = db_settings_files.next().await {
|
|
||||||
let db_settings_file = db_settings_file?;
|
|
||||||
if let Some(worktree) = worktrees
|
|
||||||
.iter_mut()
|
|
||||||
.find(|w| w.id == db_settings_file.worktree_id as u64)
|
|
||||||
{
|
|
||||||
worktree.settings_files.push(WorktreeSettingsFile {
|
|
||||||
path: db_settings_file.path,
|
|
||||||
content: db_settings_file.content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut collaborators = project
|
|
||||||
.find_related(project_collaborator::Entity)
|
|
||||||
.all(&*tx)
|
|
||||||
.await?;
|
|
||||||
let self_collaborator = if let Some(self_collaborator_ix) = collaborators
|
|
||||||
.iter()
|
|
||||||
.position(|collaborator| collaborator.user_id == user_id)
|
|
||||||
{
|
|
||||||
collaborators.swap_remove(self_collaborator_ix)
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let old_connection_id = self_collaborator.connection();
|
|
||||||
project_collaborator::Entity::update(project_collaborator::ActiveModel {
|
|
||||||
connection_id: ActiveValue::set(connection.id as i32),
|
|
||||||
connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
|
|
||||||
..self_collaborator.into_active_model()
|
|
||||||
})
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let collaborators = collaborators
|
|
||||||
.into_iter()
|
|
||||||
.map(|collaborator| ProjectCollaborator {
|
|
||||||
connection_id: collaborator.connection(),
|
|
||||||
user_id: collaborator.user_id,
|
|
||||||
replica_id: collaborator.replica_id,
|
|
||||||
is_host: collaborator.is_host,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
rejoined_projects.push(RejoinedProject {
|
|
||||||
id: project_id,
|
|
||||||
old_connection_id,
|
|
||||||
collaborators,
|
|
||||||
worktrees,
|
|
||||||
language_servers,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let (channel, room) = self.get_channel_room(room_id, &tx).await?;
|
let (channel, room) = self.get_channel_room(room_id, &tx).await?;
|
||||||
@ -760,10 +592,192 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn rejoin_project_internal(
|
||||||
|
&self,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
rejoined_project: &proto::RejoinProject,
|
||||||
|
user_id: UserId,
|
||||||
|
connection: ConnectionId,
|
||||||
|
) -> Result<Option<RejoinedProject>> {
|
||||||
|
let project_id = ProjectId::from_proto(rejoined_project.id);
|
||||||
|
let Some(project) = project::Entity::find_by_id(project_id).one(tx).await? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut worktrees = Vec::new();
|
||||||
|
let db_worktrees = project.find_related(worktree::Entity).all(tx).await?;
|
||||||
|
for db_worktree in db_worktrees {
|
||||||
|
let mut worktree = RejoinedWorktree {
|
||||||
|
id: db_worktree.id as u64,
|
||||||
|
abs_path: db_worktree.abs_path,
|
||||||
|
root_name: db_worktree.root_name,
|
||||||
|
visible: db_worktree.visible,
|
||||||
|
updated_entries: Default::default(),
|
||||||
|
removed_entries: Default::default(),
|
||||||
|
updated_repositories: Default::default(),
|
||||||
|
removed_repositories: Default::default(),
|
||||||
|
diagnostic_summaries: Default::default(),
|
||||||
|
settings_files: Default::default(),
|
||||||
|
scan_id: db_worktree.scan_id as u64,
|
||||||
|
completed_scan_id: db_worktree.completed_scan_id as u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
let rejoined_worktree = rejoined_project
|
||||||
|
.worktrees
|
||||||
|
.iter()
|
||||||
|
.find(|worktree| worktree.id == db_worktree.id as u64);
|
||||||
|
|
||||||
|
// File entries
|
||||||
|
{
|
||||||
|
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
|
||||||
|
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
|
||||||
|
} else {
|
||||||
|
worktree_entry::Column::IsDeleted.eq(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut db_entries = worktree_entry::Entity::find()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(worktree_entry::Column::ProjectId.eq(project.id))
|
||||||
|
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
|
||||||
|
.add(entry_filter),
|
||||||
|
)
|
||||||
|
.stream(tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
while let Some(db_entry) = db_entries.next().await {
|
||||||
|
let db_entry = db_entry?;
|
||||||
|
if db_entry.is_deleted {
|
||||||
|
worktree.removed_entries.push(db_entry.id as u64);
|
||||||
|
} else {
|
||||||
|
worktree.updated_entries.push(proto::Entry {
|
||||||
|
id: db_entry.id as u64,
|
||||||
|
is_dir: db_entry.is_dir,
|
||||||
|
path: db_entry.path,
|
||||||
|
inode: db_entry.inode as u64,
|
||||||
|
mtime: Some(proto::Timestamp {
|
||||||
|
seconds: db_entry.mtime_seconds as u64,
|
||||||
|
nanos: db_entry.mtime_nanos as u32,
|
||||||
|
}),
|
||||||
|
is_symlink: db_entry.is_symlink,
|
||||||
|
is_ignored: db_entry.is_ignored,
|
||||||
|
is_external: db_entry.is_external,
|
||||||
|
git_status: db_entry.git_status.map(|status| status as i32),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repository Entries
|
||||||
|
{
|
||||||
|
let repository_entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
|
||||||
|
worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
|
||||||
|
} else {
|
||||||
|
worktree_repository::Column::IsDeleted.eq(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut db_repositories = worktree_repository::Entity::find()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(worktree_repository::Column::ProjectId.eq(project.id))
|
||||||
|
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
|
||||||
|
.add(repository_entry_filter),
|
||||||
|
)
|
||||||
|
.stream(tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
while let Some(db_repository) = db_repositories.next().await {
|
||||||
|
let db_repository = db_repository?;
|
||||||
|
if db_repository.is_deleted {
|
||||||
|
worktree
|
||||||
|
.removed_repositories
|
||||||
|
.push(db_repository.work_directory_id as u64);
|
||||||
|
} else {
|
||||||
|
worktree.updated_repositories.push(proto::RepositoryEntry {
|
||||||
|
work_directory_id: db_repository.work_directory_id as u64,
|
||||||
|
branch: db_repository.branch,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
worktrees.push(worktree);
|
||||||
|
}
|
||||||
|
|
||||||
|
let language_servers = project
|
||||||
|
.find_related(language_server::Entity)
|
||||||
|
.all(tx)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|language_server| proto::LanguageServer {
|
||||||
|
id: language_server.id as u64,
|
||||||
|
name: language_server.name,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut db_settings_files = worktree_settings_file::Entity::find()
|
||||||
|
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
|
||||||
|
.stream(tx)
|
||||||
|
.await?;
|
||||||
|
while let Some(db_settings_file) = db_settings_files.next().await {
|
||||||
|
let db_settings_file = db_settings_file?;
|
||||||
|
if let Some(worktree) = worktrees
|
||||||
|
.iter_mut()
|
||||||
|
.find(|w| w.id == db_settings_file.worktree_id as u64)
|
||||||
|
{
|
||||||
|
worktree.settings_files.push(WorktreeSettingsFile {
|
||||||
|
path: db_settings_file.path,
|
||||||
|
content: db_settings_file.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut collaborators = project
|
||||||
|
.find_related(project_collaborator::Entity)
|
||||||
|
.all(tx)
|
||||||
|
.await?;
|
||||||
|
let self_collaborator = if let Some(self_collaborator_ix) = collaborators
|
||||||
|
.iter()
|
||||||
|
.position(|collaborator| collaborator.user_id == user_id)
|
||||||
|
{
|
||||||
|
collaborators.swap_remove(self_collaborator_ix)
|
||||||
|
} else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let old_connection_id = self_collaborator.connection();
|
||||||
|
project_collaborator::Entity::update(project_collaborator::ActiveModel {
|
||||||
|
connection_id: ActiveValue::set(connection.id as i32),
|
||||||
|
connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
|
||||||
|
..self_collaborator.into_active_model()
|
||||||
|
})
|
||||||
|
.exec(tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let collaborators = collaborators
|
||||||
|
.into_iter()
|
||||||
|
.map(|collaborator| ProjectCollaborator {
|
||||||
|
connection_id: collaborator.connection(),
|
||||||
|
user_id: collaborator.user_id,
|
||||||
|
replica_id: collaborator.replica_id,
|
||||||
|
is_host: collaborator.is_host,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
return Ok(Some(RejoinedProject {
|
||||||
|
id: project_id,
|
||||||
|
old_connection_id,
|
||||||
|
collaborators,
|
||||||
|
worktrees,
|
||||||
|
language_servers,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn leave_room(
|
pub async fn leave_room(
|
||||||
&self,
|
&self,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> Result<Option<RoomGuard<LeftRoom>>> {
|
) -> Result<Option<TransactionGuard<LeftRoom>>> {
|
||||||
self.optional_room_transaction(|tx| async move {
|
self.optional_room_transaction(|tx| async move {
|
||||||
let leaving_participant = room_participant::Entity::find()
|
let leaving_participant = room_participant::Entity::find()
|
||||||
.filter(
|
.filter(
|
||||||
@ -935,7 +949,7 @@ impl Database {
|
|||||||
room_id: RoomId,
|
room_id: RoomId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
location: proto::ParticipantLocation,
|
location: proto::ParticipantLocation,
|
||||||
) -> Result<RoomGuard<proto::Room>> {
|
) -> Result<TransactionGuard<proto::Room>> {
|
||||||
self.room_transaction(room_id, |tx| async {
|
self.room_transaction(room_id, |tx| async {
|
||||||
let tx = tx;
|
let tx = tx;
|
||||||
let location_kind;
|
let location_kind;
|
||||||
@ -997,7 +1011,7 @@ impl Database {
|
|||||||
room_id: RoomId,
|
room_id: RoomId,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
role: ChannelRole,
|
role: ChannelRole,
|
||||||
) -> Result<RoomGuard<proto::Room>> {
|
) -> Result<TransactionGuard<proto::Room>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
room_participant::Entity::find()
|
room_participant::Entity::find()
|
||||||
.filter(
|
.filter(
|
||||||
@ -1150,7 +1164,7 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
room_id: RoomId,
|
room_id: RoomId,
|
||||||
connection_id: ConnectionId,
|
connection_id: ConnectionId,
|
||||||
) -> Result<RoomGuard<HashSet<ConnectionId>>> {
|
) -> Result<TransactionGuard<HashSet<ConnectionId>>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
let mut participants = room_participant::Entity::find()
|
let mut participants = room_participant::Entity::find()
|
||||||
.filter(room_participant::Column::RoomId.eq(room_id))
|
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||||
|
@ -24,6 +24,7 @@ pub mod observed_channel_messages;
|
|||||||
pub mod project;
|
pub mod project;
|
||||||
pub mod project_collaborator;
|
pub mod project_collaborator;
|
||||||
pub mod rate_buckets;
|
pub mod rate_buckets;
|
||||||
|
pub mod remote_project;
|
||||||
pub mod room;
|
pub mod room;
|
||||||
pub mod room_participant;
|
pub mod room_participant;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use crate::db::{ChannelId, DevServerId};
|
use crate::db::{ChannelId, DevServerId};
|
||||||
|
use rpc::proto;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||||
@ -15,3 +16,14 @@ impl ActiveModelBehavior for ActiveModel {}
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {}
|
pub enum Relation {}
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub fn to_proto(&self, status: proto::DevServerStatus) -> proto::DevServer {
|
||||||
|
proto::DevServer {
|
||||||
|
dev_server_id: self.id.to_proto(),
|
||||||
|
channel_id: self.channel_id.to_proto(),
|
||||||
|
name: self.name.clone(),
|
||||||
|
status: status as i32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::db::{HostedProjectId, ProjectId, Result, RoomId, ServerId, UserId};
|
use crate::db::{HostedProjectId, ProjectId, RemoteProjectId, Result, RoomId, ServerId, UserId};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use rpc::ConnectionId;
|
use rpc::ConnectionId;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
@ -13,6 +13,7 @@ pub struct Model {
|
|||||||
pub host_connection_id: Option<i32>,
|
pub host_connection_id: Option<i32>,
|
||||||
pub host_connection_server_id: Option<ServerId>,
|
pub host_connection_server_id: Option<ServerId>,
|
||||||
pub hosted_project_id: Option<HostedProjectId>,
|
pub hosted_project_id: Option<HostedProjectId>,
|
||||||
|
pub remote_project_id: Option<RemoteProjectId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
@ -56,6 +57,12 @@ pub enum Relation {
|
|||||||
to = "super::hosted_project::Column::Id"
|
to = "super::hosted_project::Column::Id"
|
||||||
)]
|
)]
|
||||||
HostedProject,
|
HostedProject,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::remote_project::Entity",
|
||||||
|
from = "Column::RemoteProjectId",
|
||||||
|
to = "super::remote_project::Column::Id"
|
||||||
|
)]
|
||||||
|
RemoteProject,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Related<super::user::Entity> for Entity {
|
impl Related<super::user::Entity> for Entity {
|
||||||
@ -94,4 +101,10 @@ impl Related<super::hosted_project::Entity> for Entity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Related<super::remote_project::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::RemoteProject.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
42
crates/collab/src/db/tables/remote_project.rs
Normal file
42
crates/collab/src/db/tables/remote_project.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use super::project;
|
||||||
|
use crate::db::{ChannelId, DevServerId, RemoteProjectId};
|
||||||
|
use rpc::proto;
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||||
|
#[sea_orm(table_name = "remote_projects")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: RemoteProjectId,
|
||||||
|
pub channel_id: ChannelId,
|
||||||
|
pub dev_server_id: DevServerId,
|
||||||
|
pub name: String,
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_one = "super::project::Entity")]
|
||||||
|
Project,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::project::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Project.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub fn to_proto(&self, project: Option<project::Model>) -> proto::RemoteProject {
|
||||||
|
proto::RemoteProject {
|
||||||
|
id: self.id.to_proto(),
|
||||||
|
project_id: project.map(|p| p.id.to_proto()),
|
||||||
|
channel_id: self.channel_id.to_proto(),
|
||||||
|
dev_server_id: self.dev_server_id.to_proto(),
|
||||||
|
name: self.name.clone(),
|
||||||
|
path: self.path.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
use crate::db::{ChannelId, ChannelRole, UserId};
|
use crate::db::{ChannelId, ChannelRole, DevServerId, PrincipalId, UserId};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use collections::{BTreeMap, HashMap, HashSet};
|
use collections::{BTreeMap, HashMap, HashSet};
|
||||||
use rpc::ConnectionId;
|
use rpc::{proto, ConnectionId};
|
||||||
use semantic_version::SemanticVersion;
|
use semantic_version::SemanticVersion;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@ -10,12 +10,13 @@ use tracing::instrument;
|
|||||||
#[derive(Default, Serialize)]
|
#[derive(Default, Serialize)]
|
||||||
pub struct ConnectionPool {
|
pub struct ConnectionPool {
|
||||||
connections: BTreeMap<ConnectionId, Connection>,
|
connections: BTreeMap<ConnectionId, Connection>,
|
||||||
connected_users: BTreeMap<UserId, ConnectedUser>,
|
connected_users: BTreeMap<UserId, ConnectedPrincipal>,
|
||||||
|
connected_dev_servers: BTreeMap<DevServerId, ConnectionId>,
|
||||||
channels: ChannelPool,
|
channels: ChannelPool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize)]
|
#[derive(Default, Serialize)]
|
||||||
struct ConnectedUser {
|
struct ConnectedPrincipal {
|
||||||
connection_ids: HashSet<ConnectionId>,
|
connection_ids: HashSet<ConnectionId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ impl ZedVersion {
|
|||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct Connection {
|
pub struct Connection {
|
||||||
pub user_id: UserId,
|
pub principal_id: PrincipalId,
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub zed_version: ZedVersion,
|
pub zed_version: ZedVersion,
|
||||||
}
|
}
|
||||||
@ -59,7 +60,7 @@ impl ConnectionPool {
|
|||||||
self.connections.insert(
|
self.connections.insert(
|
||||||
connection_id,
|
connection_id,
|
||||||
Connection {
|
Connection {
|
||||||
user_id,
|
principal_id: PrincipalId::UserId(user_id),
|
||||||
admin,
|
admin,
|
||||||
zed_version,
|
zed_version,
|
||||||
},
|
},
|
||||||
@ -68,6 +69,25 @@ impl ConnectionPool {
|
|||||||
connected_user.connection_ids.insert(connection_id);
|
connected_user.connection_ids.insert(connection_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_dev_server(
|
||||||
|
&mut self,
|
||||||
|
connection_id: ConnectionId,
|
||||||
|
dev_server_id: DevServerId,
|
||||||
|
zed_version: ZedVersion,
|
||||||
|
) {
|
||||||
|
self.connections.insert(
|
||||||
|
connection_id,
|
||||||
|
Connection {
|
||||||
|
principal_id: PrincipalId::DevServerId(dev_server_id),
|
||||||
|
admin: false,
|
||||||
|
zed_version,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
self.connected_dev_servers
|
||||||
|
.insert(dev_server_id, connection_id);
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub fn remove_connection(&mut self, connection_id: ConnectionId) -> Result<()> {
|
pub fn remove_connection(&mut self, connection_id: ConnectionId) -> Result<()> {
|
||||||
let connection = self
|
let connection = self
|
||||||
@ -75,12 +95,18 @@ impl ConnectionPool {
|
|||||||
.get_mut(&connection_id)
|
.get_mut(&connection_id)
|
||||||
.ok_or_else(|| anyhow!("no such connection"))?;
|
.ok_or_else(|| anyhow!("no such connection"))?;
|
||||||
|
|
||||||
let user_id = connection.user_id;
|
match connection.principal_id {
|
||||||
let connected_user = self.connected_users.get_mut(&user_id).unwrap();
|
PrincipalId::UserId(user_id) => {
|
||||||
connected_user.connection_ids.remove(&connection_id);
|
let connected_user = self.connected_users.get_mut(&user_id).unwrap();
|
||||||
if connected_user.connection_ids.is_empty() {
|
connected_user.connection_ids.remove(&connection_id);
|
||||||
self.connected_users.remove(&user_id);
|
if connected_user.connection_ids.is_empty() {
|
||||||
self.channels.remove_user(&user_id);
|
self.connected_users.remove(&user_id);
|
||||||
|
self.channels.remove_user(&user_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PrincipalId::DevServerId(dev_server_id) => {
|
||||||
|
self.connected_dev_servers.remove(&dev_server_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.connections.remove(&connection_id).unwrap();
|
self.connections.remove(&connection_id).unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -110,6 +136,18 @@ impl ConnectionPool {
|
|||||||
.copied()
|
.copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dev_server_status(&self, dev_server_id: DevServerId) -> proto::DevServerStatus {
|
||||||
|
if self.dev_server_connection_id(dev_server_id).is_some() {
|
||||||
|
proto::DevServerStatus::Online
|
||||||
|
} else {
|
||||||
|
proto::DevServerStatus::Offline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dev_server_connection_id(&self, dev_server_id: DevServerId) -> Option<ConnectionId> {
|
||||||
|
self.connected_dev_servers.get(&dev_server_id).copied()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn channel_user_ids(
|
pub fn channel_user_ids(
|
||||||
&self,
|
&self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
@ -154,22 +192,39 @@ impl ConnectionPool {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn check_invariants(&self) {
|
pub fn check_invariants(&self) {
|
||||||
for (connection_id, connection) in &self.connections {
|
for (connection_id, connection) in &self.connections {
|
||||||
assert!(self
|
match &connection.principal_id {
|
||||||
.connected_users
|
PrincipalId::UserId(user_id) => {
|
||||||
.get(&connection.user_id)
|
assert!(self
|
||||||
.unwrap()
|
.connected_users
|
||||||
.connection_ids
|
.get(user_id)
|
||||||
.contains(connection_id));
|
.unwrap()
|
||||||
|
.connection_ids
|
||||||
|
.contains(connection_id));
|
||||||
|
}
|
||||||
|
PrincipalId::DevServerId(dev_server_id) => {
|
||||||
|
assert_eq!(
|
||||||
|
self.connected_dev_servers.get(&dev_server_id).unwrap(),
|
||||||
|
connection_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (user_id, state) in &self.connected_users {
|
for (user_id, state) in &self.connected_users {
|
||||||
for connection_id in &state.connection_ids {
|
for connection_id in &state.connection_ids {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
self.connections.get(connection_id).unwrap().user_id,
|
self.connections.get(connection_id).unwrap().principal_id,
|
||||||
*user_id
|
PrincipalId::UserId(*user_id)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (dev_server_id, connection_id) in &self.connected_dev_servers {
|
||||||
|
assert_eq!(
|
||||||
|
self.connections.get(connection_id).unwrap().principal_id,
|
||||||
|
PrincipalId::DevServerId(*dev_server_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ mod channel_buffer_tests;
|
|||||||
mod channel_guest_tests;
|
mod channel_guest_tests;
|
||||||
mod channel_message_tests;
|
mod channel_message_tests;
|
||||||
mod channel_tests;
|
mod channel_tests;
|
||||||
|
mod dev_server_tests;
|
||||||
mod editor_tests;
|
mod editor_tests;
|
||||||
mod following_tests;
|
mod following_tests;
|
||||||
mod integration_tests;
|
mod integration_tests;
|
||||||
|
110
crates/collab/src/tests/dev_server_tests.rs
Normal file
110
crates/collab/src/tests/dev_server_tests.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use editor::Editor;
|
||||||
|
use fs::Fs;
|
||||||
|
use gpui::VisualTestContext;
|
||||||
|
use rpc::proto::DevServerStatus;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::tests::TestServer;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
|
||||||
|
let (server, client) = TestServer::start1(cx).await;
|
||||||
|
|
||||||
|
let channel_id = server
|
||||||
|
.make_channel("test", None, (&client, cx), &mut [])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.channel_store()
|
||||||
|
.update(cx, |store, cx| {
|
||||||
|
store.create_dev_server(channel_id, "server-1".to_string(), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
client.channel_store().update(cx, |store, _| {
|
||||||
|
assert_eq!(store.dev_servers_for_id(channel_id).len(), 1);
|
||||||
|
assert_eq!(store.dev_servers_for_id(channel_id)[0].name, "server-1");
|
||||||
|
assert_eq!(
|
||||||
|
store.dev_servers_for_id(channel_id)[0].status,
|
||||||
|
DevServerStatus::Offline
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let dev_server = server.create_dev_server(resp.access_token, cx2).await;
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
client.channel_store().update(cx, |store, _| {
|
||||||
|
assert_eq!(
|
||||||
|
store.dev_servers_for_id(channel_id)[0].status,
|
||||||
|
DevServerStatus::Online
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
dev_server
|
||||||
|
.fs()
|
||||||
|
.insert_tree(
|
||||||
|
"/remote",
|
||||||
|
json!({
|
||||||
|
"1.txt": "remote\nremote\nremote",
|
||||||
|
"2.js": "function two() { return 2; }",
|
||||||
|
"3.rs": "mod test",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
client
|
||||||
|
.channel_store()
|
||||||
|
.update(cx, |store, cx| {
|
||||||
|
store.create_remote_project(
|
||||||
|
channel_id,
|
||||||
|
client::DevServerId(resp.dev_server_id),
|
||||||
|
"project-1".to_string(),
|
||||||
|
"/remote".to_string(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
|
let remote_workspace = client
|
||||||
|
.channel_store()
|
||||||
|
.update(cx, |store, cx| {
|
||||||
|
let projects = store.remote_projects_for_id(channel_id);
|
||||||
|
assert_eq!(projects.len(), 1);
|
||||||
|
assert_eq!(projects[0].name, "project-1");
|
||||||
|
workspace::join_remote_project(
|
||||||
|
projects[0].project_id.unwrap(),
|
||||||
|
client.app_state.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
|
let cx2 = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut();
|
||||||
|
cx2.simulate_keystrokes("cmd-p 1 enter");
|
||||||
|
|
||||||
|
let editor = remote_workspace
|
||||||
|
.update(cx2, |ws, cx| {
|
||||||
|
ws.active_item_as::<Editor>(cx).unwrap().clone()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
editor.update(cx2, |ed, cx| {
|
||||||
|
assert_eq!(ed.text(cx).to_string(), "remote\nremote\nremote");
|
||||||
|
});
|
||||||
|
cx2.simulate_input("wow!");
|
||||||
|
cx2.simulate_keystrokes("cmd-s");
|
||||||
|
|
||||||
|
let content = dev_server
|
||||||
|
.fs()
|
||||||
|
.load(&Path::new("/remote/1.txt"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(content, "wow!remote\nremote\nremote\n");
|
||||||
|
}
|
@ -3760,7 +3760,7 @@ async fn test_leaving_project(
|
|||||||
|
|
||||||
// Client B can't join the project, unless they re-join the room.
|
// Client B can't join the project, unless they re-join the room.
|
||||||
cx_b.spawn(|cx| {
|
cx_b.spawn(|cx| {
|
||||||
Project::remote(
|
Project::in_room(
|
||||||
project_id,
|
project_id,
|
||||||
client_b.app_state.client.clone(),
|
client_b.app_state.client.clone(),
|
||||||
client_b.user_store().clone(),
|
client_b.user_store().clone(),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
auth::split_dev_server_token,
|
||||||
db::{tests::TestDb, NewUserParams, UserId},
|
db::{tests::TestDb, NewUserParams, UserId},
|
||||||
executor::Executor,
|
executor::Executor,
|
||||||
rpc::{Principal, Server, ZedVersion, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
rpc::{Principal, Server, ZedVersion, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
||||||
@ -302,6 +303,130 @@ impl TestServer {
|
|||||||
client
|
client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_dev_server(
|
||||||
|
&self,
|
||||||
|
access_token: String,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) -> TestClient {
|
||||||
|
cx.update(|cx| {
|
||||||
|
if cx.has_global::<SettingsStore>() {
|
||||||
|
panic!("Same cx used to create two test clients")
|
||||||
|
}
|
||||||
|
let settings = SettingsStore::test(cx);
|
||||||
|
cx.set_global(settings);
|
||||||
|
release_channel::init("0.0.0", cx);
|
||||||
|
client::init_settings(cx);
|
||||||
|
});
|
||||||
|
let (dev_server_id, _) = split_dev_server_token(&access_token).unwrap();
|
||||||
|
|
||||||
|
let clock = Arc::new(FakeSystemClock::default());
|
||||||
|
let http = FakeHttpClient::with_404_response();
|
||||||
|
let mut client = cx.update(|cx| Client::new(clock, http.clone(), cx));
|
||||||
|
let server = self.server.clone();
|
||||||
|
let db = self.app_state.db.clone();
|
||||||
|
let connection_killers = self.connection_killers.clone();
|
||||||
|
let forbid_connections = self.forbid_connections.clone();
|
||||||
|
Arc::get_mut(&mut client)
|
||||||
|
.unwrap()
|
||||||
|
.set_id(1)
|
||||||
|
.set_dev_server_token(client::DevServerToken(access_token.clone()))
|
||||||
|
.override_establish_connection(move |credentials, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
credentials,
|
||||||
|
&Credentials::DevServer {
|
||||||
|
token: client::DevServerToken(access_token.to_string())
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let server = server.clone();
|
||||||
|
let db = db.clone();
|
||||||
|
let connection_killers = connection_killers.clone();
|
||||||
|
let forbid_connections = forbid_connections.clone();
|
||||||
|
cx.spawn(move |cx| async move {
|
||||||
|
if forbid_connections.load(SeqCst) {
|
||||||
|
Err(EstablishConnectionError::other(anyhow!(
|
||||||
|
"server is forbidding connections"
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
let (client_conn, server_conn, killed) =
|
||||||
|
Connection::in_memory(cx.background_executor().clone());
|
||||||
|
let (connection_id_tx, connection_id_rx) = oneshot::channel();
|
||||||
|
let dev_server = db
|
||||||
|
.get_dev_server(dev_server_id)
|
||||||
|
.await
|
||||||
|
.expect("retrieving dev_server failed");
|
||||||
|
cx.background_executor()
|
||||||
|
.spawn(server.handle_connection(
|
||||||
|
server_conn,
|
||||||
|
"dev-server".to_string(),
|
||||||
|
Principal::DevServer(dev_server),
|
||||||
|
ZedVersion(SemanticVersion::new(1, 0, 0)),
|
||||||
|
Some(connection_id_tx),
|
||||||
|
Executor::Deterministic(cx.background_executor().clone()),
|
||||||
|
))
|
||||||
|
.detach();
|
||||||
|
let connection_id = connection_id_rx.await.map_err(|e| {
|
||||||
|
EstablishConnectionError::Other(anyhow!(
|
||||||
|
"{} (is server shutting down?)",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
connection_killers
|
||||||
|
.lock()
|
||||||
|
.insert(connection_id.into(), killed);
|
||||||
|
Ok(client_conn)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||||
|
let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||||
|
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||||
|
let app_state = Arc::new(workspace::AppState {
|
||||||
|
client: client.clone(),
|
||||||
|
user_store: user_store.clone(),
|
||||||
|
workspace_store,
|
||||||
|
languages: language_registry,
|
||||||
|
fs: fs.clone(),
|
||||||
|
build_window_options: |_, _| Default::default(),
|
||||||
|
node_runtime: FakeNodeRuntime::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
theme::init(theme::LoadThemes::JustBase, cx);
|
||||||
|
Project::init(&client, cx);
|
||||||
|
client::init(&client, cx);
|
||||||
|
language::init(cx);
|
||||||
|
editor::init(cx);
|
||||||
|
workspace::init(app_state.clone(), cx);
|
||||||
|
call::init(client.clone(), user_store.clone(), cx);
|
||||||
|
channel::init(&client, user_store.clone(), cx);
|
||||||
|
notifications::init(client.clone(), user_store, cx);
|
||||||
|
collab_ui::init(&app_state, cx);
|
||||||
|
file_finder::init(cx);
|
||||||
|
menu::init();
|
||||||
|
headless::init(
|
||||||
|
client.clone(),
|
||||||
|
headless::AppState {
|
||||||
|
languages: app_state.languages.clone(),
|
||||||
|
user_store: app_state.user_store.clone(),
|
||||||
|
fs: fs.clone(),
|
||||||
|
node_runtime: app_state.node_runtime.clone(),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
TestClient {
|
||||||
|
app_state,
|
||||||
|
username: "dev-server".to_string(),
|
||||||
|
channel_store: cx.read(ChannelStore::global).clone(),
|
||||||
|
notification_store: cx.read(NotificationStore::global).clone(),
|
||||||
|
state: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn disconnect_client(&self, peer_id: PeerId) {
|
pub fn disconnect_client(&self, peer_id: PeerId) {
|
||||||
self.connection_killers
|
self.connection_killers
|
||||||
.lock()
|
.lock()
|
||||||
|
@ -39,6 +39,7 @@ db.workspace = true
|
|||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
emojis.workspace = true
|
emojis.workspace = true
|
||||||
extensions_ui.workspace = true
|
extensions_ui.workspace = true
|
||||||
|
feature_flags.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
mod channel_modal;
|
mod channel_modal;
|
||||||
mod contact_finder;
|
mod contact_finder;
|
||||||
|
mod dev_server_modal;
|
||||||
|
|
||||||
use self::channel_modal::ChannelModal;
|
use self::channel_modal::ChannelModal;
|
||||||
|
use self::dev_server_modal::DevServerModal;
|
||||||
use crate::{
|
use crate::{
|
||||||
channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
|
channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
|
||||||
CollaborationPanelSettings,
|
CollaborationPanelSettings,
|
||||||
};
|
};
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use channel::{Channel, ChannelEvent, ChannelStore};
|
use channel::{Channel, ChannelEvent, ChannelStore, RemoteProject};
|
||||||
use client::{ChannelId, Client, Contact, ProjectId, User, UserStore};
|
use client::{ChannelId, Client, Contact, ProjectId, User, UserStore};
|
||||||
use contact_finder::ContactFinder;
|
use contact_finder::ContactFinder;
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::{Editor, EditorElement, EditorStyle};
|
use editor::{Editor, EditorElement, EditorStyle};
|
||||||
|
use feature_flags::{self, FeatureFlagAppExt};
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, anchored, canvas, deferred, div, fill, list, point, prelude::*, px, AnyElement,
|
actions, anchored, canvas, deferred, div, fill, list, point, prelude::*, px, AnyElement,
|
||||||
@ -24,7 +27,7 @@ use gpui::{
|
|||||||
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
|
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
|
||||||
use project::{Fs, Project};
|
use project::{Fs, Project};
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{self, ChannelVisibility, PeerId},
|
proto::{self, ChannelVisibility, DevServerStatus, PeerId},
|
||||||
ErrorCode, ErrorExt,
|
ErrorCode, ErrorExt,
|
||||||
};
|
};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
@ -188,6 +191,7 @@ enum ListEntry {
|
|||||||
id: ProjectId,
|
id: ProjectId,
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
},
|
},
|
||||||
|
RemoteProject(channel::RemoteProject),
|
||||||
Contact {
|
Contact {
|
||||||
contact: Arc<Contact>,
|
contact: Arc<Contact>,
|
||||||
calling: bool,
|
calling: bool,
|
||||||
@ -278,10 +282,23 @@ impl CollabPanel {
|
|||||||
.push(cx.observe(&this.user_store, |this, _, cx| {
|
.push(cx.observe(&this.user_store, |this, _, cx| {
|
||||||
this.update_entries(true, cx)
|
this.update_entries(true, cx)
|
||||||
}));
|
}));
|
||||||
this.subscriptions
|
let mut has_opened = false;
|
||||||
.push(cx.observe(&this.channel_store, |this, _, cx| {
|
this.subscriptions.push(cx.observe(
|
||||||
|
&this.channel_store,
|
||||||
|
move |this, channel_store, cx| {
|
||||||
|
if !has_opened {
|
||||||
|
if !channel_store
|
||||||
|
.read(cx)
|
||||||
|
.dev_servers_for_id(ChannelId(1))
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
this.manage_remote_projects(ChannelId(1), cx);
|
||||||
|
has_opened = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
this.update_entries(true, cx)
|
this.update_entries(true, cx)
|
||||||
}));
|
},
|
||||||
|
));
|
||||||
this.subscriptions
|
this.subscriptions
|
||||||
.push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
|
.push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
|
||||||
this.subscriptions.push(cx.subscribe(
|
this.subscriptions.push(cx.subscribe(
|
||||||
@ -569,6 +586,7 @@ impl CollabPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let hosted_projects = channel_store.projects_for_id(channel.id);
|
let hosted_projects = channel_store.projects_for_id(channel.id);
|
||||||
|
let remote_projects = channel_store.remote_projects_for_id(channel.id);
|
||||||
let has_children = channel_store
|
let has_children = channel_store
|
||||||
.channel_at_index(mat.candidate_id + 1)
|
.channel_at_index(mat.candidate_id + 1)
|
||||||
.map_or(false, |next_channel| {
|
.map_or(false, |next_channel| {
|
||||||
@ -604,7 +622,13 @@ impl CollabPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (name, id) in hosted_projects {
|
for (name, id) in hosted_projects {
|
||||||
self.entries.push(ListEntry::HostedProject { id, name })
|
self.entries.push(ListEntry::HostedProject { id, name });
|
||||||
|
}
|
||||||
|
|
||||||
|
if cx.has_flag::<feature_flags::Remoting>() {
|
||||||
|
for remote_project in remote_projects {
|
||||||
|
self.entries.push(ListEntry::RemoteProject(remote_project));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1065,6 +1089,59 @@ impl CollabPanel {
|
|||||||
.tooltip(move |cx| Tooltip::text("Open Project", cx))
|
.tooltip(move |cx| Tooltip::text("Open Project", cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_remote_project(
|
||||||
|
&self,
|
||||||
|
remote_project: &RemoteProject,
|
||||||
|
is_selected: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let id = remote_project.id;
|
||||||
|
let name = remote_project.name.clone();
|
||||||
|
let maybe_project_id = remote_project.project_id;
|
||||||
|
|
||||||
|
let dev_server = self
|
||||||
|
.channel_store
|
||||||
|
.read(cx)
|
||||||
|
.find_dev_server_by_id(remote_project.dev_server_id);
|
||||||
|
|
||||||
|
let tooltip_text = SharedString::from(match dev_server {
|
||||||
|
Some(dev_server) => format!("Open Remote Project ({})", dev_server.name),
|
||||||
|
None => "Open Remote Project".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let dev_server_is_online = dev_server.map(|s| s.status) == Some(DevServerStatus::Online);
|
||||||
|
|
||||||
|
let dev_server_text_color = if dev_server_is_online {
|
||||||
|
Color::Default
|
||||||
|
} else {
|
||||||
|
Color::Disabled
|
||||||
|
};
|
||||||
|
|
||||||
|
ListItem::new(ElementId::NamedInteger(
|
||||||
|
"remote-project".into(),
|
||||||
|
id.0 as usize,
|
||||||
|
))
|
||||||
|
.indent_level(2)
|
||||||
|
.indent_step_size(px(20.))
|
||||||
|
.selected(is_selected)
|
||||||
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
|
//TODO display error message if dev server is offline
|
||||||
|
if dev_server_is_online {
|
||||||
|
if let Some(project_id) = maybe_project_id {
|
||||||
|
this.join_remote_project(project_id, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.start_slot(
|
||||||
|
h_flex()
|
||||||
|
.relative()
|
||||||
|
.gap_1()
|
||||||
|
.child(IconButton::new(0, IconName::FileTree).icon_color(dev_server_text_color)),
|
||||||
|
)
|
||||||
|
.child(Label::new(name.clone()).color(dev_server_text_color))
|
||||||
|
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx))
|
||||||
|
}
|
||||||
|
|
||||||
fn has_subchannels(&self, ix: usize) -> bool {
|
fn has_subchannels(&self, ix: usize) -> bool {
|
||||||
self.entries.get(ix).map_or(false, |entry| {
|
self.entries.get(ix).map_or(false, |entry| {
|
||||||
if let ListEntry::Channel { has_children, .. } = entry {
|
if let ListEntry::Channel { has_children, .. } = entry {
|
||||||
@ -1266,11 +1343,24 @@ impl CollabPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.channel_store.read(cx).is_root_channel(channel_id) {
|
if self.channel_store.read(cx).is_root_channel(channel_id) {
|
||||||
context_menu = context_menu.separator().entry(
|
context_menu = context_menu
|
||||||
"Manage Members",
|
.separator()
|
||||||
None,
|
.entry(
|
||||||
cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
|
"Manage Members",
|
||||||
)
|
None,
|
||||||
|
cx.handler_for(&this, move |this, cx| {
|
||||||
|
this.manage_members(channel_id, cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.when(cx.has_flag::<feature_flags::Remoting>(), |context_menu| {
|
||||||
|
context_menu.entry(
|
||||||
|
"Manage Remote Projects",
|
||||||
|
None,
|
||||||
|
cx.handler_for(&this, move |this, cx| {
|
||||||
|
this.manage_remote_projects(channel_id, cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
context_menu = context_menu.entry(
|
context_menu = context_menu.entry(
|
||||||
"Move this channel",
|
"Move this channel",
|
||||||
@ -1534,6 +1624,11 @@ impl CollabPanel {
|
|||||||
} => {
|
} => {
|
||||||
// todo()
|
// todo()
|
||||||
}
|
}
|
||||||
|
ListEntry::RemoteProject(project) => {
|
||||||
|
if let Some(project_id) = project.project_id {
|
||||||
|
self.join_remote_project(project_id, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ListEntry::OutgoingRequest(_) => {}
|
ListEntry::OutgoingRequest(_) => {}
|
||||||
ListEntry::ChannelEditor { .. } => {}
|
ListEntry::ChannelEditor { .. } => {}
|
||||||
@ -1706,6 +1801,18 @@ impl CollabPanel {
|
|||||||
self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
|
self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn manage_remote_projects(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||||
|
let channel_store = self.channel_store.clone();
|
||||||
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.toggle_modal(cx, |cx| {
|
||||||
|
DevServerModal::new(channel_store.clone(), channel_id, cx)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
|
fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(channel) = self.selected_channel() {
|
if let Some(channel) = self.selected_channel() {
|
||||||
self.remove_channel(channel.id, cx)
|
self.remove_channel(channel.id, cx)
|
||||||
@ -2006,6 +2113,18 @@ impl CollabPanel {
|
|||||||
.detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
|
.detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn join_remote_project(&mut self, project_id: ProjectId, cx: &mut ViewContext<Self>) {
|
||||||
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let app_state = workspace.read(cx).app_state().clone();
|
||||||
|
workspace::join_remote_project(project_id, app_state, cx).detach_and_prompt_err(
|
||||||
|
"Failed to join project",
|
||||||
|
cx,
|
||||||
|
|_, _| None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||||
let Some(workspace) = self.workspace.upgrade() else {
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
return;
|
return;
|
||||||
@ -2141,6 +2260,9 @@ impl CollabPanel {
|
|||||||
ListEntry::HostedProject { id, name } => self
|
ListEntry::HostedProject { id, name } => self
|
||||||
.render_channel_project(*id, name, is_selected, cx)
|
.render_channel_project(*id, name, is_selected, cx)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
|
ListEntry::RemoteProject(remote_project) => self
|
||||||
|
.render_remote_project(remote_project, is_selected, cx)
|
||||||
|
.into_any_element(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2883,6 +3005,11 @@ impl PartialEq for ListEntry {
|
|||||||
return id == other_id;
|
return id == other_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ListEntry::RemoteProject(project) => {
|
||||||
|
if let ListEntry::RemoteProject(other) = other {
|
||||||
|
return project.id == other.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
ListEntry::ChannelNotes { channel_id } => {
|
ListEntry::ChannelNotes { channel_id } => {
|
||||||
if let ListEntry::ChannelNotes {
|
if let ListEntry::ChannelNotes {
|
||||||
channel_id: other_id,
|
channel_id: other_id,
|
||||||
|
622
crates/collab_ui/src/collab_panel/dev_server_modal.rs
Normal file
622
crates/collab_ui/src/collab_panel/dev_server_modal.rs
Normal file
@ -0,0 +1,622 @@
|
|||||||
|
use channel::{ChannelStore, DevServer, RemoteProject};
|
||||||
|
use client::{ChannelId, DevServerId, RemoteProjectId};
|
||||||
|
use editor::Editor;
|
||||||
|
use gpui::{
|
||||||
|
AppContext, ClipboardItem, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
|
||||||
|
ScrollHandle, Task, View, ViewContext,
|
||||||
|
};
|
||||||
|
use rpc::proto::{self, CreateDevServerResponse, DevServerStatus};
|
||||||
|
use ui::{prelude::*, Indicator, List, ListHeader, ModalContent, ModalHeader, Tooltip};
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::ModalView;
|
||||||
|
|
||||||
|
pub struct DevServerModal {
|
||||||
|
mode: Mode,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
scroll_handle: ScrollHandle,
|
||||||
|
channel_store: Model<ChannelStore>,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
remote_project_name_editor: View<Editor>,
|
||||||
|
remote_project_path_editor: View<Editor>,
|
||||||
|
dev_server_name_editor: View<Editor>,
|
||||||
|
_subscriptions: [gpui::Subscription; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct CreateDevServer {
|
||||||
|
creating: Option<Task<()>>,
|
||||||
|
dev_server: Option<CreateDevServerResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CreateRemoteProject {
|
||||||
|
dev_server_id: DevServerId,
|
||||||
|
creating: Option<Task<()>>,
|
||||||
|
remote_project: Option<proto::RemoteProject>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Mode {
|
||||||
|
Default,
|
||||||
|
CreateRemoteProject(CreateRemoteProject),
|
||||||
|
CreateDevServer(CreateDevServer),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DevServerModal {
|
||||||
|
pub fn new(
|
||||||
|
channel_store: Model<ChannelStore>,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let name_editor = cx.new_view(|cx| Editor::single_line(cx));
|
||||||
|
let path_editor = cx.new_view(|cx| Editor::single_line(cx));
|
||||||
|
let dev_server_name_editor = cx.new_view(|cx| {
|
||||||
|
let mut editor = Editor::single_line(cx);
|
||||||
|
editor.set_placeholder_text("Dev server name", cx);
|
||||||
|
editor
|
||||||
|
});
|
||||||
|
|
||||||
|
let focus_handle = cx.focus_handle();
|
||||||
|
|
||||||
|
let subscriptions = [
|
||||||
|
cx.observe(&channel_store, |_, _, cx| {
|
||||||
|
cx.notify();
|
||||||
|
}),
|
||||||
|
cx.on_focus_out(&focus_handle, |_, _cx| { /* cx.emit(DismissEvent) */ }),
|
||||||
|
];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
mode: Mode::Default,
|
||||||
|
focus_handle,
|
||||||
|
scroll_handle: ScrollHandle::new(),
|
||||||
|
channel_store,
|
||||||
|
channel_id,
|
||||||
|
remote_project_name_editor: name_editor,
|
||||||
|
remote_project_path_editor: path_editor,
|
||||||
|
dev_server_name_editor,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_remote_project(
|
||||||
|
&mut self,
|
||||||
|
dev_server_id: DevServerId,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let channel_id = self.channel_id;
|
||||||
|
let name = self
|
||||||
|
.remote_project_name_editor
|
||||||
|
.read(cx)
|
||||||
|
.text(cx)
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
let path = self
|
||||||
|
.remote_project_path_editor
|
||||||
|
.read(cx)
|
||||||
|
.text(cx)
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if path == "" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let create = self.channel_store.update(cx, |store, cx| {
|
||||||
|
store.create_remote_project(channel_id, dev_server_id, name, path, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
let task = cx.spawn(|this, mut cx| async move {
|
||||||
|
let result = create.await;
|
||||||
|
if let Err(e) = &result {
|
||||||
|
cx.prompt(
|
||||||
|
gpui::PromptLevel::Critical,
|
||||||
|
"Failed to create project",
|
||||||
|
Some(&format!("{:?}. Please try again.", e)),
|
||||||
|
&["Ok"],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
|
||||||
|
dev_server_id,
|
||||||
|
creating: None,
|
||||||
|
remote_project: result.ok().and_then(|r| r.remote_project),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.mode = Mode::CreateRemoteProject(CreateRemoteProject {
|
||||||
|
dev_server_id,
|
||||||
|
creating: Some(task),
|
||||||
|
remote_project: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_dev_server(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
let name = self
|
||||||
|
.dev_server_name_editor
|
||||||
|
.read(cx)
|
||||||
|
.text(cx)
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dev_server = self.channel_store.update(cx, |store, cx| {
|
||||||
|
store.create_dev_server(self.channel_id, name.clone(), cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
let task = cx.spawn(|this, mut cx| async move {
|
||||||
|
match dev_server.await {
|
||||||
|
Ok(dev_server) => {
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
this.mode = Mode::CreateDevServer(CreateDevServer {
|
||||||
|
creating: None,
|
||||||
|
dev_server: Some(dev_server),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
cx.prompt(
|
||||||
|
gpui::PromptLevel::Critical,
|
||||||
|
"Failed to create server",
|
||||||
|
Some(&format!("{:?}. Please try again.", e)),
|
||||||
|
&["Ok"],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
this.mode = Mode::CreateDevServer(Default::default());
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.mode = Mode::CreateDevServer(CreateDevServer {
|
||||||
|
creating: Some(task),
|
||||||
|
dev_server: None,
|
||||||
|
});
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
|
match self.mode {
|
||||||
|
Mode::Default => cx.emit(DismissEvent),
|
||||||
|
Mode::CreateRemoteProject(_) | Mode::CreateDevServer(_) => {
|
||||||
|
self.mode = Mode::Default;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_dev_server(
|
||||||
|
&mut self,
|
||||||
|
dev_server: &DevServer,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let channel_store = self.channel_store.read(cx);
|
||||||
|
let dev_server_id = dev_server.id;
|
||||||
|
let status = dev_server.status;
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.group("dev-server")
|
||||||
|
.justify_between()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id(("status", dev_server.id.0))
|
||||||
|
.relative()
|
||||||
|
.child(Icon::new(IconName::Server).size(IconSize::Small))
|
||||||
|
.child(
|
||||||
|
div().absolute().bottom_0().left(rems_from_px(8.0)).child(
|
||||||
|
Indicator::dot().color(match status {
|
||||||
|
DevServerStatus::Online => Color::Created,
|
||||||
|
DevServerStatus::Offline => Color::Deleted,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.tooltip(move |cx| {
|
||||||
|
Tooltip::text(
|
||||||
|
match status {
|
||||||
|
DevServerStatus::Online => "Online",
|
||||||
|
DevServerStatus::Offline => "Offline",
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(dev_server.name.clone())
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.visible_on_hover("dev-server")
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
IconButton::new("edit-dev-server", IconName::Pencil)
|
||||||
|
.disabled(true) //TODO implement this on the collab side
|
||||||
|
.tooltip(|cx| {
|
||||||
|
Tooltip::text("Coming Soon - Edit dev server", cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
IconButton::new("remove-dev-server", IconName::Trash)
|
||||||
|
.disabled(true) //TODO implement this on the collab side
|
||||||
|
.tooltip(|cx| {
|
||||||
|
Tooltip::text("Coming Soon - Remove dev server", cx)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex().gap_1().child(
|
||||||
|
IconButton::new("add-remote-project", IconName::Plus)
|
||||||
|
.tooltip(|cx| Tooltip::text("Add a remote project", cx))
|
||||||
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
|
this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
|
||||||
|
dev_server_id,
|
||||||
|
creating: None,
|
||||||
|
remote_project: None,
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.bg(cx.theme().colors().title_bar_background)
|
||||||
|
.border()
|
||||||
|
.border_color(cx.theme().colors().border_variant)
|
||||||
|
.rounded_md()
|
||||||
|
.my_1()
|
||||||
|
.py_0p5()
|
||||||
|
.px_3()
|
||||||
|
.child(
|
||||||
|
List::new().empty_message("No projects.").children(
|
||||||
|
channel_store
|
||||||
|
.remote_projects_for_id(dev_server.channel_id)
|
||||||
|
.iter()
|
||||||
|
.filter_map(|remote_project| {
|
||||||
|
if remote_project.dev_server_id == dev_server.id {
|
||||||
|
Some(self.render_remote_project(remote_project, cx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// .child(div().ml_8().child(
|
||||||
|
// Button::new(("add-project", dev_server_id.0), "Add Project").on_click(cx.listener(
|
||||||
|
// move |this, _, cx| {
|
||||||
|
// this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
|
||||||
|
// dev_server_id,
|
||||||
|
// creating: None,
|
||||||
|
// remote_project: None,
|
||||||
|
// });
|
||||||
|
// cx.notify();
|
||||||
|
// },
|
||||||
|
// )),
|
||||||
|
// ))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_remote_project(
|
||||||
|
&mut self,
|
||||||
|
project: &RemoteProject,
|
||||||
|
_: &mut ViewContext<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(Icon::new(IconName::FileTree))
|
||||||
|
.child(Label::new(project.name.clone()))
|
||||||
|
.child(Label::new(format!("({})", project.path.clone())).color(Color::Muted))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_create_dev_server(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let Mode::CreateDevServer(CreateDevServer {
|
||||||
|
creating,
|
||||||
|
dev_server,
|
||||||
|
}) = &self.mode
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.dev_server_name_editor.update(cx, |editor, _| {
|
||||||
|
editor.set_read_only(creating.is_some() || dev_server.is_some())
|
||||||
|
});
|
||||||
|
v_flex()
|
||||||
|
.px_1()
|
||||||
|
.pt_0p5()
|
||||||
|
.gap_px()
|
||||||
|
.child(
|
||||||
|
v_flex().py_0p5().px_1().child(
|
||||||
|
h_flex()
|
||||||
|
.px_1()
|
||||||
|
.py_0p5()
|
||||||
|
.child(
|
||||||
|
IconButton::new("back", IconName::ArrowLeft)
|
||||||
|
.style(ButtonStyle::Transparent)
|
||||||
|
.on_click(cx.listener(|this, _: &gpui::ClickEvent, cx| {
|
||||||
|
this.mode = Mode::Default;
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(Headline::new("Register dev server")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.ml_5()
|
||||||
|
.gap_2()
|
||||||
|
.child("Name")
|
||||||
|
.child(self.dev_server_name_editor.clone())
|
||||||
|
.on_action(
|
||||||
|
cx.listener(|this, _: &menu::Confirm, cx| this.create_dev_server(cx)),
|
||||||
|
)
|
||||||
|
.when(creating.is_none() && dev_server.is_none(), |div| {
|
||||||
|
div.child(
|
||||||
|
Button::new("create-dev-server", "Create").on_click(cx.listener(
|
||||||
|
move |this, _, cx| {
|
||||||
|
this.create_dev_server(cx);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(creating.is_some() && dev_server.is_none(), |div| {
|
||||||
|
div.child(Button::new("create-dev-server", "Creating...").disabled(true))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.when_some(dev_server.clone(), |div, dev_server| {
|
||||||
|
let channel_store = self.channel_store.read(cx);
|
||||||
|
let status = channel_store
|
||||||
|
.find_dev_server_by_id(DevServerId(dev_server.dev_server_id))
|
||||||
|
.map(|server| server.status)
|
||||||
|
.unwrap_or(DevServerStatus::Offline);
|
||||||
|
let instructions = SharedString::from(format!(
|
||||||
|
"zed --dev-server-token {}",
|
||||||
|
dev_server.access_token
|
||||||
|
));
|
||||||
|
div.child(
|
||||||
|
v_flex()
|
||||||
|
.ml_8()
|
||||||
|
.gap_2()
|
||||||
|
.child(Label::new(format!(
|
||||||
|
"Please log into `{}` and run:",
|
||||||
|
dev_server.name
|
||||||
|
)))
|
||||||
|
.child(instructions.clone())
|
||||||
|
.child(
|
||||||
|
IconButton::new("copy-access-token", IconName::Copy)
|
||||||
|
.on_click(cx.listener(move |_, _, cx| {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new(
|
||||||
|
instructions.to_string(),
|
||||||
|
))
|
||||||
|
}))
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.tooltip(|cx| Tooltip::text("Copy access token", cx)),
|
||||||
|
)
|
||||||
|
.when(status == DevServerStatus::Offline, |this| {
|
||||||
|
this.child(Label::new("Waiting for connection..."))
|
||||||
|
})
|
||||||
|
.when(status == DevServerStatus::Online, |this| {
|
||||||
|
this.child(Label::new("Connection established! 🎊")).child(
|
||||||
|
Button::new("done", "Done").on_click(cx.listener(|this, _, cx| {
|
||||||
|
this.mode = Mode::Default;
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let channel_store = self.channel_store.read(cx);
|
||||||
|
let dev_servers = channel_store.dev_servers_for_id(self.channel_id);
|
||||||
|
// let dev_servers = Vec::new();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.id("scroll-container")
|
||||||
|
.h_full()
|
||||||
|
.overflow_y_scroll()
|
||||||
|
.track_scroll(&self.scroll_handle)
|
||||||
|
.px_1()
|
||||||
|
.pt_0p5()
|
||||||
|
.gap_px()
|
||||||
|
.child(
|
||||||
|
ModalHeader::new("Manage Remote Project")
|
||||||
|
.child(Headline::new("Remote Projects").size(HeadlineSize::Small)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
ModalContent::new().child(
|
||||||
|
List::new()
|
||||||
|
.empty_message("No dev servers registered.")
|
||||||
|
.header(Some(
|
||||||
|
ListHeader::new("Dev Servers").end_slot(
|
||||||
|
Button::new("register-dev-server-button", "New Server")
|
||||||
|
.icon(IconName::Plus)
|
||||||
|
.icon_position(IconPosition::Start)
|
||||||
|
.tooltip(|cx| Tooltip::text("Register a new dev server", cx))
|
||||||
|
.on_click(cx.listener(|this, _, cx| {
|
||||||
|
this.mode = Mode::CreateDevServer(Default::default());
|
||||||
|
this.dev_server_name_editor
|
||||||
|
.read(cx)
|
||||||
|
.focus_handle(cx)
|
||||||
|
.focus(cx);
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.children(dev_servers.iter().map(|dev_server| {
|
||||||
|
self.render_dev_server(dev_server, cx).into_any_element()
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_create_project(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let Mode::CreateRemoteProject(CreateRemoteProject {
|
||||||
|
dev_server_id,
|
||||||
|
creating,
|
||||||
|
remote_project,
|
||||||
|
}) = &self.mode
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
let channel_store = self.channel_store.read(cx);
|
||||||
|
let (dev_server_name, dev_server_status) = channel_store
|
||||||
|
.find_dev_server_by_id(*dev_server_id)
|
||||||
|
.map(|server| (server.name.clone(), server.status))
|
||||||
|
.unwrap_or((SharedString::from(""), DevServerStatus::Offline));
|
||||||
|
v_flex()
|
||||||
|
.px_1()
|
||||||
|
.pt_0p5()
|
||||||
|
.gap_px()
|
||||||
|
.child(
|
||||||
|
ModalHeader::new("Manage Remote Project")
|
||||||
|
.child(Headline::new("Manage Remote Projects")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.py_0p5()
|
||||||
|
.px_1()
|
||||||
|
.child(div().px_1().py_0p5().child(
|
||||||
|
IconButton::new("back", IconName::ArrowLeft).on_click(cx.listener(
|
||||||
|
|this, _, cx| {
|
||||||
|
this.mode = Mode::Default;
|
||||||
|
cx.notify()
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.child("Add Project..."),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.ml_5()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id(("status", dev_server_id.0))
|
||||||
|
.relative()
|
||||||
|
.child(Icon::new(IconName::Server))
|
||||||
|
.child(div().absolute().bottom_0().left(rems_from_px(12.0)).child(
|
||||||
|
Indicator::dot().color(match dev_server_status {
|
||||||
|
DevServerStatus::Online => Color::Created,
|
||||||
|
DevServerStatus::Offline => Color::Deleted,
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
.tooltip(move |cx| {
|
||||||
|
Tooltip::text(
|
||||||
|
match dev_server_status {
|
||||||
|
DevServerStatus::Online => "Online",
|
||||||
|
DevServerStatus::Offline => "Offline",
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(dev_server_name.clone()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.ml_5()
|
||||||
|
.gap_2()
|
||||||
|
.child("Name")
|
||||||
|
.child(self.remote_project_name_editor.clone())
|
||||||
|
.on_action(cx.listener(|this, _: &menu::Confirm, cx| {
|
||||||
|
cx.focus_view(&this.remote_project_path_editor)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.ml_5()
|
||||||
|
.gap_2()
|
||||||
|
.child("Path")
|
||||||
|
.child(self.remote_project_path_editor.clone())
|
||||||
|
.on_action(
|
||||||
|
cx.listener(|this, _: &menu::Confirm, cx| this.create_dev_server(cx)),
|
||||||
|
)
|
||||||
|
.when(creating.is_none() && remote_project.is_none(), |div| {
|
||||||
|
div.child(Button::new("create-remote-server", "Create").on_click({
|
||||||
|
let dev_server_id = *dev_server_id;
|
||||||
|
cx.listener(move |this, _, cx| {
|
||||||
|
this.create_remote_project(dev_server_id, cx)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.when(creating.is_some(), |div| {
|
||||||
|
div.child(Button::new("create-dev-server", "Creating...").disabled(true))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.when_some(remote_project.clone(), |div, remote_project| {
|
||||||
|
let channel_store = self.channel_store.read(cx);
|
||||||
|
let status = channel_store
|
||||||
|
.find_remote_project_by_id(RemoteProjectId(remote_project.id))
|
||||||
|
.map(|project| {
|
||||||
|
if project.project_id.is_some() {
|
||||||
|
DevServerStatus::Online
|
||||||
|
} else {
|
||||||
|
DevServerStatus::Offline
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(DevServerStatus::Offline);
|
||||||
|
div.child(
|
||||||
|
v_flex()
|
||||||
|
.ml_5()
|
||||||
|
.ml_8()
|
||||||
|
.gap_2()
|
||||||
|
.when(status == DevServerStatus::Offline, |this| {
|
||||||
|
this.child(Label::new("Waiting for project..."))
|
||||||
|
})
|
||||||
|
.when(status == DevServerStatus::Online, |this| {
|
||||||
|
this.child(Label::new("Project online! 🎊")).child(
|
||||||
|
Button::new("done", "Done").on_click(cx.listener(|this, _, cx| {
|
||||||
|
this.mode = Mode::Default;
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ModalView for DevServerModal {}
|
||||||
|
|
||||||
|
impl FocusableView for DevServerModal {
|
||||||
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<DismissEvent> for DevServerModal {}
|
||||||
|
|
||||||
|
impl Render for DevServerModal {
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.track_focus(&self.focus_handle)
|
||||||
|
.elevation_3(cx)
|
||||||
|
.key_context("DevServerModal")
|
||||||
|
.on_action(cx.listener(Self::cancel))
|
||||||
|
.pb_4()
|
||||||
|
.w(rems(34.))
|
||||||
|
.min_h(rems(20.))
|
||||||
|
.max_h(rems(40.))
|
||||||
|
.child(match &self.mode {
|
||||||
|
Mode::Default => self.render_default(cx).into_any_element(),
|
||||||
|
Mode::CreateRemoteProject(_) => self.render_create_project(cx).into_any_element(),
|
||||||
|
Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,11 @@ pub trait FeatureFlag {
|
|||||||
const NAME: &'static str;
|
const NAME: &'static str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Remoting {}
|
||||||
|
impl FeatureFlag for Remoting {
|
||||||
|
const NAME: &'static str = "remoting";
|
||||||
|
}
|
||||||
|
|
||||||
pub trait FeatureFlagViewExt<V: 'static> {
|
pub trait FeatureFlagViewExt<V: 'static> {
|
||||||
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
|
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, BorrowAppContext, Context,
|
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, BorrowAppContext, Context,
|
||||||
DismissEvent, FocusableView, ForegroundExecutor, Global, Model, ModelContext, Render,
|
DismissEvent, FocusableView, ForegroundExecutor, Global, Model, ModelContext, PromptLevel,
|
||||||
Reservation, Result, Task, View, ViewContext, VisualContext, WindowContext, WindowHandle,
|
Render, Reservation, Result, Task, View, ViewContext, VisualContext, WindowContext,
|
||||||
|
WindowHandle,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as _};
|
use anyhow::{anyhow, Context as _};
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
|
use futures::channel::oneshot;
|
||||||
use std::{future::Future, rc::Weak};
|
use std::{future::Future, rc::Weak};
|
||||||
|
|
||||||
/// An async-friendly version of [AppContext] with a static lifetime so it can be held across `await` points in async code.
|
/// An async-friendly version of [AppContext] with a static lifetime so it can be held across `await` points in async code.
|
||||||
@ -285,6 +287,21 @@ impl AsyncWindowContext {
|
|||||||
{
|
{
|
||||||
self.foreground_executor.spawn(f(self.clone()))
|
self.foreground_executor.spawn(f(self.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Present a platform dialog.
|
||||||
|
/// The provided message will be presented, along with buttons for each answer.
|
||||||
|
/// When a button is clicked, the returned Receiver will receive the index of the clicked button.
|
||||||
|
pub fn prompt(
|
||||||
|
&mut self,
|
||||||
|
level: PromptLevel,
|
||||||
|
message: &str,
|
||||||
|
detail: Option<&str>,
|
||||||
|
answers: &[&str],
|
||||||
|
) -> oneshot::Receiver<usize> {
|
||||||
|
self.window
|
||||||
|
.update(self, |_, cx| cx.prompt(level, message, detail, answers))
|
||||||
|
.unwrap_or_else(|_| oneshot::channel().1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context for AsyncWindowContext {
|
impl Context for AsyncWindowContext {
|
||||||
|
@ -73,12 +73,17 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
||||||
let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
|
let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
|
||||||
|
let x11_display = std::env::var_os("DISPLAY");
|
||||||
|
|
||||||
let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
|
let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
|
||||||
|
let use_x11 = x11_display.is_some_and(|display| !display.is_empty());
|
||||||
|
|
||||||
if use_wayland {
|
if use_wayland {
|
||||||
Rc::new(WaylandClient::new())
|
Rc::new(WaylandClient::new())
|
||||||
} else {
|
} else if use_x11 {
|
||||||
Rc::new(X11Client::new())
|
Rc::new(X11Client::new())
|
||||||
|
} else {
|
||||||
|
Rc::new(HeadlessClient::new())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// todo("windows")
|
// todo("windows")
|
||||||
|
@ -2,11 +2,13 @@
|
|||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
|
|
||||||
mod dispatcher;
|
mod dispatcher;
|
||||||
|
mod headless;
|
||||||
mod platform;
|
mod platform;
|
||||||
mod wayland;
|
mod wayland;
|
||||||
mod x11;
|
mod x11;
|
||||||
|
|
||||||
pub(crate) use dispatcher::*;
|
pub(crate) use dispatcher::*;
|
||||||
|
pub(crate) use headless::*;
|
||||||
pub(crate) use platform::*;
|
pub(crate) use platform::*;
|
||||||
pub(crate) use wayland::*;
|
pub(crate) use wayland::*;
|
||||||
pub(crate) use x11::*;
|
pub(crate) use x11::*;
|
||||||
|
3
crates/gpui/src/platform/linux/headless.rs
Normal file
3
crates/gpui/src/platform/linux/headless.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod client;
|
||||||
|
|
||||||
|
pub(crate) use client::*;
|
98
crates/gpui/src/platform/linux/headless/client.rs
Normal file
98
crates/gpui/src/platform/linux/headless/client.rs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use calloop::{EventLoop, LoopHandle};
|
||||||
|
use collections::HashMap;
|
||||||
|
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
use crate::platform::linux::LinuxClient;
|
||||||
|
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||||
|
use crate::{
|
||||||
|
px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Modifiers, ModifiersChangedEvent, Pixels,
|
||||||
|
PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams,
|
||||||
|
};
|
||||||
|
|
||||||
|
use calloop::{
|
||||||
|
generic::{FdWrapper, Generic},
|
||||||
|
RegistrationToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct HeadlessClientState {
|
||||||
|
pub(crate) loop_handle: LoopHandle<'static, HeadlessClient>,
|
||||||
|
pub(crate) event_loop: Option<calloop::EventLoop<'static, HeadlessClient>>,
|
||||||
|
pub(crate) common: LinuxCommon,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct HeadlessClient(Rc<RefCell<HeadlessClientState>>);
|
||||||
|
|
||||||
|
impl HeadlessClient {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
let event_loop = EventLoop::try_new().unwrap();
|
||||||
|
|
||||||
|
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||||
|
|
||||||
|
let handle = event_loop.handle();
|
||||||
|
|
||||||
|
handle.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
|
||||||
|
if let calloop::channel::Event::Msg(runnable) = event {
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
HeadlessClient(Rc::new(RefCell::new(HeadlessClientState {
|
||||||
|
event_loop: Some(event_loop),
|
||||||
|
loop_handle: handle,
|
||||||
|
common,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinuxClient for HeadlessClient {
|
||||||
|
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
|
||||||
|
f(&mut self.0.borrow_mut().common)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_window(
|
||||||
|
&self,
|
||||||
|
_handle: AnyWindowHandle,
|
||||||
|
params: WindowParams,
|
||||||
|
) -> Box<dyn PlatformWindow> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
//todo(linux)
|
||||||
|
fn set_cursor_style(&self, _style: CursorStyle) {}
|
||||||
|
|
||||||
|
fn write_to_clipboard(&self, item: crate::ClipboardItem) {}
|
||||||
|
|
||||||
|
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self) {
|
||||||
|
let mut event_loop = self
|
||||||
|
.0
|
||||||
|
.borrow_mut()
|
||||||
|
.event_loop
|
||||||
|
.take()
|
||||||
|
.expect("App is already running");
|
||||||
|
|
||||||
|
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
|
||||||
|
}
|
||||||
|
}
|
36
crates/headless/Cargo.toml
Normal file
36
crates/headless/Cargo.toml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
[package]
|
||||||
|
name = "headless"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/headless.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
client.workspace = true
|
||||||
|
ctrlc.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
rpc.workspace = true
|
||||||
|
util.workspace = true
|
||||||
|
node_runtime.workspace = true
|
||||||
|
language.workspace = true
|
||||||
|
project.workspace = true
|
||||||
|
fs.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
settings.workspace = true
|
||||||
|
postage.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
client = { workspace = true, features = ["test-support"] }
|
||||||
|
fs = { workspace = true, features = ["test-support"] }
|
||||||
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
rpc = { workspace = true, features = ["test-support"] }
|
||||||
|
util = { workspace = true, features = ["test-support"] }
|
1
crates/headless/LICENSE-GPL
Symbolic link
1
crates/headless/LICENSE-GPL
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../LICENSE-GPL
|
265
crates/headless/src/headless.rs
Normal file
265
crates/headless/src/headless.rs
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use client::{user::UserStore, Client, ClientSettings, RemoteProjectId};
|
||||||
|
use fs::Fs;
|
||||||
|
use futures::Future;
|
||||||
|
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, Task, WeakModel};
|
||||||
|
use language::LanguageRegistry;
|
||||||
|
use node_runtime::NodeRuntime;
|
||||||
|
use postage::stream::Stream;
|
||||||
|
use project::Project;
|
||||||
|
use rpc::{proto, TypedEnvelope};
|
||||||
|
use settings::Settings;
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
use util::{ResultExt, TryFutureExt};
|
||||||
|
|
||||||
|
pub struct DevServer {
|
||||||
|
client: Arc<Client>,
|
||||||
|
app_state: AppState,
|
||||||
|
projects: HashMap<RemoteProjectId, Model<Project>>,
|
||||||
|
_subscriptions: Vec<client::Subscription>,
|
||||||
|
_maintain_connection: Task<Option<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub node_runtime: Arc<dyn NodeRuntime>,
|
||||||
|
pub user_store: Model<UserStore>,
|
||||||
|
pub languages: Arc<LanguageRegistry>,
|
||||||
|
pub fs: Arc<dyn Fs>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlobalDevServer(Model<DevServer>);
|
||||||
|
|
||||||
|
impl Global for GlobalDevServer {}
|
||||||
|
|
||||||
|
pub fn init(client: Arc<Client>, app_state: AppState, cx: &mut AppContext) {
|
||||||
|
let dev_server = cx.new_model(|cx| DevServer::new(client.clone(), app_state, cx));
|
||||||
|
cx.set_global(GlobalDevServer(dev_server.clone()));
|
||||||
|
|
||||||
|
// Set up a handler when the dev server is shut down by the user pressing Ctrl-C
|
||||||
|
let (tx, rx) = futures::channel::oneshot::channel();
|
||||||
|
set_ctrlc_handler(move || tx.send(()).log_err().unwrap()).log_err();
|
||||||
|
|
||||||
|
cx.spawn(|cx| async move {
|
||||||
|
rx.await.log_err();
|
||||||
|
log::info!("Received interrupt signal");
|
||||||
|
cx.update(|cx| cx.quit()).log_err();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
let server_url = ClientSettings::get_global(&cx).server_url.clone();
|
||||||
|
cx.spawn(|cx| async move {
|
||||||
|
match client.authenticate_and_connect(false, &cx).await {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!("Connected to {}", server_url);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error connecting to {}: {}", server_url, e);
|
||||||
|
cx.update(|cx| cx.quit()).log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_ctrlc_handler<F>(f: F) -> Result<(), ctrlc::Error>
|
||||||
|
where
|
||||||
|
F: FnOnce() + 'static + Send,
|
||||||
|
{
|
||||||
|
let f = std::sync::Mutex::new(Some(f));
|
||||||
|
ctrlc::set_handler(move || {
|
||||||
|
if let Ok(mut guard) = f.lock() {
|
||||||
|
let f = guard.take().expect("f can only be taken once");
|
||||||
|
f();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DevServer {
|
||||||
|
pub fn global(cx: &AppContext) -> Model<DevServer> {
|
||||||
|
cx.global::<GlobalDevServer>().0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(client: Arc<Client>, app_state: AppState, cx: &mut ModelContext<Self>) -> Self {
|
||||||
|
cx.on_app_quit(Self::app_will_quit).detach();
|
||||||
|
|
||||||
|
let maintain_connection = cx.spawn({
|
||||||
|
let client = client.clone();
|
||||||
|
move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
|
||||||
|
});
|
||||||
|
|
||||||
|
DevServer {
|
||||||
|
_subscriptions: vec![
|
||||||
|
client.add_message_handler(cx.weak_model(), Self::handle_dev_server_instructions)
|
||||||
|
],
|
||||||
|
_maintain_connection: maintain_connection,
|
||||||
|
projects: Default::default(),
|
||||||
|
app_state,
|
||||||
|
client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app_will_quit(&mut self, _: &mut ModelContext<Self>) -> impl Future<Output = ()> {
|
||||||
|
let request = self.client.request(proto::ShutdownDevServer {});
|
||||||
|
async move {
|
||||||
|
request.await.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_dev_server_instructions(
|
||||||
|
this: Model<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::DevServerInstructions>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (added_projects, removed_projects_ids) = this.read_with(&mut cx, |this, _| {
|
||||||
|
let removed_projects = this
|
||||||
|
.projects
|
||||||
|
.keys()
|
||||||
|
.filter(|remote_project_id| {
|
||||||
|
!envelope
|
||||||
|
.payload
|
||||||
|
.projects
|
||||||
|
.iter()
|
||||||
|
.any(|p| p.id == remote_project_id.0)
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let added_projects = envelope
|
||||||
|
.payload
|
||||||
|
.projects
|
||||||
|
.into_iter()
|
||||||
|
.filter(|project| !this.projects.contains_key(&RemoteProjectId(project.id)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
(added_projects, removed_projects)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for remote_project in added_projects {
|
||||||
|
DevServer::share_project(this.clone(), &remote_project, &mut cx).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
for old_project_id in &removed_projects_ids {
|
||||||
|
this.unshare_project(old_project_id, cx)?;
|
||||||
|
}
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
})??;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unshare_project(
|
||||||
|
&mut self,
|
||||||
|
remote_project_id: &RemoteProjectId,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Some(project) = self.projects.remove(remote_project_id) {
|
||||||
|
project.update(cx, |project, cx| project.unshare(cx))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn share_project(
|
||||||
|
this: Model<Self>,
|
||||||
|
remote_project: &proto::RemoteProject,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (client, project) = this.update(cx, |this, cx| {
|
||||||
|
let project = Project::local(
|
||||||
|
this.client.clone(),
|
||||||
|
this.app_state.node_runtime.clone(),
|
||||||
|
this.app_state.user_store.clone(),
|
||||||
|
this.app_state.languages.clone(),
|
||||||
|
this.app_state.fs.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
(this.client.clone(), project)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.find_or_create_local_worktree(&remote_project.path, true, cx)
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let worktrees =
|
||||||
|
project.read_with(cx, |project, cx| project.worktree_metadata_protos(cx))?;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.request(proto::ShareRemoteProject {
|
||||||
|
remote_project_id: remote_project.id,
|
||||||
|
worktrees,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let project_id = response.project_id;
|
||||||
|
project.update(cx, |project, cx| project.shared(project_id, cx))??;
|
||||||
|
this.update(cx, |this, _| {
|
||||||
|
this.projects
|
||||||
|
.insert(RemoteProjectId(remote_project.id), project);
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn maintain_connection(
|
||||||
|
this: WeakModel<Self>,
|
||||||
|
client: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut client_status = client.status();
|
||||||
|
|
||||||
|
let _ = client_status.try_recv();
|
||||||
|
let current_status = *client_status.borrow();
|
||||||
|
if current_status.is_connected() {
|
||||||
|
// wait for first disconnect
|
||||||
|
client_status.recv().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Some(current_status) = client_status.recv().await else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let Some(this) = this.upgrade() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if !current_status.is_connected() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| this.rejoin(cx))?.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
|
let mut projects: HashMap<u64, Model<Project>> = HashMap::default();
|
||||||
|
let request = self.client.request(proto::ReconnectDevServer {
|
||||||
|
reshared_projects: self
|
||||||
|
.projects
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(_, handle)| {
|
||||||
|
let project = handle.read(cx);
|
||||||
|
let project_id = project.remote_id()?;
|
||||||
|
projects.insert(project_id, handle.clone());
|
||||||
|
Some(proto::UpdateProject {
|
||||||
|
project_id,
|
||||||
|
worktrees: project.worktree_metadata_protos(cx),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
let response = request.await?;
|
||||||
|
|
||||||
|
for reshared_project in response.reshared_projects {
|
||||||
|
if let Some(project) = projects.get(&reshared_project.id) {
|
||||||
|
project.update(&mut cx, |project, cx| {
|
||||||
|
project.reshared(reshared_project, cx).log_err();
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
212
crates/project/src/connection_manager.rs
Normal file
212
crates/project/src/connection_manager.rs
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
use super::Project;
|
||||||
|
use anyhow::Result;
|
||||||
|
use client::Client;
|
||||||
|
use collections::{HashMap, HashSet};
|
||||||
|
use futures::{FutureExt, StreamExt};
|
||||||
|
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, Task, WeakModel};
|
||||||
|
use postage::stream::Stream;
|
||||||
|
use rpc::proto;
|
||||||
|
use std::{sync::Arc, time::Duration};
|
||||||
|
use util::{ResultExt, TryFutureExt};
|
||||||
|
|
||||||
|
impl Global for GlobalManager {}
|
||||||
|
struct GlobalManager(Model<Manager>);
|
||||||
|
|
||||||
|
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
pub struct Manager {
|
||||||
|
client: Arc<Client>,
|
||||||
|
maintain_connection: Option<Task<Option<()>>>,
|
||||||
|
projects: HashSet<WeakModel<Project>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||||
|
let manager = cx.new_model(|_| Manager {
|
||||||
|
client,
|
||||||
|
maintain_connection: None,
|
||||||
|
projects: HashSet::default(),
|
||||||
|
});
|
||||||
|
cx.set_global(GlobalManager(manager));
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn global(cx: &AppContext) -> Model<Manager> {
|
||||||
|
cx.global::<GlobalManager>().0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn maintain_project_connection(
|
||||||
|
&mut self,
|
||||||
|
project: &Model<Project>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
let manager = cx.weak_model();
|
||||||
|
project.update(cx, |_, cx| {
|
||||||
|
let manager = manager.clone();
|
||||||
|
cx.on_release(move |project, cx| {
|
||||||
|
manager
|
||||||
|
.update(cx, |manager, cx| {
|
||||||
|
manager.projects.retain(|p| {
|
||||||
|
if let Some(p) = p.upgrade() {
|
||||||
|
p.read(cx).remote_id() != project.remote_id()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if manager.projects.is_empty() {
|
||||||
|
manager.maintain_connection.take();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.projects.insert(project.downgrade());
|
||||||
|
if self.maintain_connection.is_none() {
|
||||||
|
self.maintain_connection = Some(cx.spawn({
|
||||||
|
let client = self.client.clone();
|
||||||
|
move |_, cx| Self::maintain_connection(manager, client.clone(), cx).log_err()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reconnected(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
|
let mut projects = HashMap::default();
|
||||||
|
|
||||||
|
let request = self.client.request_envelope(proto::RejoinRemoteProjects {
|
||||||
|
rejoined_projects: self
|
||||||
|
.projects
|
||||||
|
.iter()
|
||||||
|
.filter_map(|project| {
|
||||||
|
if let Some(handle) = project.upgrade() {
|
||||||
|
let project = handle.read(cx);
|
||||||
|
let project_id = project.remote_id()?;
|
||||||
|
projects.insert(project_id, handle.clone());
|
||||||
|
Some(proto::RejoinProject {
|
||||||
|
id: project_id,
|
||||||
|
worktrees: project
|
||||||
|
.worktrees()
|
||||||
|
.map(|worktree| {
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
proto::RejoinWorktree {
|
||||||
|
id: worktree.id().to_proto(),
|
||||||
|
scan_id: worktree.completed_scan_id() as u64,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let response = request.await?;
|
||||||
|
let message_id = response.message_id;
|
||||||
|
|
||||||
|
this.update(&mut cx, |_, cx| {
|
||||||
|
for rejoined_project in response.payload.rejoined_projects {
|
||||||
|
if let Some(project) = projects.get(&rejoined_project.id) {
|
||||||
|
project.update(cx, |project, cx| {
|
||||||
|
project.rejoined(rejoined_project, message_id, cx).log_err();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connection_lost(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
|
for project in self.projects.drain() {
|
||||||
|
if let Some(project) = project.upgrade() {
|
||||||
|
project.update(cx, |project, cx| {
|
||||||
|
project.disconnected_from_host(cx);
|
||||||
|
project.close(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.maintain_connection.take();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn maintain_connection(
|
||||||
|
this: WeakModel<Self>,
|
||||||
|
client: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut client_status = client.status();
|
||||||
|
loop {
|
||||||
|
let _ = client_status.try_recv();
|
||||||
|
|
||||||
|
let is_connected = client_status.borrow().is_connected();
|
||||||
|
// Even if we're initially connected, any future change of the status means we momentarily disconnected.
|
||||||
|
if !is_connected || client_status.next().await.is_some() {
|
||||||
|
log::info!("detected client disconnection");
|
||||||
|
|
||||||
|
// Wait for client to re-establish a connection to the server.
|
||||||
|
{
|
||||||
|
let mut reconnection_timeout =
|
||||||
|
cx.background_executor().timer(RECONNECT_TIMEOUT).fuse();
|
||||||
|
let client_reconnection = async {
|
||||||
|
let mut remaining_attempts = 3;
|
||||||
|
while remaining_attempts > 0 {
|
||||||
|
if client_status.borrow().is_connected() {
|
||||||
|
log::info!("client reconnected, attempting to rejoin projects");
|
||||||
|
|
||||||
|
let Some(this) = this.upgrade() else { break };
|
||||||
|
match this.update(&mut cx, |this, cx| this.reconnected(cx)) {
|
||||||
|
Ok(task) => {
|
||||||
|
if task.await.log_err().is_some() {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
remaining_attempts -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_app_dropped) => return false,
|
||||||
|
}
|
||||||
|
} else if client_status.borrow().is_signed_out() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"waiting for client status change, remaining attempts {}",
|
||||||
|
remaining_attempts
|
||||||
|
);
|
||||||
|
client_status.next().await;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
.fuse();
|
||||||
|
futures::pin_mut!(client_reconnection);
|
||||||
|
|
||||||
|
futures::select_biased! {
|
||||||
|
reconnected = client_reconnection => {
|
||||||
|
if reconnected {
|
||||||
|
log::info!("successfully reconnected");
|
||||||
|
// If we successfully joined the room, go back around the loop
|
||||||
|
// waiting for future connection status changes.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = reconnection_timeout => {
|
||||||
|
log::info!("rejoin project reconnection timeout expired");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The client failed to re-establish a connection to the server
|
||||||
|
// or an error occurred while trying to re-join the room. Either way
|
||||||
|
// we leave the room and return an error.
|
||||||
|
if let Some(this) = this.upgrade() {
|
||||||
|
log::info!("reconnection failed, disconnecting projects");
|
||||||
|
let _ = this.update(&mut cx, |this, cx| this.connection_lost(cx))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
pub mod connection_manager;
|
||||||
pub mod debounced_delay;
|
pub mod debounced_delay;
|
||||||
pub mod lsp_command;
|
pub mod lsp_command;
|
||||||
pub mod lsp_ext_command;
|
pub mod lsp_ext_command;
|
||||||
@ -234,6 +235,7 @@ enum BufferOrderedMessage {
|
|||||||
Resync,
|
Resync,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
enum LocalProjectUpdate {
|
enum LocalProjectUpdate {
|
||||||
WorktreesChanged,
|
WorktreesChanged,
|
||||||
CreateBufferForPeer {
|
CreateBufferForPeer {
|
||||||
@ -597,6 +599,7 @@ impl Project {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
|
pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
|
||||||
|
connection_manager::init(client.clone(), cx);
|
||||||
Self::init_settings(cx);
|
Self::init_settings(cx);
|
||||||
|
|
||||||
client.add_model_message_handler(Self::handle_add_collaborator);
|
client.add_model_message_handler(Self::handle_add_collaborator);
|
||||||
@ -733,6 +736,24 @@ impl Project {
|
|||||||
languages: Arc<LanguageRegistry>,
|
languages: Arc<LanguageRegistry>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
cx: AsyncAppContext,
|
cx: AsyncAppContext,
|
||||||
|
) -> Result<Model<Self>> {
|
||||||
|
let project =
|
||||||
|
Self::in_room(remote_id, client, user_store, languages, fs, cx.clone()).await?;
|
||||||
|
cx.update(|cx| {
|
||||||
|
connection_manager::Manager::global(cx).update(cx, |manager, cx| {
|
||||||
|
manager.maintain_project_connection(&project, cx)
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
Ok(project)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn in_room(
|
||||||
|
remote_id: u64,
|
||||||
|
client: Arc<Client>,
|
||||||
|
user_store: Model<UserStore>,
|
||||||
|
languages: Arc<LanguageRegistry>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
cx: AsyncAppContext,
|
||||||
) -> Result<Model<Self>> {
|
) -> Result<Model<Self>> {
|
||||||
client.authenticate_and_connect(true, &cx).await?;
|
client.authenticate_and_connect(true, &cx).await?;
|
||||||
|
|
||||||
@ -753,6 +774,7 @@ impl Project {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn from_join_project_response(
|
async fn from_join_project_response(
|
||||||
response: TypedEnvelope<proto::JoinProjectResponse>,
|
response: TypedEnvelope<proto::JoinProjectResponse>,
|
||||||
subscription: PendingEntitySubscription<Project>,
|
subscription: PendingEntitySubscription<Project>,
|
||||||
@ -1561,7 +1583,7 @@ impl Project {
|
|||||||
})
|
})
|
||||||
})?
|
})?
|
||||||
.await;
|
.await;
|
||||||
if update_project.is_ok() {
|
if update_project.log_err().is_some() {
|
||||||
for worktree in worktrees {
|
for worktree in worktrees {
|
||||||
worktree.update(&mut cx, |worktree, cx| {
|
worktree.update(&mut cx, |worktree, cx| {
|
||||||
let worktree = worktree.as_local_mut().unwrap();
|
let worktree = worktree.as_local_mut().unwrap();
|
||||||
|
@ -213,7 +213,21 @@ message Envelope {
|
|||||||
UpdateNotification update_notification = 174;
|
UpdateNotification update_notification = 174;
|
||||||
|
|
||||||
MultiLspQuery multi_lsp_query = 175;
|
MultiLspQuery multi_lsp_query = 175;
|
||||||
MultiLspQueryResponse multi_lsp_query_response = 176; // current max
|
MultiLspQueryResponse multi_lsp_query_response = 176;
|
||||||
|
|
||||||
|
CreateRemoteProject create_remote_project = 177;
|
||||||
|
CreateRemoteProjectResponse create_remote_project_response = 188; // current max
|
||||||
|
CreateDevServer create_dev_server = 178;
|
||||||
|
CreateDevServerResponse create_dev_server_response = 179;
|
||||||
|
ShutdownDevServer shutdown_dev_server = 180;
|
||||||
|
DevServerInstructions dev_server_instructions = 181;
|
||||||
|
ReconnectDevServer reconnect_dev_server = 182;
|
||||||
|
ReconnectDevServerResponse reconnect_dev_server_response = 183;
|
||||||
|
|
||||||
|
ShareRemoteProject share_remote_project = 184;
|
||||||
|
JoinRemoteProject join_remote_project = 185;
|
||||||
|
RejoinRemoteProjects rejoin_remote_projects = 186;
|
||||||
|
RejoinRemoteProjectsResponse rejoin_remote_projects_response = 187;
|
||||||
}
|
}
|
||||||
|
|
||||||
reserved 158 to 161;
|
reserved 158 to 161;
|
||||||
@ -249,6 +263,7 @@ enum ErrorCode {
|
|||||||
WrongMoveTarget = 11;
|
WrongMoveTarget = 11;
|
||||||
UnsharedItem = 12;
|
UnsharedItem = 12;
|
||||||
NoSuchProject = 13;
|
NoSuchProject = 13;
|
||||||
|
DevServerAlreadyOnline = 14;
|
||||||
reserved 6;
|
reserved 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,6 +295,13 @@ message RejoinRoom {
|
|||||||
repeated UpdateProject reshared_projects = 2;
|
repeated UpdateProject reshared_projects = 2;
|
||||||
repeated RejoinProject rejoined_projects = 3;
|
repeated RejoinProject rejoined_projects = 3;
|
||||||
}
|
}
|
||||||
|
message RejoinRemoteProjects {
|
||||||
|
repeated RejoinProject rejoined_projects = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RejoinRemoteProjectsResponse {
|
||||||
|
repeated RejoinedProject rejoined_projects = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message RejoinProject {
|
message RejoinProject {
|
||||||
uint64 id = 1;
|
uint64 id = 1;
|
||||||
@ -429,6 +451,52 @@ message JoinHostedProject {
|
|||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message CreateRemoteProject {
|
||||||
|
uint64 channel_id = 1;
|
||||||
|
string name = 2;
|
||||||
|
uint64 dev_server_id = 3;
|
||||||
|
string path = 4;
|
||||||
|
}
|
||||||
|
message CreateRemoteProjectResponse {
|
||||||
|
RemoteProject remote_project = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateDevServer {
|
||||||
|
uint64 channel_id = 1;
|
||||||
|
string name = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateDevServerResponse {
|
||||||
|
uint64 dev_server_id = 1;
|
||||||
|
uint64 channel_id = 2;
|
||||||
|
string access_token = 3;
|
||||||
|
string name = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ShutdownDevServer {
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReconnectDevServer {
|
||||||
|
repeated UpdateProject reshared_projects = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReconnectDevServerResponse {
|
||||||
|
repeated ResharedProject reshared_projects = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DevServerInstructions {
|
||||||
|
repeated RemoteProject projects = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ShareRemoteProject {
|
||||||
|
uint64 remote_project_id = 1;
|
||||||
|
repeated WorktreeMetadata worktrees = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message JoinRemoteProject {
|
||||||
|
uint64 remote_project_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message JoinProjectResponse {
|
message JoinProjectResponse {
|
||||||
uint64 project_id = 5;
|
uint64 project_id = 5;
|
||||||
uint32 replica_id = 1;
|
uint32 replica_id = 1;
|
||||||
@ -1057,6 +1125,12 @@ message UpdateChannels {
|
|||||||
|
|
||||||
repeated HostedProject hosted_projects = 10;
|
repeated HostedProject hosted_projects = 10;
|
||||||
repeated uint64 deleted_hosted_projects = 11;
|
repeated uint64 deleted_hosted_projects = 11;
|
||||||
|
|
||||||
|
repeated DevServer dev_servers = 12;
|
||||||
|
repeated uint64 deleted_dev_servers = 13;
|
||||||
|
|
||||||
|
repeated RemoteProject remote_projects = 14;
|
||||||
|
repeated uint64 deleted_remote_projects = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
message UpdateUserChannels {
|
message UpdateUserChannels {
|
||||||
@ -1092,6 +1166,27 @@ message HostedProject {
|
|||||||
ChannelVisibility visibility = 4;
|
ChannelVisibility visibility = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message RemoteProject {
|
||||||
|
uint64 id = 1;
|
||||||
|
optional uint64 project_id = 2;
|
||||||
|
uint64 channel_id = 3;
|
||||||
|
string name = 4;
|
||||||
|
uint64 dev_server_id = 5;
|
||||||
|
string path = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DevServer {
|
||||||
|
uint64 channel_id = 1;
|
||||||
|
uint64 dev_server_id = 2;
|
||||||
|
string name = 3;
|
||||||
|
DevServerStatus status = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DevServerStatus {
|
||||||
|
Offline = 0;
|
||||||
|
Online = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message JoinChannel {
|
message JoinChannel {
|
||||||
uint64 channel_id = 1;
|
uint64 channel_id = 1;
|
||||||
}
|
}
|
||||||
|
@ -299,6 +299,18 @@ messages!(
|
|||||||
(SetRoomParticipantRole, Foreground),
|
(SetRoomParticipantRole, Foreground),
|
||||||
(BlameBuffer, Foreground),
|
(BlameBuffer, Foreground),
|
||||||
(BlameBufferResponse, Foreground),
|
(BlameBufferResponse, Foreground),
|
||||||
|
(CreateRemoteProject, Foreground),
|
||||||
|
(CreateRemoteProjectResponse, Foreground),
|
||||||
|
(CreateDevServer, Foreground),
|
||||||
|
(CreateDevServerResponse, Foreground),
|
||||||
|
(DevServerInstructions, Foreground),
|
||||||
|
(ShutdownDevServer, Foreground),
|
||||||
|
(ReconnectDevServer, Foreground),
|
||||||
|
(ReconnectDevServerResponse, Foreground),
|
||||||
|
(ShareRemoteProject, Foreground),
|
||||||
|
(JoinRemoteProject, Foreground),
|
||||||
|
(RejoinRemoteProjects, Foreground),
|
||||||
|
(RejoinRemoteProjectsResponse, Foreground),
|
||||||
(MultiLspQuery, Background),
|
(MultiLspQuery, Background),
|
||||||
(MultiLspQueryResponse, Background),
|
(MultiLspQueryResponse, Background),
|
||||||
);
|
);
|
||||||
@ -392,6 +404,13 @@ request_messages!(
|
|||||||
(LspExtExpandMacro, LspExtExpandMacroResponse),
|
(LspExtExpandMacro, LspExtExpandMacroResponse),
|
||||||
(SetRoomParticipantRole, Ack),
|
(SetRoomParticipantRole, Ack),
|
||||||
(BlameBuffer, BlameBufferResponse),
|
(BlameBuffer, BlameBufferResponse),
|
||||||
|
(CreateRemoteProject, CreateRemoteProjectResponse),
|
||||||
|
(CreateDevServer, CreateDevServerResponse),
|
||||||
|
(ShutdownDevServer, Ack),
|
||||||
|
(ShareRemoteProject, ShareProjectResponse),
|
||||||
|
(JoinRemoteProject, JoinProjectResponse),
|
||||||
|
(RejoinRemoteProjects, RejoinRemoteProjectsResponse),
|
||||||
|
(ReconnectDevServer, ReconnectDevServerResponse),
|
||||||
(MultiLspQuery, MultiLspQueryResponse),
|
(MultiLspQuery, MultiLspQueryResponse),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ mod indicator;
|
|||||||
mod keybinding;
|
mod keybinding;
|
||||||
mod label;
|
mod label;
|
||||||
mod list;
|
mod list;
|
||||||
|
mod modal;
|
||||||
mod popover;
|
mod popover;
|
||||||
mod popover_menu;
|
mod popover_menu;
|
||||||
mod right_click_menu;
|
mod right_click_menu;
|
||||||
@ -32,6 +33,7 @@ pub use indicator::*;
|
|||||||
pub use keybinding::*;
|
pub use keybinding::*;
|
||||||
pub use label::*;
|
pub use label::*;
|
||||||
pub use list::*;
|
pub use list::*;
|
||||||
|
pub use modal::*;
|
||||||
pub use popover::*;
|
pub use popover::*;
|
||||||
pub use popover_menu::*;
|
pub use popover_menu::*;
|
||||||
pub use right_click_menu::*;
|
pub use right_click_menu::*;
|
||||||
|
@ -106,12 +106,14 @@ pub enum IconName {
|
|||||||
Settings,
|
Settings,
|
||||||
Screen,
|
Screen,
|
||||||
SelectAll,
|
SelectAll,
|
||||||
|
Server,
|
||||||
Shift,
|
Shift,
|
||||||
Snip,
|
Snip,
|
||||||
Space,
|
Space,
|
||||||
Split,
|
Split,
|
||||||
Tab,
|
Tab,
|
||||||
Terminal,
|
Terminal,
|
||||||
|
Trash,
|
||||||
Update,
|
Update,
|
||||||
WholeWord,
|
WholeWord,
|
||||||
XCircle,
|
XCircle,
|
||||||
@ -202,12 +204,14 @@ impl IconName {
|
|||||||
IconName::Settings => "icons/file_icons/settings.svg",
|
IconName::Settings => "icons/file_icons/settings.svg",
|
||||||
IconName::Screen => "icons/desktop.svg",
|
IconName::Screen => "icons/desktop.svg",
|
||||||
IconName::SelectAll => "icons/select_all.svg",
|
IconName::SelectAll => "icons/select_all.svg",
|
||||||
|
IconName::Server => "icons/server.svg",
|
||||||
IconName::Shift => "icons/shift.svg",
|
IconName::Shift => "icons/shift.svg",
|
||||||
IconName::Snip => "icons/snip.svg",
|
IconName::Snip => "icons/snip.svg",
|
||||||
IconName::Space => "icons/space.svg",
|
IconName::Space => "icons/space.svg",
|
||||||
IconName::Split => "icons/split.svg",
|
IconName::Split => "icons/split.svg",
|
||||||
IconName::Tab => "icons/tab.svg",
|
IconName::Tab => "icons/tab.svg",
|
||||||
IconName::Terminal => "icons/terminal.svg",
|
IconName::Terminal => "icons/terminal.svg",
|
||||||
|
IconName::Trash => "icons/trash.svg",
|
||||||
IconName::Update => "icons/update.svg",
|
IconName::Update => "icons/update.svg",
|
||||||
IconName::WholeWord => "icons/word_search.svg",
|
IconName::WholeWord => "icons/word_search.svg",
|
||||||
IconName::XCircle => "icons/error.svg",
|
IconName::XCircle => "icons/error.svg",
|
||||||
|
133
crates/ui/src/components/modal.rs
Normal file
133
crates/ui/src/components/modal.rs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
use gpui::*;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::{h_flex, IconButton, IconButtonShape, IconName, Label, LabelCommon, LabelSize};
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct ModalHeader {
|
||||||
|
id: ElementId,
|
||||||
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModalHeader {
|
||||||
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
children: SmallVec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for ModalHeader {
|
||||||
|
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||||
|
self.children.extend(elements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for ModalHeader {
|
||||||
|
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||||
|
h_flex()
|
||||||
|
.id(self.id)
|
||||||
|
.w_full()
|
||||||
|
.px_2()
|
||||||
|
.py_1p5()
|
||||||
|
.child(div().flex_1().children(self.children))
|
||||||
|
.justify_between()
|
||||||
|
.child(IconButton::new("dismiss", IconName::Close).shape(IconButtonShape::Square))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct ModalContent {
|
||||||
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModalContent {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
children: SmallVec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for ModalContent {
|
||||||
|
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||||
|
self.children.extend(elements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for ModalContent {
|
||||||
|
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||||
|
h_flex().w_full().px_2().py_1p5().children(self.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct ModalRow {
|
||||||
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModalRow {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
children: SmallVec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for ModalRow {
|
||||||
|
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||||
|
self.children.extend(elements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for ModalRow {
|
||||||
|
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||||
|
h_flex().w_full().px_2().py_1().children(self.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct SectionHeader {
|
||||||
|
/// The label of the header.
|
||||||
|
label: SharedString,
|
||||||
|
/// A slot for content that appears after the label, usually on the other side of the header.
|
||||||
|
/// This might be a button, a disclosure arrow, a face pile, etc.
|
||||||
|
end_slot: Option<AnyElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SectionHeader {
|
||||||
|
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||||
|
Self {
|
||||||
|
label: label.into(),
|
||||||
|
end_slot: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
|
||||||
|
self.end_slot = end_slot.into().map(IntoElement::into_any_element);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for SectionHeader {
|
||||||
|
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||||
|
h_flex().id(self.label.clone()).w_full().child(
|
||||||
|
div()
|
||||||
|
.h_7()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.w_full()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div().flex_1().child(
|
||||||
|
Label::new(self.label.clone())
|
||||||
|
.size(LabelSize::Large)
|
||||||
|
.into_element(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(h_flex().children(self.end_slot)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -4704,6 +4704,61 @@ pub fn join_hosted_project(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn join_remote_project(
|
||||||
|
project_id: ProjectId,
|
||||||
|
app_state: Arc<AppState>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Task<Result<WindowHandle<Workspace>>> {
|
||||||
|
let windows = cx.windows();
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let existing_workspace = windows.into_iter().find_map(|window| {
|
||||||
|
window.downcast::<Workspace>().and_then(|window| {
|
||||||
|
window
|
||||||
|
.update(&mut cx, |workspace, cx| {
|
||||||
|
if workspace.project().read(cx).remote_id() == Some(project_id.0) {
|
||||||
|
Some(window)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(None)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let workspace = if let Some(existing_workspace) = existing_workspace {
|
||||||
|
existing_workspace
|
||||||
|
} else {
|
||||||
|
let project = Project::remote(
|
||||||
|
project_id.0,
|
||||||
|
app_state.client.clone(),
|
||||||
|
app_state.user_store.clone(),
|
||||||
|
app_state.languages.clone(),
|
||||||
|
app_state.fs.clone(),
|
||||||
|
cx.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let window_bounds_override = window_bounds_env_override();
|
||||||
|
cx.update(|cx| {
|
||||||
|
let mut options = (app_state.build_window_options)(None, cx);
|
||||||
|
options.bounds = window_bounds_override;
|
||||||
|
cx.open_window(options, |cx| {
|
||||||
|
cx.new_view(|cx| {
|
||||||
|
Workspace::new(Default::default(), project, app_state.clone(), cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
|
workspace.update(&mut cx, |_, cx| {
|
||||||
|
cx.activate(true);
|
||||||
|
cx.activate_window();
|
||||||
|
})?;
|
||||||
|
|
||||||
|
anyhow::Ok(workspace)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn join_in_room_project(
|
pub fn join_in_room_project(
|
||||||
project_id: u64,
|
project_id: u64,
|
||||||
follow_user_id: u64,
|
follow_user_id: u64,
|
||||||
|
@ -48,6 +48,7 @@ fs.workspace = true
|
|||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
go_to_line.workspace = true
|
go_to_line.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
headless.workspace = true
|
||||||
image_viewer.workspace = true
|
image_viewer.workspace = true
|
||||||
install_cli.workspace = true
|
install_cli.workspace = true
|
||||||
isahc.workspace = true
|
isahc.workspace = true
|
||||||
|
@ -10,9 +10,7 @@ use backtrace::Backtrace;
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use clap::{command, Parser};
|
use clap::{command, Parser};
|
||||||
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
||||||
use client::{
|
use client::{parse_zed_link, telemetry::Telemetry, Client, DevServerToken, UserStore};
|
||||||
parse_zed_link, telemetry::Telemetry, Client, ClientSettings, DevServerToken, UserStore,
|
|
||||||
};
|
|
||||||
use collab_ui::channel_view::ChannelView;
|
use collab_ui::channel_view::ChannelView;
|
||||||
use copilot::Copilot;
|
use copilot::Copilot;
|
||||||
use copilot_ui::CopilotCompletionProvider;
|
use copilot_ui::CopilotCompletionProvider;
|
||||||
@ -88,7 +86,72 @@ fn fail_to_launch(e: anyhow::Error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn init_headless(dev_server_token: DevServerToken) {
|
||||||
|
if let Err(e) = init_paths() {
|
||||||
|
log::error!("Failed to launch: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
init_logger();
|
||||||
|
|
||||||
|
App::new().run(|cx| {
|
||||||
|
release_channel::init(env!("CARGO_PKG_VERSION"), cx);
|
||||||
|
if let Some(build_sha) = option_env!("ZED_COMMIT_SHA") {
|
||||||
|
AppCommitSha::set_global(AppCommitSha(build_sha.into()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut store = SettingsStore::default();
|
||||||
|
store
|
||||||
|
.set_default_settings(default_settings().as_ref(), cx)
|
||||||
|
.unwrap();
|
||||||
|
cx.set_global(store);
|
||||||
|
|
||||||
|
client::init_settings(cx);
|
||||||
|
|
||||||
|
let clock = Arc::new(clock::RealSystemClock);
|
||||||
|
let http = Arc::new(HttpClientWithUrl::new(
|
||||||
|
&client::ClientSettings::get_global(cx).server_url,
|
||||||
|
));
|
||||||
|
|
||||||
|
let client = client::Client::new(clock, http.clone(), cx);
|
||||||
|
let client = client.clone();
|
||||||
|
client.set_dev_server_token(dev_server_token);
|
||||||
|
|
||||||
|
project::Project::init(&client, cx);
|
||||||
|
client::init(&client, cx);
|
||||||
|
|
||||||
|
let git_binary_path = if option_env!("ZED_BUNDLE").as_deref() == Some("true") {
|
||||||
|
cx.path_for_auxiliary_executable("git")
|
||||||
|
.context("could not find git binary path")
|
||||||
|
.log_err()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let fs = Arc::new(RealFs::new(git_binary_path));
|
||||||
|
|
||||||
|
let mut languages =
|
||||||
|
LanguageRegistry::new(Task::ready(()), cx.background_executor().clone());
|
||||||
|
languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
|
||||||
|
let languages = Arc::new(languages);
|
||||||
|
let node_runtime = RealNodeRuntime::new(http.clone());
|
||||||
|
|
||||||
|
language::init(cx);
|
||||||
|
languages::init(languages.clone(), node_runtime.clone(), cx);
|
||||||
|
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||||
|
|
||||||
|
headless::init(
|
||||||
|
client.clone(),
|
||||||
|
headless::AppState {
|
||||||
|
languages: languages.clone(),
|
||||||
|
user_store: user_store.clone(),
|
||||||
|
fs: fs.clone(),
|
||||||
|
node_runtime: node_runtime.clone(),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_ui() {
|
||||||
menu::init();
|
menu::init();
|
||||||
zed_actions::init();
|
zed_actions::init();
|
||||||
|
|
||||||
@ -269,7 +332,6 @@ fn main() {
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
telemetry.flush_events();
|
telemetry.flush_events();
|
||||||
|
|
||||||
let app_state = Arc::new(AppState {
|
let app_state = Arc::new(AppState {
|
||||||
languages: languages.clone(),
|
languages: languages.clone(),
|
||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
@ -277,7 +339,7 @@ fn main() {
|
|||||||
fs: fs.clone(),
|
fs: fs.clone(),
|
||||||
build_window_options,
|
build_window_options,
|
||||||
workspace_store,
|
workspace_store,
|
||||||
node_runtime,
|
node_runtime: node_runtime.clone(),
|
||||||
});
|
});
|
||||||
AppState::set_global(Arc::downgrade(&app_state), cx);
|
AppState::set_global(Arc::downgrade(&app_state), cx);
|
||||||
|
|
||||||
@ -319,31 +381,17 @@ fn main() {
|
|||||||
|
|
||||||
cx.activate(true);
|
cx.activate(true);
|
||||||
|
|
||||||
let mut args = Args::parse();
|
let args = Args::parse();
|
||||||
if let Some(dev_server_token) = args.dev_server_token.take() {
|
|
||||||
let dev_server_token = DevServerToken(dev_server_token);
|
|
||||||
let server_url = ClientSettings::get_global(&cx).server_url.clone();
|
|
||||||
let client = client.clone();
|
|
||||||
client.set_dev_server_token(dev_server_token);
|
|
||||||
cx.spawn(|cx| async move {
|
|
||||||
client.authenticate_and_connect(false, &cx).await?;
|
|
||||||
log::info!("Connected to {}", server_url);
|
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
} else {
|
|
||||||
let urls: Vec<_> = args
|
|
||||||
.paths_or_urls
|
|
||||||
.iter()
|
|
||||||
.filter_map(|arg| parse_url_arg(arg, cx).log_err())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if !urls.is_empty() {
|
|
||||||
listener.open_urls(urls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut triggered_authentication = false;
|
let mut triggered_authentication = false;
|
||||||
|
let urls: Vec<_> = args
|
||||||
|
.paths_or_urls
|
||||||
|
.iter()
|
||||||
|
.filter_map(|arg| parse_url_arg(arg, cx).log_err())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !urls.is_empty() {
|
||||||
|
listener.open_urls(urls)
|
||||||
|
}
|
||||||
|
|
||||||
match open_rx
|
match open_rx
|
||||||
.try_next()
|
.try_next()
|
||||||
@ -382,6 +430,16 @@ fn main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut args = Args::parse();
|
||||||
|
if let Some(dev_server_token) = args.dev_server_token.take() {
|
||||||
|
let dev_server_token = DevServerToken(dev_server_token);
|
||||||
|
init_headless(dev_server_token)
|
||||||
|
} else {
|
||||||
|
init_ui()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_open_request(
|
fn handle_open_request(
|
||||||
request: OpenRequest,
|
request: OpenRequest,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
|
Loading…
Reference in New Issue
Block a user