Merge branch 'main' into allow-following-outside-of-projects

This commit is contained in:
Max Brunsfeld 2023-09-28 11:45:40 -07:00
commit a8b35eb8f5
147 changed files with 5383 additions and 1726 deletions

284
Cargo.lock generated
View File

@ -79,9 +79,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0"
checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
dependencies = [
"memchr",
]
@ -91,36 +91,25 @@ name = "ai"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"client",
"collections",
"ctor",
"editor",
"env_logger 0.9.3",
"fs",
"async-trait",
"bincode",
"futures 0.3.28",
"gpui",
"indoc",
"isahc",
"language",
"lazy_static",
"log",
"menu",
"matrixmultiply",
"ordered-float",
"parking_lot 0.11.2",
"project",
"parse_duration",
"postage",
"rand 0.8.5",
"regex",
"schemars",
"search",
"rusqlite",
"serde",
"serde_json",
"settings",
"smol",
"theme",
"tiktoken-rs 0.4.5",
"tiktoken-rs 0.5.4",
"util",
"uuid 1.4.1",
"workspace",
]
[[package]]
@ -305,6 +294,44 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
[[package]]
name = "assistant"
version = "0.1.0"
dependencies = [
"ai",
"anyhow",
"chrono",
"client",
"collections",
"ctor",
"editor",
"env_logger 0.9.3",
"fs",
"futures 0.3.28",
"gpui",
"indoc",
"isahc",
"language",
"log",
"menu",
"ordered-float",
"parking_lot 0.11.2",
"project",
"rand 0.8.5",
"regex",
"schemars",
"search",
"serde",
"serde_json",
"settings",
"smol",
"theme",
"tiktoken-rs 0.4.5",
"util",
"uuid 1.4.1",
"workspace",
]
[[package]]
name = "async-broadcast"
version = "0.4.1"
@ -2144,7 +2171,7 @@ dependencies = [
"convert_case 0.4.0",
"proc-macro2",
"quote",
"rustc_version 0.4.0",
"rustc_version",
"syn 1.0.109",
]
@ -2586,6 +2613,7 @@ dependencies = [
name = "file_finder"
version = "0.1.0"
dependencies = [
"collections",
"ctor",
"editor",
"env_logger 0.9.3",
@ -3237,7 +3265,7 @@ dependencies = [
"indexmap 1.9.3",
"slab",
"tokio",
"tokio-util 0.7.8",
"tokio-util 0.7.9",
"tracing",
]
@ -3358,9 +3386,9 @@ dependencies = [
[[package]]
name = "hermit-abi"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
[[package]]
name = "hex"
@ -3654,7 +3682,7 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
"hermit-abi 0.3.2",
"hermit-abi 0.3.3",
"libc",
"windows-sys",
]
@ -3711,8 +3739,8 @@ version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi 0.3.2",
"rustix 0.38.13",
"hermit-abi 0.3.3",
"rustix 0.38.14",
"windows-sys",
]
@ -4280,9 +4308,9 @@ checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb"
[[package]]
name = "matrixmultiply"
version = "0.3.7"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77"
checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2"
dependencies = [
"autocfg",
"rawpointer",
@ -4556,6 +4584,19 @@ dependencies = [
"tempfile",
]
[[package]]
name = "ndarray"
version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32"
dependencies = [
"matrixmultiply",
"num-complex 0.4.4",
"num-integer",
"num-traits",
"rawpointer",
]
[[package]]
name = "ndk"
version = "0.7.0"
@ -4682,7 +4723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36"
dependencies = [
"num-bigint 0.2.6",
"num-complex",
"num-complex 0.2.4",
"num-integer",
"num-iter",
"num-rational 0.2.4",
@ -4738,6 +4779,15 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214"
dependencies = [
"num-traits",
]
[[package]]
name = "num-derive"
version = "0.3.3"
@ -4809,7 +4859,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi 0.3.2",
"hermit-abi 0.3.3",
"libc",
]
@ -4846,7 +4896,7 @@ dependencies = [
"rmp",
"rmpv",
"tokio",
"tokio-util 0.7.8",
"tokio-util 0.7.9",
]
[[package]]
@ -5147,11 +5197,11 @@ dependencies = [
[[package]]
name = "pathfinder_simd"
version = "0.5.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff"
checksum = "0444332826c70dc47be74a7c6a5fc44e23a7905ad6858d4162b658320455ef93"
dependencies = [
"rustc_version 0.3.3",
"rustc_version",
]
[[package]]
@ -5186,17 +5236,6 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "pest"
version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33"
dependencies = [
"memchr",
"thiserror",
"ucd-trie",
]
[[package]]
name = "petgraph"
version = "0.6.4"
@ -5732,7 +5771,7 @@ dependencies = [
name = "quick_action_bar"
version = "0.1.0"
dependencies = [
"ai",
"assistant",
"editor",
"gpui",
"search",
@ -5868,9 +5907,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "rayon"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
dependencies = [
"either",
"rayon-core",
@ -5878,14 +5917,12 @@ dependencies = [
[[package]]
name = "rayon-core"
version = "1.11.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"num_cpus",
]
[[package]]
@ -6335,22 +6372,13 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
dependencies = [
"semver 0.11.0",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver 1.0.18",
"semver",
]
[[package]]
@ -6385,9 +6413,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.13"
version = "0.38.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662"
checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
dependencies = [
"bitflags 2.4.0",
"errno 0.3.3",
@ -6736,9 +6764,9 @@ dependencies = [
name = "semantic_index"
version = "0.1.0"
dependencies = [
"ai",
"anyhow",
"async-trait",
"bincode",
"client",
"collections",
"ctor",
@ -6747,15 +6775,13 @@ dependencies = [
"futures 0.3.28",
"globset",
"gpui",
"isahc",
"language",
"lazy_static",
"log",
"matrixmultiply",
"ndarray",
"node_runtime",
"ordered-float",
"parking_lot 0.11.2",
"parse_duration",
"picker",
"postage",
"pretty_assertions",
@ -6789,30 +6815,12 @@ dependencies = [
"zed",
]
[[package]]
name = "semver"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "semver-parser"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
dependencies = [
"pest",
]
[[package]]
name = "seq-macro"
version = "0.2.2"
@ -6982,9 +6990,9 @@ dependencies = [
[[package]]
name = "sha1"
version = "0.10.5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if 1.0.0",
"cpufeatures",
@ -7157,9 +7165,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
[[package]]
name = "smol"
@ -7394,13 +7402,16 @@ name = "storybook"
version = "0.1.0"
dependencies = [
"anyhow",
"clap 4.4.4",
"gpui2",
"log",
"rust-embed",
"serde",
"settings",
"simplelog",
"strum",
"theme",
"ui",
"util",
]
@ -7421,6 +7432,28 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.37",
]
[[package]]
name = "subtle"
version = "2.4.1"
@ -7440,15 +7473,15 @@ dependencies = [
[[package]]
name = "sval"
version = "2.6.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b031320a434d3e9477ccf9b5756d57d4272937b8d22cb88af80b7633a1b78b1"
checksum = "05d11eec9fbe2bc8bc71e7349f0e7534db9a96d961fb9f302574275b7880ad06"
[[package]]
name = "sval_buffer"
version = "2.6.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bf7e9412af26b342f3f2cc5cc4122b0105e9d16eb76046cd14ed10106cf6028"
checksum = "6b7451f69a93c5baf2653d5aa8bb4178934337f16c22830a50b06b386f72d761"
dependencies = [
"sval",
"sval_ref",
@ -7456,18 +7489,18 @@ dependencies = [
[[package]]
name = "sval_dynamic"
version = "2.6.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0ef628e8a77a46ed3338db8d1b08af77495123cc229453084e47cd716d403cf"
checksum = "c34f5a2cc12b4da2adfb59d5eedfd9b174a23cc3fae84cec71dcbcd9302068f5"
dependencies = [
"sval",
]
[[package]]
name = "sval_fmt"
version = "2.6.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326"
checksum = "2f578b2301341e246d00b35957f2952c4ec554ad9c7cfaee10bc86bc92896578"
dependencies = [
"itoa",
"ryu",
@ -7476,9 +7509,9 @@ dependencies = [
[[package]]
name = "sval_json"
version = "2.6.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d"
checksum = "8346c00f5dc6efe18bea8d13c1f7ca4f112b20803434bf3657ac17c0f74cbc4b"
dependencies = [
"itoa",
"ryu",
@ -7487,18 +7520,18 @@ dependencies = [
[[package]]
name = "sval_ref"
version = "2.6.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703ca1942a984bd0d9b5a4c0a65ab8b4b794038d080af4eb303c71bc6bf22d7c"
checksum = "6617cc89952f792aebc0f4a1a76bc51e80c70b18c491bd52215c7989c4c3dd06"
dependencies = [
"sval",
]
[[package]]
name = "sval_serde"
version = "2.6.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830926cd0581f7c3e5d51efae4d35c6b6fc4db583842652891ba2f1bed8db046"
checksum = "fe3d1e59f023341d9af75d86f3bc148a6704f3f831eef0dd90bbe9cb445fa024"
dependencies = [
"serde",
"sval",
@ -7649,7 +7682,7 @@ dependencies = [
"cfg-if 1.0.0",
"fastrand 2.0.0",
"redox_syscall 0.3.5",
"rustix 0.38.13",
"rustix 0.38.14",
"windows-sys",
]
@ -8051,9 +8084,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.8"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d"
dependencies = [
"bytes 1.5.0",
"futures-core",
@ -8152,7 +8185,7 @@ dependencies = [
"rand 0.8.5",
"slab",
"tokio",
"tokio-util 0.7.8",
"tokio-util 0.7.9",
"tower-layer",
"tower-service",
"tracing",
@ -8598,10 +8631,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ucd-trie"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
name = "ui"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui2",
"serde",
"settings",
"theme",
]
[[package]]
name = "unicase"
@ -8671,9 +8709,9 @@ checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
[[package]]
name = "unicode-width"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "unicode_categories"
@ -8864,6 +8902,7 @@ dependencies = [
"async-trait",
"collections",
"command_palette",
"diagnostics",
"editor",
"futures 0.3.28",
"gpui",
@ -8885,6 +8924,7 @@ dependencies = [
"tokio",
"util",
"workspace",
"zed-actions",
]
[[package]]
@ -9386,7 +9426,7 @@ dependencies = [
"either",
"home",
"once_cell",
"rustix 0.38.13",
"rustix 0.38.14",
]
[[package]]
@ -9471,9 +9511,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
"winapi 0.3.9",
]
@ -9796,11 +9836,11 @@ dependencies = [
[[package]]
name = "zed"
version = "0.106.0"
version = "0.107.0"
dependencies = [
"activity_indicator",
"ai",
"anyhow",
"assistant",
"async-compression",
"async-recursion 0.3.2",
"async-tar",
@ -9865,12 +9905,14 @@ dependencies = [
"rpc",
"rsa",
"rust-embed",
"schemars",
"search",
"semantic_index",
"serde",
"serde_derive",
"serde_json",
"settings",
"shellexpand",
"simplelog",
"smallvec",
"smol",

View File

@ -2,6 +2,7 @@
members = [
"crates/activity_indicator",
"crates/ai",
"crates/assistant",
"crates/audio",
"crates/auto_update",
"crates/breadcrumbs",
@ -69,6 +70,7 @@ members = [
"crates/text",
"crates/theme",
"crates/theme_selector",
"crates/ui",
"crates/util",
"crates/semantic_index",
"crates/vim",

View File

@ -13,7 +13,7 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
sudo xcodebuild -license
```
* Install homebrew, node and rustup-init (rutup, rust, cargo, etc.)
* Install homebrew, node and rustup-init (rustup, rust, cargo, etc.)
```
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install node rustup-init
@ -36,7 +36,7 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
brew install foreman
```
* Ensure the Zed.dev website is checked out in a sibling directory and install it's dependencies:
* Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies:
```
cd ..

View File

@ -30,6 +30,7 @@
"cmd-s": "workspace::Save",
"cmd-shift-s": "workspace::SaveAs",
"cmd-=": "zed::IncreaseBufferFontSize",
"cmd-+": "zed::IncreaseBufferFontSize",
"cmd--": "zed::DecreaseBufferFontSize",
"cmd-0": "zed::ResetBufferFontSize",
"cmd-,": "zed::OpenSettings",
@ -249,6 +250,7 @@
"bindings": {
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ActivateRegexMode",
"alt-cmd-s": "search::ActivateSemanticMode",
"alt-cmd-x": "search::ActivateTextMode"
@ -261,11 +263,19 @@
"down": "search::NextHistoryQuery"
}
},
{
"context": "ProjectSearchBar && in_replace",
"bindings": {
"enter": "search::ReplaceNext",
"cmd-enter": "search::ReplaceAll"
}
},
{
"context": "ProjectSearchView",
"bindings": {
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ActivateRegexMode",
"alt-cmd-s": "search::ActivateSemanticMode",
"alt-cmd-x": "search::ActivateTextMode"
@ -277,6 +287,7 @@
"cmd-f": "project_search::ToggleFocus",
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
"cmd-shift-h": "search::ToggleReplace",
"alt-enter": "search::SelectAllMatches",
"alt-cmd-c": "search::ToggleCaseSensitive",
"alt-cmd-w": "search::ToggleWholeWord",
@ -498,6 +509,22 @@
"cmd-k cmd-down": [
"workspace::ActivatePaneInDirection",
"Down"
],
"cmd-k shift-left": [
"workspace::SwapPaneInDirection",
"Left"
],
"cmd-k shift-right": [
"workspace::SwapPaneInDirection",
"Right"
],
"cmd-k shift-up": [
"workspace::SwapPaneInDirection",
"Up"
],
"cmd-k shift-down": [
"workspace::SwapPaneInDirection",
"Down"
]
}
},
@ -562,7 +589,7 @@
}
},
{
"context": "ProjectSearchBar",
"context": "ProjectSearchBar && !in_replace",
"bindings": {
"cmd-enter": "project_search::SearchInNew"
}
@ -588,14 +615,20 @@
}
},
{
"context": "CollabPanel",
"context": "CollabPanel && not_editing",
"bindings": {
"ctrl-backspace": "collab_panel::Remove",
"space": "menu::Confirm"
}
},
{
"context": "CollabPanel > Editor",
"context": "(CollabPanel && editing) > Editor",
"bindings": {
"space": "collab_panel::InsertSpace"
}
},
{
"context": "(CollabPanel && not_editing) > Editor",
"bindings": {
"cmd-c": "collab_panel::StartLinkChannel",
"cmd-x": "collab_panel::StartMoveChannel",

View File

@ -18,6 +18,7 @@
}
}
],
":": "command_palette::Toggle",
"h": "vim::Left",
"left": "vim::Left",
"backspace": "vim::Backspace",
@ -125,6 +126,21 @@
"g shift-t": "pane::ActivatePrevItem",
"g d": "editor::GoToDefinition",
"g shift-d": "editor::GoToTypeDefinition",
"g n": "vim::SelectNext",
"g shift-n": "vim::SelectPrevious",
"g >": [
"editor::SelectNext",
{
"replace_newest": true
}
],
"g <": [
"editor::SelectPrevious",
{
"replace_newest": true
}
],
"g a": "editor::SelectAllMatches",
"g s": "outline::Toggle",
"g shift-s": "project_symbols::Toggle",
"g .": "editor::ToggleCodeActions", // zed specific
@ -205,13 +221,13 @@
"shift-z shift-q": [
"pane::CloseActiveItem",
{
"saveBehavior": "dontSave"
"saveIntent": "skip"
}
],
"shift-z shift-z": [
"pane::CloseActiveItem",
{
"saveBehavior": "promptOnConflict"
"saveIntent": "saveAll"
}
],
// Count support
@ -300,6 +316,38 @@
"workspace::ActivatePaneInDirection",
"Down"
],
"ctrl-w shift-left": [
"workspace::SwapPaneInDirection",
"Left"
],
"ctrl-w shift-right": [
"workspace::SwapPaneInDirection",
"Right"
],
"ctrl-w shift-up": [
"workspace::SwapPaneInDirection",
"Up"
],
"ctrl-w shift-down": [
"workspace::SwapPaneInDirection",
"Down"
],
"ctrl-w shift-h": [
"workspace::SwapPaneInDirection",
"Left"
],
"ctrl-w shift-l": [
"workspace::SwapPaneInDirection",
"Right"
],
"ctrl-w shift-k": [
"workspace::SwapPaneInDirection",
"Up"
],
"ctrl-w shift-j": [
"workspace::SwapPaneInDirection",
"Down"
],
"ctrl-w g t": "pane::ActivateNextItem",
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
"ctrl-w g shift-t": "pane::ActivatePrevItem",
@ -318,7 +366,17 @@
"ctrl-w c": "pane::CloseAllItems",
"ctrl-w ctrl-c": "pane::CloseAllItems",
"ctrl-w q": "pane::CloseAllItems",
"ctrl-w ctrl-q": "pane::CloseAllItems"
"ctrl-w ctrl-q": "pane::CloseAllItems",
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w n": [
"workspace::NewFileInDirection",
"Up"
],
"ctrl-w ctrl-n": [
"workspace::NewFileInDirection",
"Up"
]
}
},
{

View File

@ -372,6 +372,27 @@
"semantic_index": {
"enabled": false
},
// Settings specific to our elixir integration
"elixir": {
// Set Zed to use the experimental Next LS LSP server.
// Note that changing this setting requires a restart of Zed
// to take effect.
//
// May take 3 values:
// 1. Use the standard elixir-ls LSP server
// "next": "off"
// 2. Use a bundled version of the next Next LS LSP server
// "next": "on",
// 3. Use a local build of the next Next LS LSP server:
// "next": {
// "local": {
// "path": "~/next-ls/bin/start",
// "arguments": ["--stdio"]
// }
// },
//
"next": "off"
},
// Different settings for specific languages.
"languages": {
"Plain Text": {

View File

@ -9,39 +9,26 @@ path = "src/ai.rs"
doctest = false
[dependencies]
client = { path = "../client" }
collections = { path = "../collections"}
editor = { path = "../editor" }
fs = { path = "../fs" }
gpui = { path = "../gpui" }
language = { path = "../language" }
menu = { path = "../menu" }
search = { path = "../search" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
uuid = { version = "1.1.2", features = ["v4"] }
workspace = { path = "../workspace" }
async-trait.workspace = true
anyhow.workspace = true
chrono = { version = "0.4", features = ["serde"] }
futures.workspace = true
indoc.workspace = true
isahc.workspace = true
lazy_static.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
isahc.workspace = true
regex.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
tiktoken-rs = "0.4"
postage.workspace = true
rand.workspace = true
log.workspace = true
parse_duration = "2.1.1"
tiktoken-rs = "0.5.0"
matrixmultiply = "0.3.7"
rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] }
bincode = "1.3.3"
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
log.workspace = true
rand.workspace = true
gpui = { path = "../gpui", features = ["test-support"] }

View File

@ -1,294 +1,2 @@
pub mod assistant;
mod assistant_settings;
mod codegen;
mod streaming_diff;
use anyhow::{anyhow, Result};
pub use assistant::AssistantPanel;
use assistant_settings::OpenAIModel;
use chrono::{DateTime, Local};
use collections::HashMap;
use fs::Fs;
use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
use gpui::{executor::Background, AppContext};
use isahc::{http::StatusCode, Request, RequestExt};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{
cmp::Reverse,
ffi::OsStr,
fmt::{self, Display},
io,
path::PathBuf,
sync::Arc,
};
use util::paths::CONVERSATIONS_DIR;
const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
// Data types for chat completion requests
#[derive(Debug, Default, Serialize)]
pub struct OpenAIRequest {
model: String,
messages: Vec<RequestMessage>,
stream: bool,
}
#[derive(
Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
struct MessageId(usize);
#[derive(Clone, Debug, Serialize, Deserialize)]
struct MessageMetadata {
role: Role,
sent_at: DateTime<Local>,
status: MessageStatus,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
enum MessageStatus {
Pending,
Done,
Error(Arc<str>),
}
#[derive(Serialize, Deserialize)]
struct SavedMessage {
id: MessageId,
start: usize,
}
#[derive(Serialize, Deserialize)]
struct SavedConversation {
id: Option<String>,
zed: String,
version: String,
text: String,
messages: Vec<SavedMessage>,
message_metadata: HashMap<MessageId, MessageMetadata>,
summary: String,
model: OpenAIModel,
}
impl SavedConversation {
const VERSION: &'static str = "0.1.0";
}
struct SavedConversationMetadata {
title: String,
path: PathBuf,
mtime: chrono::DateTime<chrono::Local>,
}
impl SavedConversationMetadata {
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
fs.create_dir(&CONVERSATIONS_DIR).await?;
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
let mut conversations = Vec::<SavedConversationMetadata>::new();
while let Some(path) = paths.next().await {
let path = path?;
if path.extension() != Some(OsStr::new("json")) {
continue;
}
let pattern = r" - \d+.zed.json$";
let re = Regex::new(pattern).unwrap();
let metadata = fs.metadata(&path).await?;
if let Some((file_name, metadata)) = path
.file_name()
.and_then(|name| name.to_str())
.zip(metadata)
{
let title = re.replace(file_name, "");
conversations.push(Self {
title: title.into_owned(),
path,
mtime: metadata.mtime.into(),
});
}
}
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
Ok(conversations)
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct RequestMessage {
role: Role,
content: String,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct ResponseMessage {
role: Option<Role>,
content: Option<String>,
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
enum Role {
User,
Assistant,
System,
}
impl Role {
pub fn cycle(&mut self) {
*self = match self {
Role::User => Role::Assistant,
Role::Assistant => Role::System,
Role::System => Role::User,
}
}
}
impl Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Role::User => write!(f, "User"),
Role::Assistant => write!(f, "Assistant"),
Role::System => write!(f, "System"),
}
}
}
#[derive(Deserialize, Debug)]
pub struct OpenAIResponseStreamEvent {
pub id: Option<String>,
pub object: String,
pub created: u32,
pub model: String,
pub choices: Vec<ChatChoiceDelta>,
pub usage: Option<Usage>,
}
#[derive(Deserialize, Debug)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(Deserialize, Debug)]
pub struct ChatChoiceDelta {
pub index: u32,
pub delta: ResponseMessage,
pub finish_reason: Option<String>,
}
#[derive(Deserialize, Debug)]
struct OpenAIUsage {
prompt_tokens: u64,
completion_tokens: u64,
total_tokens: u64,
}
#[derive(Deserialize, Debug)]
struct OpenAIChoice {
text: String,
index: u32,
logprobs: Option<serde_json::Value>,
finish_reason: Option<String>,
}
pub fn init(cx: &mut AppContext) {
assistant::init(cx);
}
pub async fn stream_completion(
api_key: String,
executor: Arc<Background>,
mut request: OpenAIRequest,
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
request.stream = true;
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
let json_data = serde_json::to_string(&request)?;
let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.body(json_data)?
.send_async()
.await?;
let status = response.status();
if status == StatusCode::OK {
executor
.spawn(async move {
let mut lines = BufReader::new(response.body_mut()).lines();
fn parse_line(
line: Result<String, io::Error>,
) -> Result<Option<OpenAIResponseStreamEvent>> {
if let Some(data) = line?.strip_prefix("data: ") {
let event = serde_json::from_str(&data)?;
Ok(Some(event))
} else {
Ok(None)
}
}
while let Some(line) = lines.next().await {
if let Some(event) = parse_line(line).transpose() {
let done = event.as_ref().map_or(false, |event| {
event
.choices
.last()
.map_or(false, |choice| choice.finish_reason.is_some())
});
if tx.unbounded_send(event).is_err() {
break;
}
if done {
break;
}
}
}
anyhow::Ok(())
})
.detach();
Ok(rx)
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
#[derive(Deserialize)]
struct OpenAIResponse {
error: OpenAIError,
}
#[derive(Deserialize)]
struct OpenAIError {
message: String,
}
match serde_json::from_str::<OpenAIResponse>(&body) {
Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
"Failed to connect to OpenAI API: {}",
response.error.message,
)),
_ => Err(anyhow!(
"Failed to connect to OpenAI API: {} {}",
response.status(),
body,
)),
}
}
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}
pub mod completion;
pub mod embedding;

212
crates/ai/src/completion.rs Normal file
View File

@ -0,0 +1,212 @@
use anyhow::{anyhow, Result};
use futures::{
future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt,
Stream, StreamExt,
};
use gpui::executor::Background;
use isahc::{http::StatusCode, Request, RequestExt};
use serde::{Deserialize, Serialize};
use std::{
fmt::{self, Display},
io,
sync::Arc,
};
pub const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
System,
}
impl Role {
pub fn cycle(&mut self) {
*self = match self {
Role::User => Role::Assistant,
Role::Assistant => Role::System,
Role::System => Role::User,
}
}
}
impl Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Role::User => write!(f, "User"),
Role::Assistant => write!(f, "Assistant"),
Role::System => write!(f, "System"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct RequestMessage {
pub role: Role,
pub content: String,
}
#[derive(Debug, Default, Serialize)]
pub struct OpenAIRequest {
pub model: String,
pub messages: Vec<RequestMessage>,
pub stream: bool,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct ResponseMessage {
pub role: Option<Role>,
pub content: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct OpenAIUsage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(Deserialize, Debug)]
pub struct ChatChoiceDelta {
pub index: u32,
pub delta: ResponseMessage,
pub finish_reason: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct OpenAIResponseStreamEvent {
pub id: Option<String>,
pub object: String,
pub created: u32,
pub model: String,
pub choices: Vec<ChatChoiceDelta>,
pub usage: Option<OpenAIUsage>,
}
pub async fn stream_completion(
api_key: String,
executor: Arc<Background>,
mut request: OpenAIRequest,
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
request.stream = true;
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
let json_data = serde_json::to_string(&request)?;
let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.body(json_data)?
.send_async()
.await?;
let status = response.status();
if status == StatusCode::OK {
executor
.spawn(async move {
let mut lines = BufReader::new(response.body_mut()).lines();
fn parse_line(
line: Result<String, io::Error>,
) -> Result<Option<OpenAIResponseStreamEvent>> {
if let Some(data) = line?.strip_prefix("data: ") {
let event = serde_json::from_str(&data)?;
Ok(Some(event))
} else {
Ok(None)
}
}
while let Some(line) = lines.next().await {
if let Some(event) = parse_line(line).transpose() {
let done = event.as_ref().map_or(false, |event| {
event
.choices
.last()
.map_or(false, |choice| choice.finish_reason.is_some())
});
if tx.unbounded_send(event).is_err() {
break;
}
if done {
break;
}
}
}
anyhow::Ok(())
})
.detach();
Ok(rx)
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
#[derive(Deserialize)]
struct OpenAIResponse {
error: OpenAIError,
}
#[derive(Deserialize)]
struct OpenAIError {
message: String,
}
match serde_json::from_str::<OpenAIResponse>(&body) {
Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
"Failed to connect to OpenAI API: {}",
response.error.message,
)),
_ => Err(anyhow!(
"Failed to connect to OpenAI API: {} {}",
response.status(),
body,
)),
}
}
}
pub trait CompletionProvider {
fn complete(
&self,
prompt: OpenAIRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
}
pub struct OpenAICompletionProvider {
api_key: String,
executor: Arc<Background>,
}
impl OpenAICompletionProvider {
pub fn new(api_key: String, executor: Arc<Background>) -> Self {
Self { api_key, executor }
}
}
impl CompletionProvider for OpenAICompletionProvider {
fn complete(
&self,
prompt: OpenAIRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt);
async move {
let response = request.await?;
let stream = response
.filter_map(|response| async move {
match response {
Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
Err(error) => Some(Err(error)),
}
})
.boxed();
Ok(stream)
}
.boxed()
}
}

View File

@ -27,8 +27,30 @@ lazy_static! {
}
#[derive(Debug, PartialEq, Clone)]
pub struct Embedding(Vec<f32>);
pub struct Embedding(pub Vec<f32>);
// This is needed for semantic index functionality
// Unfortunately it has to live wherever the "Embedding" struct is created.
// Keeping this in here though, introduces a 'rusqlite' dependency into AI
// which is less than ideal
impl FromSql for Embedding {
fn column_result(value: ValueRef) -> FromSqlResult<Self> {
let bytes = value.as_blob()?;
let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
if embedding.is_err() {
return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
}
Ok(Embedding(embedding.unwrap()))
}
}
impl ToSql for Embedding {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
let bytes = bincode::serialize(&self.0)
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
}
}
impl From<Vec<f32>> for Embedding {
fn from(value: Vec<f32>) -> Self {
Embedding(value)
@ -63,24 +85,24 @@ impl Embedding {
}
}
impl FromSql for Embedding {
fn column_result(value: ValueRef) -> FromSqlResult<Self> {
let bytes = value.as_blob()?;
let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
if embedding.is_err() {
return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
}
Ok(Embedding(embedding.unwrap()))
}
}
// impl FromSql for Embedding {
// fn column_result(value: ValueRef) -> FromSqlResult<Self> {
// let bytes = value.as_blob()?;
// let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
// if embedding.is_err() {
// return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
// }
// Ok(Embedding(embedding.unwrap()))
// }
// }
impl ToSql for Embedding {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
let bytes = bincode::serialize(&self.0)
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
}
}
// impl ToSql for Embedding {
// fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
// let bytes = bincode::serialize(&self.0)
// .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
// Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
// }
// }
#[derive(Clone)]
pub struct OpenAIEmbeddings {
@ -117,6 +139,7 @@ struct OpenAIEmbeddingUsage {
#[async_trait]
pub trait EmbeddingProvider: Sync + Send {
fn is_authenticated(&self) -> bool;
async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>>;
fn max_tokens_per_batch(&self) -> usize;
fn truncate(&self, span: &str) -> (String, usize);
@ -127,6 +150,9 @@ pub struct DummyEmbeddings {}
#[async_trait]
impl EmbeddingProvider for DummyEmbeddings {
fn is_authenticated(&self) -> bool {
true
}
fn rate_limit_expiration(&self) -> Option<Instant> {
None
}
@ -229,6 +255,9 @@ impl OpenAIEmbeddings {
#[async_trait]
impl EmbeddingProvider for OpenAIEmbeddings {
fn is_authenticated(&self) -> bool {
OPENAI_API_KEY.as_ref().is_some()
}
fn max_tokens_per_batch(&self) -> usize {
50000
}

View File

@ -0,0 +1,48 @@
[package]
name = "assistant"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/assistant.rs"
doctest = false
[dependencies]
ai = { path = "../ai" }
client = { path = "../client" }
collections = { path = "../collections"}
editor = { path = "../editor" }
fs = { path = "../fs" }
gpui = { path = "../gpui" }
language = { path = "../language" }
menu = { path = "../menu" }
search = { path = "../search" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
uuid = { version = "1.1.2", features = ["v4"] }
workspace = { path = "../workspace" }
anyhow.workspace = true
chrono = { version = "0.4", features = ["serde"] }
futures.workspace = true
indoc.workspace = true
isahc.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
regex.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
tiktoken-rs = "0.4"
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
log.workspace = true
rand.workspace = true

View File

@ -0,0 +1,112 @@
pub mod assistant_panel;
mod assistant_settings;
mod codegen;
mod streaming_diff;
use ai::completion::Role;
use anyhow::Result;
pub use assistant_panel::AssistantPanel;
use assistant_settings::OpenAIModel;
use chrono::{DateTime, Local};
use collections::HashMap;
use fs::Fs;
use futures::StreamExt;
use gpui::AppContext;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
use util::paths::CONVERSATIONS_DIR;
#[derive(
Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
struct MessageId(usize);
#[derive(Clone, Debug, Serialize, Deserialize)]
struct MessageMetadata {
role: Role,
sent_at: DateTime<Local>,
status: MessageStatus,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
enum MessageStatus {
Pending,
Done,
Error(Arc<str>),
}
#[derive(Serialize, Deserialize)]
struct SavedMessage {
id: MessageId,
start: usize,
}
#[derive(Serialize, Deserialize)]
struct SavedConversation {
id: Option<String>,
zed: String,
version: String,
text: String,
messages: Vec<SavedMessage>,
message_metadata: HashMap<MessageId, MessageMetadata>,
summary: String,
model: OpenAIModel,
}
impl SavedConversation {
const VERSION: &'static str = "0.1.0";
}
struct SavedConversationMetadata {
title: String,
path: PathBuf,
mtime: chrono::DateTime<chrono::Local>,
}
impl SavedConversationMetadata {
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
fs.create_dir(&CONVERSATIONS_DIR).await?;
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
let mut conversations = Vec::<SavedConversationMetadata>::new();
while let Some(path) = paths.next().await {
let path = path?;
if path.extension() != Some(OsStr::new("json")) {
continue;
}
let pattern = r" - \d+.zed.json$";
let re = Regex::new(pattern).unwrap();
let metadata = fs.metadata(&path).await?;
if let Some((file_name, metadata)) = path
.file_name()
.and_then(|name| name.to_str())
.zip(metadata)
{
let title = re.replace(file_name, "");
conversations.push(Self {
title: title.into_owned(),
path,
mtime: metadata.mtime.into(),
});
}
}
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
Ok(conversations)
}
}
pub fn init(cx: &mut AppContext) {
assistant_panel::init(cx);
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}

View File

@ -1,8 +1,11 @@
use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
codegen::{self, Codegen, CodegenKind, OpenAICompletionProvider},
stream_completion, MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage,
Role, SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL,
codegen::{self, Codegen, CodegenKind},
MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
SavedMessage,
};
use ai::completion::{
stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL,
};
use anyhow::{anyhow, Result};
use chrono::{DateTime, Local};

View File

@ -1,59 +1,14 @@
use crate::{
stream_completion,
streaming_diff::{Hunk, StreamingDiff},
OpenAIRequest,
};
use crate::streaming_diff::{Hunk, StreamingDiff};
use ai::completion::{CompletionProvider, OpenAIRequest};
use anyhow::Result;
use editor::{
multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
};
use futures::{
channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, SinkExt, Stream, StreamExt,
};
use gpui::{executor::Background, Entity, ModelContext, ModelHandle, Task};
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
use gpui::{Entity, ModelContext, ModelHandle, Task};
use language::{Rope, TransactionId};
use std::{cmp, future, ops::Range, sync::Arc};
pub trait CompletionProvider {
fn complete(
&self,
prompt: OpenAIRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
}
pub struct OpenAICompletionProvider {
api_key: String,
executor: Arc<Background>,
}
impl OpenAICompletionProvider {
pub fn new(api_key: String, executor: Arc<Background>) -> Self {
Self { api_key, executor }
}
}
impl CompletionProvider for OpenAICompletionProvider {
fn complete(
&self,
prompt: OpenAIRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt);
async move {
let response = request.await?;
let stream = response
.filter_map(|response| async move {
match response {
Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
Err(error) => Some(Err(error)),
}
})
.boxed();
Ok(stream)
}
.boxed()
}
}
pub enum Event {
Finished,
Undone,
@ -397,13 +352,17 @@ fn strip_markdown_codeblock(
#[cfg(test)]
mod tests {
use super::*;
use futures::stream;
use futures::{
future::BoxFuture,
stream::{self, BoxStream},
};
use gpui::{executor::Deterministic, TestAppContext};
use indoc::indoc;
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
use parking_lot::Mutex;
use rand::prelude::*;
use settings::SettingsStore;
use smol::future::FutureExt;
#[gpui::test(iterations = 10)]
async fn test_transform_autoindent(

View File

@ -394,9 +394,14 @@ impl ActiveCall {
cx.spawn(|this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx));
} else {
// TODO: Resport collaboration error
}
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&called_user_id);
this.report_call_event("invite", cx);
cx.notify();
});
result
@ -461,13 +466,7 @@ impl ActiveCall {
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
Self::report_call_event_for_room(
"decline incoming",
Some(call.room_id),
None,
&self.client,
cx,
);
report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
@ -597,31 +596,46 @@ impl ActiveCall {
&self.pending_invites
}
fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
let (room_id, channel_id) = match self.room() {
Some(room) => {
let room = room.read(cx);
(Some(room.id()), room.channel_id())
}
None => (None, None),
};
Self::report_call_event_for_room(operation, room_id, channel_id, &self.client, cx)
}
pub fn report_call_event_for_room(
operation: &'static str,
room_id: Option<u64>,
channel_id: Option<u64>,
client: &Arc<Client>,
cx: &AppContext,
) {
let telemetry = client.telemetry();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let event = ClickhouseEvent::Call {
operation,
room_id,
channel_id,
};
telemetry.report_clickhouse_event(event, telemetry_settings);
pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
if let Some(room) = self.room() {
let room = room.read(cx);
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx);
}
}
}
pub fn report_call_event_for_room(
operation: &'static str,
room_id: u64,
channel_id: Option<u64>,
client: &Arc<Client>,
cx: &AppContext,
) {
let telemetry = client.telemetry();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let event = ClickhouseEvent::Call {
operation,
room_id: Some(room_id),
channel_id,
};
telemetry.report_clickhouse_event(event, telemetry_settings);
}
pub fn report_call_event_for_channel(
operation: &'static str,
channel_id: u64,
client: &Arc<Client>,
cx: &AppContext,
) {
let room = ActiveCall::global(cx).read(cx).room();
let telemetry = client.telemetry();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let event = ClickhouseEvent::Call {
operation,
room_id: room.map(|r| r.read(cx).id()),
channel_id: Some(channel_id),
};
telemetry.report_clickhouse_event(event, telemetry_settings);
}

View File

@ -1,5 +1,5 @@
use anyhow::{anyhow, Result};
use call::ActiveCall;
use call::report_call_event_for_channel;
use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId};
use client::{
proto::{self, PeerId},
@ -52,14 +52,9 @@ impl ChannelView {
cx.spawn(|mut cx| async move {
let channel_view = channel_view.await?;
pane.update(&mut cx, |pane, cx| {
let room_id = ActiveCall::global(cx)
.read(cx)
.room()
.map(|room| room.read(cx).id());
ActiveCall::report_call_event_for_room(
report_call_event_for_channel(
"open channel notes",
room_id,
Some(channel_id),
channel_id,
&workspace.read(cx).app_state().client,
cx,
);

View File

@ -136,6 +136,7 @@ actions!(
StartMoveChannel,
StartLinkChannel,
MoveOrLinkToSelected,
InsertSpace,
]
);
@ -184,6 +185,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(CollabPanel::select_next);
cx.add_action(CollabPanel::select_prev);
cx.add_action(CollabPanel::confirm);
cx.add_action(CollabPanel::insert_space);
cx.add_action(CollabPanel::remove);
cx.add_action(CollabPanel::remove_selected_channel);
cx.add_action(CollabPanel::show_inline_context_menu);
@ -2518,6 +2520,14 @@ impl CollabPanel {
}
}
fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
if self.channel_editing_state.is_some() {
self.channel_name_editor.update(cx, |editor, cx| {
editor.insert(" ", cx);
});
}
}
fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
if let Some(editing_state) = &mut self.channel_editing_state {
match editing_state {
@ -3054,6 +3064,19 @@ impl View for CollabPanel {
.on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
.into_any_named("collab panel")
}
fn update_keymap_context(
&self,
keymap: &mut gpui::keymap_matcher::KeymapContext,
_: &AppContext,
) {
Self::reset_to_default_keymap_context(keymap);
if self.channel_editing_state.is_some() {
keymap.add_identifier("editing");
} else {
keymap.add_identifier("not_editing");
}
}
}
impl Panel for CollabPanel {

View File

@ -10,7 +10,7 @@ mod panel_settings;
mod project_shared_notification;
mod sharing_status_indicator;
use call::{ActiveCall, Room};
use call::{report_call_event_for_room, ActiveCall, Room};
use gpui::{
actions,
geometry::{
@ -55,18 +55,18 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
let client = call.client();
let toggle_screen_sharing = room.update(cx, |room, cx| {
if room.is_screen_sharing() {
ActiveCall::report_call_event_for_room(
report_call_event_for_room(
"disable screen share",
Some(room.id()),
room.id(),
room.channel_id(),
&client,
cx,
);
Task::ready(room.unshare_screen(cx))
} else {
ActiveCall::report_call_event_for_room(
report_call_event_for_room(
"enable screen share",
Some(room.id()),
room.id(),
room.channel_id(),
&client,
cx,
@ -83,23 +83,13 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
if let Some(room) = call.room().cloned() {
let client = call.client();
room.update(cx, |room, cx| {
if room.is_muted(cx) {
ActiveCall::report_call_event_for_room(
"enable microphone",
Some(room.id()),
room.channel_id(),
&client,
cx,
);
let operation = if room.is_muted(cx) {
"enable microphone"
} else {
ActiveCall::report_call_event_for_room(
"disable microphone",
Some(room.id()),
room.channel_id(),
&client,
cx,
);
}
"disable microphone"
};
report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
room.toggle_mute(cx)
})
.map(|task| task.detach_and_log_err(cx))

View File

@ -18,6 +18,15 @@ actions!(command_palette, [Toggle]);
pub type CommandPalette = Picker<CommandPaletteDelegate>;
pub type CommandPaletteInterceptor =
Box<dyn Fn(&str, &AppContext) -> Option<CommandInterceptResult>>;
pub struct CommandInterceptResult {
pub action: Box<dyn Action>,
pub string: String,
pub positions: Vec<usize>,
}
pub struct CommandPaletteDelegate {
actions: Vec<Command>,
matches: Vec<StringMatch>,
@ -117,7 +126,7 @@ impl PickerDelegate for CommandPaletteDelegate {
}
})
.collect::<Vec<_>>();
let actions = cx.read(move |cx| {
let mut actions = cx.read(move |cx| {
let hit_counts = cx.optional_global::<HitCounts>();
actions.sort_by_key(|action| {
(
@ -136,7 +145,7 @@ impl PickerDelegate for CommandPaletteDelegate {
char_bag: command.name.chars().collect(),
})
.collect::<Vec<_>>();
let matches = if query.is_empty() {
let mut matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
@ -158,6 +167,40 @@ impl PickerDelegate for CommandPaletteDelegate {
)
.await
};
let intercept_result = cx.read(|cx| {
if cx.has_global::<CommandPaletteInterceptor>() {
cx.global::<CommandPaletteInterceptor>()(&query, cx)
} else {
None
}
});
if let Some(CommandInterceptResult {
action,
string,
positions,
}) = intercept_result
{
if let Some(idx) = matches
.iter()
.position(|m| actions[m.candidate_id].action.id() == action.id())
{
matches.remove(idx);
}
actions.push(Command {
name: string.clone(),
action,
keystrokes: vec![],
});
matches.insert(
0,
StringMatch {
candidate_id: actions.len() - 1,
string,
positions,
score: 0.0,
},
)
}
picker
.update(&mut cx, |picker, _| {
let delegate = picker.delegate_mut();

View File

@ -104,7 +104,7 @@ use sum_tree::TreeMap;
use text::Rope;
use theme::{DiagnosticStyle, Theme, ThemeSettings};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{ItemNavHistory, ViewId, Workspace};
use workspace::{ItemNavHistory, SplitDirection, ViewId, Workspace};
use crate::git::diff_hunk_to_display;
@ -364,6 +364,7 @@ pub fn init_settings(cx: &mut AppContext) {
pub fn init(cx: &mut AppContext) {
init_settings(cx);
cx.add_action(Editor::new_file);
cx.add_action(Editor::new_file_in_direction);
cx.add_action(Editor::cancel);
cx.add_action(Editor::newline);
cx.add_action(Editor::newline_above);
@ -1141,12 +1142,14 @@ struct CodeActionsMenu {
impl CodeActionsMenu {
fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
self.selected_item = 0;
self.list.scroll_to(ScrollTarget::Show(self.selected_item));
cx.notify()
}
fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
if self.selected_item > 0 {
self.selected_item -= 1;
self.list.scroll_to(ScrollTarget::Show(self.selected_item));
cx.notify()
}
}
@ -1154,12 +1157,14 @@ impl CodeActionsMenu {
fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
if self.selected_item + 1 < self.actions.len() {
self.selected_item += 1;
self.list.scroll_to(ScrollTarget::Show(self.selected_item));
cx.notify()
}
}
fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
self.selected_item = self.actions.len() - 1;
self.list.scroll_to(ScrollTarget::Show(self.selected_item));
cx.notify()
}
@ -1212,7 +1217,9 @@ impl CodeActionsMenu {
workspace.update(cx, |workspace, cx| {
if let Some(task) = Editor::confirm_code_action(
workspace,
&Default::default(),
&ConfirmCodeAction {
item_ix: Some(item_ix),
},
cx,
) {
task.detach_and_log_err(cx);
@ -1637,6 +1644,26 @@ impl Editor {
}
}
pub fn new_file_in_direction(
workspace: &mut Workspace,
action: &workspace::NewFileInDirection,
cx: &mut ViewContext<Workspace>,
) {
let project = workspace.project().clone();
if project.read(cx).is_remote() {
cx.propagate_action();
} else if let Some(buffer) = project
.update(cx, |project, cx| project.create_buffer("", None, cx))
.log_err()
{
workspace.split_item(
action.0,
Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
cx,
);
}
}
pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
self.buffer.read(cx).replica_id()
}
@ -4631,7 +4658,13 @@ impl Editor {
}
pub fn convert_to_title_case(&mut self, _: &ConvertToTitleCase, cx: &mut ViewContext<Self>) {
self.manipulate_text(cx, |text| text.to_case(Case::Title))
self.manipulate_text(cx, |text| {
// Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary
// https://github.com/rutrum/convert-case/issues/16
text.split("\n")
.map(|line| line.to_case(Case::Title))
.join("\n")
})
}
pub fn convert_to_snake_case(&mut self, _: &ConvertToSnakeCase, cx: &mut ViewContext<Self>) {
@ -4647,7 +4680,13 @@ impl Editor {
_: &ConvertToUpperCamelCase,
cx: &mut ViewContext<Self>,
) {
self.manipulate_text(cx, |text| text.to_case(Case::UpperCamel))
self.manipulate_text(cx, |text| {
// Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary
// https://github.com/rutrum/convert-case/issues/16
text.split("\n")
.map(|line| line.to_case(Case::UpperCamel))
.join("\n")
})
}
pub fn convert_to_lower_camel_case(
@ -7135,7 +7174,7 @@ impl Editor {
);
});
if split {
workspace.split_item(Box::new(editor), cx);
workspace.split_item(SplitDirection::Right, Box::new(editor), cx);
} else {
workspace.add_item(Box::new(editor), cx);
}
@ -8566,6 +8605,29 @@ impl Editor {
self.handle_input(text, cx);
}
pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool {
let Some(project) = self.project.as_ref() else {
return false;
};
let project = project.read(cx);
let mut supports = false;
self.buffer().read(cx).for_each_buffer(|buffer| {
if !supports {
supports = project
.language_servers_for_buffer(buffer.read(cx), cx)
.any(
|(_, server)| match server.capabilities().inlay_hint_provider {
Some(lsp::OneOf::Left(enabled)) => enabled,
Some(lsp::OneOf::Right(_)) => true,
None => false,
},
)
}
});
supports
}
}
pub trait CollaborationHub {

View File

@ -1429,7 +1429,7 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.));
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.));
editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
});
@ -2792,6 +2792,34 @@ async fn test_manipulate_text(cx: &mut TestAppContext) {
«hello worldˇ»
"});
// Test multiple line, single selection case
// Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
cx.set_state(indoc! {"
«The quick brown
fox jumps over
the lazy dogˇ»
"});
cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx));
cx.assert_editor_state(indoc! {"
«The Quick Brown
Fox Jumps Over
The Lazy Dogˇ»
"});
// Test multiple line, single selection case
// Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
cx.set_state(indoc! {"
«The quick brown
fox jumps over
the lazy dogˇ»
"});
cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx));
cx.assert_editor_state(indoc! {"
«TheQuickBrown
FoxJumpsOver
TheLazyDogˇ»
"});
// From here on out, test more complex cases of manipulate_text()
// Test no selection case - should affect words cursors are in

View File

@ -941,7 +941,7 @@ async fn fetch_and_update_hints(
})
.await;
if let Some(new_update) = new_update {
log::info!(
log::debug!(
"Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
new_update.remove_from_visible.len(),
new_update.remove_from_cache.len(),

View File

@ -996,7 +996,9 @@ impl SearchableItem for Editor {
};
if let Some(replacement) = query.replacement_for(&text) {
self.edit([(identifier.clone(), Arc::from(&*replacement))], cx);
self.transact(cx, |this, cx| {
this.edit([(identifier.clone(), Arc::from(&*replacement))], cx);
});
}
}
fn match_index_for_direction(

View File

@ -15,9 +15,13 @@ impl ScrollAmount {
Self::Line(count) => *count,
Self::Page(count) => editor
.visible_line_count()
// subtract one to leave an anchor line
// round towards zero (so page-up and page-down are symmetric)
.map(|l| (l * count).trunc() - count.signum())
.map(|mut l| {
// for full pages subtract one to leave an anchor line
if count.abs() == 1.0 {
l -= 1.0
}
(l * count).trunc()
})
.unwrap_or(0.),
}
}

View File

@ -3,8 +3,8 @@ use crate::{
};
use futures::Future;
use gpui::{
keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, ModelContext,
ViewContext, ViewHandle,
executor::Foreground, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle,
ModelContext, ViewContext, ViewHandle,
};
use indoc::indoc;
use language::{Buffer, BufferSnapshot};
@ -114,6 +114,7 @@ impl<'a> EditorTestContext<'a> {
let keystroke = Keystroke::parse(keystroke_text).unwrap();
self.cx.dispatch_keystroke(self.window, keystroke, false);
keystroke_under_test_handle
}
@ -126,6 +127,16 @@ impl<'a> EditorTestContext<'a> {
for keystroke_text in keystroke_texts.into_iter() {
self.simulate_keystroke(keystroke_text);
}
// it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
// before returning.
// NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
// quickly races with async actions.
if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() {
executor.run_until_parked();
} else {
unreachable!();
}
keystrokes_under_test_handle
}

View File

@ -10,6 +10,7 @@ doctest = false
[dependencies]
editor = { path = "../editor" }
collections = { path = "../collections" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }

View File

@ -1,5 +1,6 @@
use collections::HashMap;
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
use fuzzy::PathMatch;
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
use gpui::{
actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle,
};
@ -32,38 +33,114 @@ pub struct FileFinderDelegate {
history_items: Vec<FoundPath>,
}
#[derive(Debug)]
enum Matches {
History(Vec<FoundPath>),
Search(Vec<PathMatch>),
#[derive(Debug, Default)]
struct Matches {
history: Vec<(FoundPath, Option<PathMatch>)>,
search: Vec<PathMatch>,
}
#[derive(Debug)]
enum Match<'a> {
History(&'a FoundPath),
History(&'a FoundPath, Option<&'a PathMatch>),
Search(&'a PathMatch),
}
impl Matches {
fn len(&self) -> usize {
match self {
Self::History(items) => items.len(),
Self::Search(items) => items.len(),
}
self.history.len() + self.search.len()
}
fn get(&self, index: usize) -> Option<Match<'_>> {
match self {
Self::History(items) => items.get(index).map(Match::History),
Self::Search(items) => items.get(index).map(Match::Search),
if index < self.history.len() {
self.history
.get(index)
.map(|(path, path_match)| Match::History(path, path_match.as_ref()))
} else {
self.search
.get(index - self.history.len())
.map(Match::Search)
}
}
fn push_new_matches(
&mut self,
history_items: &Vec<FoundPath>,
query: &PathLikeWithPosition<FileSearchQuery>,
mut new_search_matches: Vec<PathMatch>,
extend_old_matches: bool,
) {
let matching_history_paths = matching_history_item_paths(history_items, query);
new_search_matches
.retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
let history_items_to_show = history_items
.iter()
.filter_map(|history_item| {
Some((
history_item.clone(),
Some(
matching_history_paths
.get(&history_item.project.path)?
.clone(),
),
))
})
.collect::<Vec<_>>();
self.history = history_items_to_show;
if extend_old_matches {
self.search
.retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
util::extend_sorted(
&mut self.search,
new_search_matches.into_iter(),
100,
|a, b| b.cmp(a),
)
} else {
self.search = new_search_matches;
}
}
}
impl Default for Matches {
fn default() -> Self {
Self::History(Vec::new())
fn matching_history_item_paths(
history_items: &Vec<FoundPath>,
query: &PathLikeWithPosition<FileSearchQuery>,
) -> HashMap<Arc<Path>, PathMatch> {
let history_items_by_worktrees = history_items
.iter()
.map(|found_path| {
let path = &found_path.project.path;
let candidate = PathMatchCandidate {
path,
char_bag: CharBag::from_iter(path.to_string_lossy().to_lowercase().chars()),
};
(found_path.project.worktree_id, candidate)
})
.fold(
HashMap::default(),
|mut candidates, (worktree_id, new_candidate)| {
candidates
.entry(worktree_id)
.or_insert_with(Vec::new)
.push(new_candidate);
candidates
},
);
let mut matching_history_paths = HashMap::default();
for (worktree, candidates) in history_items_by_worktrees {
let max_results = candidates.len() + 1;
matching_history_paths.extend(
fuzzy::match_fixed_path_set(
candidates,
worktree.to_usize(),
query.path_like.path_query(),
false,
max_results,
)
.into_iter()
.map(|path_match| (Arc::clone(&path_match.path), path_match)),
);
}
matching_history_paths
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -81,66 +158,82 @@ impl FoundPath {
actions!(file_finder, [Toggle]);
pub fn init(cx: &mut AppContext) {
cx.add_action(toggle_file_finder);
cx.add_action(toggle_or_cycle_file_finder);
FileFinder::init(cx);
}
const MAX_RECENT_SELECTIONS: usize = 20;
fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
let project = workspace.project().read(cx);
fn toggle_or_cycle_file_finder(
workspace: &mut Workspace,
_: &Toggle,
cx: &mut ViewContext<Workspace>,
) {
match workspace.modal::<FileFinder>() {
Some(file_finder) => file_finder.update(cx, |file_finder, cx| {
let current_index = file_finder.delegate().selected_index();
file_finder.select_next(&menu::SelectNext, cx);
let new_index = file_finder.delegate().selected_index();
if current_index == new_index {
file_finder.select_first(&menu::SelectFirst, cx);
}
}),
None => {
workspace.toggle_modal(cx, |workspace, cx| {
let project = workspace.project().read(cx);
let currently_opened_path = workspace
.active_item(cx)
.and_then(|item| item.project_path(cx))
.map(|project_path| {
let abs_path = project
.worktree_for_id(project_path.worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
FoundPath::new(project_path, abs_path)
});
let currently_opened_path = workspace
.active_item(cx)
.and_then(|item| item.project_path(cx))
.map(|project_path| {
let abs_path = project
.worktree_for_id(project_path.worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
FoundPath::new(project_path, abs_path)
});
// if exists, bubble the currently opened path to the top
let history_items = currently_opened_path
.clone()
.into_iter()
.chain(
workspace
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
// if exists, bubble the currently opened path to the top
let history_items = currently_opened_path
.clone()
.into_iter()
.filter(|(history_path, _)| {
Some(history_path)
!= currently_opened_path
.as_ref()
.map(|found_path| &found_path.project)
})
.filter(|(_, history_abs_path)| {
history_abs_path.as_ref()
!= currently_opened_path
.as_ref()
.and_then(|found_path| found_path.absolute.as_ref())
})
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
)
.collect();
.chain(
workspace
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
.into_iter()
.filter(|(history_path, _)| {
Some(history_path)
!= currently_opened_path
.as_ref()
.map(|found_path| &found_path.project)
})
.filter(|(_, history_abs_path)| {
history_abs_path.as_ref()
!= currently_opened_path
.as_ref()
.and_then(|found_path| found_path.absolute.as_ref())
})
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
)
.collect();
let project = workspace.project().clone();
let workspace = cx.handle().downgrade();
let finder = cx.add_view(|cx| {
Picker::new(
FileFinderDelegate::new(
workspace,
project,
currently_opened_path,
history_items,
cx,
),
cx,
)
});
finder
});
let project = workspace.project().clone();
let workspace = cx.handle().downgrade();
let finder = cx.add_view(|cx| {
Picker::new(
FileFinderDelegate::new(
workspace,
project,
currently_opened_path,
history_items,
cx,
),
cx,
)
});
finder
});
}
}
}
pub enum Event {
@ -255,24 +348,14 @@ impl FileFinderDelegate {
) {
if search_id >= self.latest_search_id {
self.latest_search_id = search_id;
if self.latest_search_did_cancel
let extend_old_matches = self.latest_search_did_cancel
&& Some(query.path_like.path_query())
== self
.latest_search_query
.as_ref()
.map(|query| query.path_like.path_query())
{
match &mut self.matches {
Matches::History(_) => self.matches = Matches::Search(matches),
Matches::Search(search_matches) => {
util::extend_sorted(search_matches, matches.into_iter(), 100, |a, b| {
b.cmp(a)
})
}
}
} else {
self.matches = Matches::Search(matches);
}
.map(|query| query.path_like.path_query());
self.matches
.push_new_matches(&self.history_items, &query, matches, extend_old_matches);
self.latest_search_query = Some(query);
self.latest_search_did_cancel = did_cancel;
cx.notify();
@ -286,7 +369,7 @@ impl FileFinderDelegate {
ix: usize,
) -> (String, Vec<usize>, String, Vec<usize>) {
let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
Match::History(found_path) => {
Match::History(found_path, found_path_match) => {
let worktree_id = found_path.project.worktree_id;
let project_relative_path = &found_path.project.path;
let has_worktree = self
@ -318,14 +401,22 @@ impl FileFinderDelegate {
path = Arc::from(absolute_path.as_path());
}
}
self.labels_for_path_match(&PathMatch {
let mut path_match = PathMatch {
score: ix as f64,
positions: Vec::new(),
worktree_id: worktree_id.to_usize(),
path,
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
})
};
if let Some(found_path_match) = found_path_match {
path_match
.positions
.extend(found_path_match.positions.iter())
}
self.labels_for_path_match(&path_match)
}
Match::Search(path_match) => self.labels_for_path_match(path_match),
};
@ -406,8 +497,9 @@ impl PickerDelegate for FileFinderDelegate {
if raw_query.is_empty() {
let project = self.project.read(cx);
self.latest_search_id = post_inc(&mut self.search_count);
self.matches = Matches::History(
self.history_items
self.matches = Matches {
history: self
.history_items
.iter()
.filter(|history_item| {
project
@ -421,8 +513,10 @@ impl PickerDelegate for FileFinderDelegate {
.is_some())
})
.cloned()
.map(|p| (p, None))
.collect(),
);
search: Vec::new(),
};
cx.notify();
Task::ready(())
} else {
@ -454,7 +548,7 @@ impl PickerDelegate for FileFinderDelegate {
}
};
match m {
Match::History(history_match) => {
Match::History(history_match, _) => {
let worktree_id = history_match.project.worktree_id;
if workspace
.project()
@ -866,11 +960,11 @@ mod tests {
finder.update(cx, |finder, cx| {
let delegate = finder.delegate_mut();
let matches = match &delegate.matches {
Matches::Search(path_matches) => path_matches,
_ => panic!("Search matches expected"),
}
.clone();
assert!(
delegate.matches.history.is_empty(),
"Search matches expected"
);
let matches = delegate.matches.search.clone();
// Simulate a search being cancelled after the time limit,
// returning only a subset of the matches that would have been found.
@ -893,12 +987,11 @@ mod tests {
cx,
);
match &delegate.matches {
Matches::Search(new_matches) => {
assert_eq!(new_matches.as_slice(), &matches[0..4])
}
_ => panic!("Search matches expected"),
};
assert!(
delegate.matches.history.is_empty(),
"Search matches expected"
);
assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]);
});
}
@ -1006,10 +1099,11 @@ mod tests {
cx.read(|cx| {
let finder = finder.read(cx);
let delegate = finder.delegate();
let matches = match &delegate.matches {
Matches::Search(path_matches) => path_matches,
_ => panic!("Search matches expected"),
};
assert!(
delegate.matches.history.is_empty(),
"Search matches expected"
);
let matches = delegate.matches.search.clone();
assert_eq!(matches.len(), 1);
let (file_name, file_name_positions, full_path, full_path_positions) =
@ -1088,10 +1182,11 @@ mod tests {
finder.read_with(cx, |f, _| {
let delegate = f.delegate();
let matches = match &delegate.matches {
Matches::Search(path_matches) => path_matches,
_ => panic!("Search matches expected"),
};
assert!(
delegate.matches.history.is_empty(),
"Search matches expected"
);
let matches = delegate.matches.search.clone();
assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
});
@ -1459,6 +1554,255 @@ mod tests {
);
}
#[gpui::test]
async fn test_toggle_panel_new_selections(
deterministic: Arc<gpui::executor::Deterministic>,
cx: &mut gpui::TestAppContext,
) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"test": {
"first.rs": "// First Rust file",
"second.rs": "// Second Rust file",
"third.rs": "// Third Rust file",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
// generate some history to select from
open_close_queried_buffer(
"fir",
1,
"first.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"sec",
1,
"second.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"thi",
1,
"third.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
let current_history = open_close_queried_buffer(
"sec",
1,
"second.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
for expected_selected_index in 0..current_history.len() {
cx.dispatch_action(window.into(), Toggle);
let selected_index = cx.read(|cx| {
workspace
.read(cx)
.modal::<FileFinder>()
.unwrap()
.read(cx)
.delegate()
.selected_index()
});
assert_eq!(
selected_index, expected_selected_index,
"Should select the next item in the history"
);
}
cx.dispatch_action(window.into(), Toggle);
let selected_index = cx.read(|cx| {
workspace
.read(cx)
.modal::<FileFinder>()
.unwrap()
.read(cx)
.delegate()
.selected_index()
});
assert_eq!(
selected_index, 0,
"Should wrap around the history and start all over"
);
}
#[gpui::test]
async fn test_search_preserves_history_items(
deterministic: Arc<gpui::executor::Deterministic>,
cx: &mut gpui::TestAppContext,
) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"test": {
"first.rs": "// First Rust file",
"second.rs": "// Second Rust file",
"third.rs": "// Third Rust file",
"fourth.rs": "// Fourth Rust file",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
let worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1,);
WorktreeId::from_usize(worktrees[0].id())
});
// generate some history to select from
open_close_queried_buffer(
"fir",
1,
"first.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"sec",
1,
"second.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"thi",
1,
"third.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"sec",
1,
"second.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
cx.dispatch_action(window.into(), Toggle);
let first_query = "f";
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder
.update(cx, |finder, cx| {
finder
.delegate_mut()
.update_matches(first_query.to_string(), cx)
})
.await;
finder.read_with(cx, |finder, _| {
let delegate = finder.delegate();
assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
let history_match = delegate.matches.history.first().unwrap();
assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
assert_eq!(history_match.0, FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
},
Some(PathBuf::from("/src/test/first.rs"))
));
assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
});
let second_query = "fsdasdsa";
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder
.update(cx, |finder, cx| {
finder
.delegate_mut()
.update_matches(second_query.to_string(), cx)
})
.await;
finder.read_with(cx, |finder, _| {
let delegate = finder.delegate();
assert!(
delegate.matches.history.is_empty(),
"No history entries should match {second_query}"
);
assert!(
delegate.matches.search.is_empty(),
"No search entries should match {second_query}"
);
});
let first_query_again = first_query;
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder
.update(cx, |finder, cx| {
finder
.delegate_mut()
.update_matches(first_query_again.to_string(), cx)
})
.await;
finder.read_with(cx, |finder, _| {
let delegate = finder.delegate();
assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
let history_match = delegate.matches.history.first().unwrap();
assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
assert_eq!(history_match.0, FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
},
Some(PathBuf::from("/src/test/first.rs"))
));
assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
});
}
async fn open_close_queried_buffer(
input: &str,
expected_matches: usize,
@ -1528,13 +1872,8 @@ mod tests {
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
active_pane
.update(cx, |pane, cx| {
pane.close_active_item(
&workspace::CloseActiveItem {
save_behavior: None,
},
cx,
)
.unwrap()
pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx)
.unwrap()
})
.await
.unwrap();

View File

@ -507,7 +507,7 @@ impl FakeFs {
state.emit_event(&[path]);
}
fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
pub fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
let mut state = self.state.lock();
let path = path.as_ref();
let inode = state.next_inode;

View File

@ -4,5 +4,7 @@ mod paths;
mod strings;
pub use char_bag::CharBag;
pub use paths::{match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet};
pub use paths::{
match_fixed_path_set, match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet,
};
pub use strings::{match_strings, StringMatch, StringMatchCandidate};

View File

@ -90,6 +90,44 @@ impl Ord for PathMatch {
}
}
pub fn match_fixed_path_set(
candidates: Vec<PathMatchCandidate>,
worktree_id: usize,
query: &str,
smart_case: bool,
max_results: usize,
) -> Vec<PathMatch> {
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let query_char_bag = CharBag::from(&lowercase_query[..]);
let mut matcher = Matcher::new(
&query,
&lowercase_query,
query_char_bag,
smart_case,
max_results,
);
let mut results = Vec::new();
matcher.match_candidates(
&[],
&[],
candidates.into_iter(),
&mut results,
&AtomicBool::new(false),
|candidate, score| PathMatch {
score,
worktree_id,
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: usize::MAX,
},
);
results
}
pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
candidate_sets: &'a [Set],
query: &str,

View File

@ -33,6 +33,7 @@ use std::{
any::{type_name, Any, TypeId},
mem,
ops::{Deref, DerefMut, Range, Sub},
sync::Arc,
};
use taffy::{
tree::{Measurable, MeasureFunc},
@ -56,7 +57,7 @@ pub struct Window {
pub(crate) rendered_views: HashMap<usize, Box<dyn AnyRootElement>>,
scene: SceneBuilder,
pub(crate) text_style_stack: Vec<TextStyle>,
pub(crate) theme_stack: Vec<Box<dyn Any>>,
pub(crate) theme_stack: Vec<Arc<dyn Any + Send + Sync>>,
pub(crate) new_parents: HashMap<usize, usize>,
pub(crate) views_to_notify_if_ancestors_change: HashMap<usize, SmallVec<[usize; 2]>>,
titlebar_height: f32,
@ -1336,18 +1337,21 @@ impl<'a> WindowContext<'a> {
self.window.text_style_stack.pop();
}
pub fn theme<T: 'static>(&self) -> &T {
pub fn theme<T: 'static + Send + Sync>(&self) -> Arc<T> {
self.window
.theme_stack
.iter()
.rev()
.find_map(|theme| theme.downcast_ref())
.find_map(|theme| {
let entry = Arc::clone(theme);
entry.downcast::<T>().ok()
})
.ok_or_else(|| anyhow!("no theme provided of type {}", type_name::<T>()))
.unwrap()
}
pub fn push_theme<T: 'static>(&mut self, theme: T) {
self.window.theme_stack.push(Box::new(theme));
pub fn push_theme<T: 'static + Send + Sync>(&mut self, theme: T) {
self.window.theme_stack.push(Arc::new(theme));
}
pub fn pop_theme(&mut self) {

View File

@ -98,7 +98,12 @@ impl FontCache {
}
Err(anyhow!(
"could not find a non-empty font family matching one of the given names"
"could not find a non-empty font family matching one of the given names: {}",
names
.iter()
.map(|name| format!("`{name}`"))
.collect::<Vec<_>>()
.join(", ")
))
}

View File

@ -320,174 +320,114 @@ use crate as gpui2;
//
// Example:
// // Sets the padding to 0.5rem, just like class="p-2" in Tailwind.
// fn p_2(mut self) -> Self where Self: Sized;
pub trait StyleHelpers: Styleable<Style = Style> {
// fn p_2(mut self) -> Self;
pub trait StyleHelpers: Sized + Styleable<Style = Style> {
styleable_helpers!();
fn full(mut self) -> Self
where
Self: Sized,
{
fn full(mut self) -> Self {
self.declared_style().size.width = Some(relative(1.).into());
self.declared_style().size.height = Some(relative(1.).into());
self
}
fn relative(mut self) -> Self
where
Self: Sized,
{
fn relative(mut self) -> Self {
self.declared_style().position = Some(Position::Relative);
self
}
fn absolute(mut self) -> Self
where
Self: Sized,
{
fn absolute(mut self) -> Self {
self.declared_style().position = Some(Position::Absolute);
self
}
fn block(mut self) -> Self
where
Self: Sized,
{
fn block(mut self) -> Self {
self.declared_style().display = Some(Display::Block);
self
}
fn flex(mut self) -> Self
where
Self: Sized,
{
fn flex(mut self) -> Self {
self.declared_style().display = Some(Display::Flex);
self
}
fn flex_col(mut self) -> Self
where
Self: Sized,
{
fn flex_col(mut self) -> Self {
self.declared_style().flex_direction = Some(FlexDirection::Column);
self
}
fn flex_row(mut self) -> Self
where
Self: Sized,
{
fn flex_row(mut self) -> Self {
self.declared_style().flex_direction = Some(FlexDirection::Row);
self
}
fn flex_1(mut self) -> Self
where
Self: Sized,
{
fn flex_1(mut self) -> Self {
self.declared_style().flex_grow = Some(1.);
self.declared_style().flex_shrink = Some(1.);
self.declared_style().flex_basis = Some(relative(0.).into());
self
}
fn flex_auto(mut self) -> Self
where
Self: Sized,
{
fn flex_auto(mut self) -> Self {
self.declared_style().flex_grow = Some(1.);
self.declared_style().flex_shrink = Some(1.);
self.declared_style().flex_basis = Some(Length::Auto);
self
}
fn flex_initial(mut self) -> Self
where
Self: Sized,
{
fn flex_initial(mut self) -> Self {
self.declared_style().flex_grow = Some(0.);
self.declared_style().flex_shrink = Some(1.);
self.declared_style().flex_basis = Some(Length::Auto);
self
}
fn flex_none(mut self) -> Self
where
Self: Sized,
{
fn flex_none(mut self) -> Self {
self.declared_style().flex_grow = Some(0.);
self.declared_style().flex_shrink = Some(0.);
self
}
fn grow(mut self) -> Self
where
Self: Sized,
{
fn grow(mut self) -> Self {
self.declared_style().flex_grow = Some(1.);
self
}
fn items_start(mut self) -> Self
where
Self: Sized,
{
fn items_start(mut self) -> Self {
self.declared_style().align_items = Some(AlignItems::FlexStart);
self
}
fn items_end(mut self) -> Self
where
Self: Sized,
{
fn items_end(mut self) -> Self {
self.declared_style().align_items = Some(AlignItems::FlexEnd);
self
}
fn items_center(mut self) -> Self
where
Self: Sized,
{
fn items_center(mut self) -> Self {
self.declared_style().align_items = Some(AlignItems::Center);
self
}
fn justify_between(mut self) -> Self
where
Self: Sized,
{
fn justify_between(mut self) -> Self {
self.declared_style().justify_content = Some(JustifyContent::SpaceBetween);
self
}
fn justify_center(mut self) -> Self
where
Self: Sized,
{
fn justify_center(mut self) -> Self {
self.declared_style().justify_content = Some(JustifyContent::Center);
self
}
fn justify_start(mut self) -> Self
where
Self: Sized,
{
fn justify_start(mut self) -> Self {
self.declared_style().justify_content = Some(JustifyContent::Start);
self
}
fn justify_end(mut self) -> Self
where
Self: Sized,
{
fn justify_end(mut self) -> Self {
self.declared_style().justify_content = Some(JustifyContent::End);
self
}
fn justify_around(mut self) -> Self
where
Self: Sized,
{
fn justify_around(mut self) -> Self {
self.declared_style().justify_content = Some(JustifyContent::SpaceAround);
self
}
@ -495,7 +435,6 @@ pub trait StyleHelpers: Styleable<Style = Style> {
fn fill<F>(mut self, fill: F) -> Self
where
F: Into<Fill>,
Self: Sized,
{
self.declared_style().fill = Some(fill.into());
self
@ -504,7 +443,6 @@ pub trait StyleHelpers: Styleable<Style = Style> {
fn border_color<C>(mut self, border_color: C) -> Self
where
C: Into<Hsla>,
Self: Sized,
{
self.declared_style().border_color = Some(border_color.into());
self
@ -513,72 +451,47 @@ pub trait StyleHelpers: Styleable<Style = Style> {
fn text_color<C>(mut self, color: C) -> Self
where
C: Into<Hsla>,
Self: Sized,
{
self.declared_style().text_color = Some(color.into());
self
}
fn text_xs(mut self) -> Self
where
Self: Sized,
{
fn text_xs(mut self) -> Self {
self.declared_style().font_size = Some(0.75);
self
}
fn text_sm(mut self) -> Self
where
Self: Sized,
{
fn text_sm(mut self) -> Self {
self.declared_style().font_size = Some(0.875);
self
}
fn text_base(mut self) -> Self
where
Self: Sized,
{
fn text_base(mut self) -> Self {
self.declared_style().font_size = Some(1.0);
self
}
fn text_lg(mut self) -> Self
where
Self: Sized,
{
fn text_lg(mut self) -> Self {
self.declared_style().font_size = Some(1.125);
self
}
fn text_xl(mut self) -> Self
where
Self: Sized,
{
fn text_xl(mut self) -> Self {
self.declared_style().font_size = Some(1.25);
self
}
fn text_2xl(mut self) -> Self
where
Self: Sized,
{
fn text_2xl(mut self) -> Self {
self.declared_style().font_size = Some(1.5);
self
}
fn text_3xl(mut self) -> Self
where
Self: Sized,
{
fn text_3xl(mut self) -> Self {
self.declared_style().font_size = Some(1.875);
self
}
fn font(mut self, family_name: impl Into<Arc<str>>) -> Self
where
Self: Sized,
{
fn font(mut self, family_name: impl Into<Arc<str>>) -> Self {
self.declared_style().font_family = Some(family_name.into());
self
}

View File

@ -135,10 +135,6 @@ fn generate_predefined_setter(
}
};
if negate {
dbg!(method.to_string());
}
method
}

View File

@ -8,8 +8,8 @@ use gpui::{
ParentElement, Stack,
},
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext,
ViewHandle, WeakModelHandle,
AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View,
ViewContext, ViewHandle, WeakModelHandle,
};
use language::{Buffer, LanguageServerId, LanguageServerName};
use lsp::IoKind;
@ -53,10 +53,12 @@ pub struct LspLogView {
current_server_id: Option<LanguageServerId>,
is_showing_rpc_trace: bool,
project: ModelHandle<Project>,
_log_store_subscription: Subscription,
}
pub struct LspLogToolbarItemView {
log_view: Option<ViewHandle<LspLogView>>,
_log_view_subscription: Option<Subscription>,
menu_open: bool,
}
@ -181,6 +183,13 @@ impl LogStore {
});
let server = project.read(cx).language_server_for_id(id);
if let Some(server) = server.as_deref() {
if server.has_notification_handler::<lsp::notification::LogMessage>() {
// Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
return Some(server_state.log_buffer.clone());
}
}
let weak_project = project.downgrade();
let io_tx = self.io_tx.clone();
server_state._io_logs_subscription = server.as_ref().map(|server| {
@ -366,12 +375,49 @@ impl LspLogView {
.get(&project.downgrade())
.and_then(|project| project.servers.keys().copied().next());
let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
let _log_store_subscription = cx.observe(&log_store, |this, store, cx| {
(|| -> Option<()> {
let project_state = store.read(cx).projects.get(&this.project.downgrade())?;
if let Some(current_lsp) = this.current_server_id {
if !project_state.servers.contains_key(&current_lsp) {
if let Some(server) = project_state.servers.iter().next() {
if this.is_showing_rpc_trace {
this.show_rpc_trace_for_server(*server.0, cx)
} else {
this.show_logs_for_server(*server.0, cx)
}
} else {
this.current_server_id = None;
this.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
editor.clear(cx);
editor.set_read_only(true);
});
cx.notify();
}
}
} else {
if let Some(server) = project_state.servers.iter().next() {
if this.is_showing_rpc_trace {
this.show_rpc_trace_for_server(*server.0, cx)
} else {
this.show_logs_for_server(*server.0, cx)
}
}
}
Some(())
})();
cx.notify();
});
let mut this = Self {
editor: Self::editor_for_buffer(project.clone(), buffer, cx),
project,
log_store,
current_server_id: None,
is_showing_rpc_trace: false,
_log_store_subscription,
};
if let Some(server_id) = server_id {
this.show_logs_for_server(server_id, cx);
@ -594,18 +640,22 @@ impl ToolbarItemView for LspLogToolbarItemView {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_: &mut ViewContext<Self>,
cx: &mut ViewContext<Self>,
) -> workspace::ToolbarItemLocation {
self.menu_open = false;
if let Some(item) = active_pane_item {
if let Some(log_view) = item.downcast::<LspLogView>() {
self.log_view = Some(log_view.clone());
self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
cx.notify();
}));
return ToolbarItemLocation::PrimaryLeft {
flex: Some((1., false)),
};
}
}
self.log_view = None;
self._log_view_subscription = None;
ToolbarItemLocation::Hidden
}
}
@ -736,6 +786,7 @@ impl LspLogToolbarItemView {
Self {
menu_open: false,
log_view: None,
_log_view_subscription: None,
}
}

View File

@ -605,6 +605,10 @@ impl LanguageServer {
self.notification_handlers.lock().remove(T::METHOD);
}
pub fn has_notification_handler<T: notification::Notification>(&self) -> bool {
self.notification_handlers.lock().contains_key(T::METHOD)
}
#[must_use]
pub fn on_custom_notification<Params, F>(&self, method: &'static str, mut f: F) -> Subscription
where
@ -712,11 +716,11 @@ impl LanguageServer {
}
}
pub fn name<'a>(self: &'a Arc<Self>) -> &'a str {
pub fn name(&self) -> &str {
&self.name
}
pub fn capabilities<'a>(self: &'a Arc<Self>) -> &'a ServerCapabilities {
pub fn capabilities(&self) -> &ServerCapabilities {
&self.capabilities
}

View File

@ -25,7 +25,8 @@ pub struct Picker<D: PickerDelegate> {
max_size: Vector2F,
theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>,
confirmed: bool,
pending_update_matches: Task<Option<()>>,
pending_update_matches: Option<Task<Option<()>>>,
confirm_on_update: Option<bool>,
has_focus: bool,
}
@ -208,7 +209,8 @@ impl<D: PickerDelegate> Picker<D> {
max_size: vec2f(540., 420.),
theme,
confirmed: false,
pending_update_matches: Task::ready(None),
pending_update_matches: None,
confirm_on_update: None,
has_focus: false,
};
this.update_matches(String::new(), cx);
@ -263,11 +265,13 @@ impl<D: PickerDelegate> Picker<D> {
pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
let update = self.delegate.update_matches(query, cx);
self.matches_updated(cx);
self.pending_update_matches = cx.spawn(|this, mut cx| async move {
self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
update.await;
this.update(&mut cx, |this, cx| this.matches_updated(cx))
.log_err()
});
this.update(&mut cx, |this, cx| {
this.matches_updated(cx);
})
.log_err()
}));
}
fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
@ -278,6 +282,11 @@ impl<D: PickerDelegate> Picker<D> {
ScrollTarget::Show(index)
};
self.list_state.scroll_to(target);
self.pending_update_matches = None;
if let Some(secondary) = self.confirm_on_update.take() {
self.confirmed = true;
self.delegate.confirm(secondary, cx)
}
cx.notify();
}
@ -331,13 +340,21 @@ impl<D: PickerDelegate> Picker<D> {
}
pub fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
self.confirmed = true;
self.delegate.confirm(false, cx);
if self.pending_update_matches.is_some() {
self.confirm_on_update = Some(false)
} else {
self.confirmed = true;
self.delegate.confirm(false, cx);
}
}
pub fn secondary_confirm(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext<Self>) {
self.confirmed = true;
self.delegate.confirm(true, cx);
if self.pending_update_matches.is_some() {
self.confirm_on_update = Some(true)
} else {
self.confirmed = true;
self.delegate.confirm(true, cx);
}
}
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {

View File

@ -2273,11 +2273,13 @@ impl Project {
};
for (_, _, server) in self.language_servers_for_worktree(worktree_id) {
let text = include_text(server.as_ref()).then(|| buffer.read(cx).text());
server
.notify::<lsp::notification::DidSaveTextDocument>(
lsp::DidSaveTextDocumentParams {
text_document: text_document.clone(),
text: None,
text,
},
)
.log_err();
@ -8034,24 +8036,27 @@ fn subscribe_for_copilot_events(
copilot::Event::CopilotLanguageServerStarted => {
match copilot.read(cx).language_server() {
Some((name, copilot_server)) => {
let new_server_id = copilot_server.server_id();
let weak_project = cx.weak_handle();
let copilot_log_subscription = copilot_server
.on_notification::<copilot::request::LogMessage, _>(
move |params, mut cx| {
if let Some(project) = weak_project.upgrade(&mut cx) {
project.update(&mut cx, |_, cx| {
cx.emit(Event::LanguageServerLog(
new_server_id,
params.message,
));
})
}
},
);
project.supplementary_language_servers.insert(new_server_id, (name.clone(), Arc::clone(copilot_server)));
project.copilot_log_subscription = Some(copilot_log_subscription);
cx.emit(Event::LanguageServerAdded(new_server_id));
// Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
if !copilot_server.has_notification_handler::<copilot::request::LogMessage>() {
let new_server_id = copilot_server.server_id();
let weak_project = cx.weak_handle();
let copilot_log_subscription = copilot_server
.on_notification::<copilot::request::LogMessage, _>(
move |params, mut cx| {
if let Some(project) = weak_project.upgrade(&mut cx) {
project.update(&mut cx, |_, cx| {
cx.emit(Event::LanguageServerLog(
new_server_id,
params.message,
));
})
}
},
);
project.supplementary_language_servers.insert(new_server_id, (name.clone(), Arc::clone(copilot_server)));
project.copilot_log_subscription = Some(copilot_log_subscription);
cx.emit(Event::LanguageServerAdded(new_server_id));
}
}
None => debug_panic!("Received Copilot language server started event, but no language server is running"),
}
@ -8308,3 +8313,19 @@ async fn wait_for_loading_buffer(
receiver.next().await;
}
}
fn include_text(server: &lsp::LanguageServer) -> bool {
server
.capabilities()
.text_document_sync
.as_ref()
.and_then(|sync| match sync {
lsp::TextDocumentSyncCapability::Kind(_) => None,
lsp::TextDocumentSyncCapability::Options(options) => options.save.as_ref(),
})
.and_then(|save_options| match save_options {
lsp::TextDocumentSyncSaveOptions::Supported(_) => None,
lsp::TextDocumentSyncSaveOptions::SaveOptions(options) => options.include_text,
})
.unwrap_or(false)
}

View File

@ -69,7 +69,7 @@ impl ProjectSymbolsDelegate {
&self.external_match_candidates,
query,
false,
MAX_MATCHES - visible_matches.len(),
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
&Default::default(),
cx.background().clone(),
));

View File

@ -9,7 +9,7 @@ path = "src/quick_action_bar.rs"
doctest = false
[dependencies]
ai = { path = "../ai" }
assistant = { path = "../assistant" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
search = { path = "../search" }

View File

@ -1,4 +1,4 @@
use ai::{assistant::InlineAssist, AssistantPanel};
use assistant::{assistant_panel::InlineAssist, AssistantPanel};
use editor::Editor;
use gpui::{
elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
@ -48,24 +48,26 @@ impl View for QuickActionBar {
return Empty::new().into_any();
};
let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
let mut bar = Flex::row().with_child(render_quick_action_bar_button(
0,
"icons/inlay_hint.svg",
inlay_hints_enabled,
(
"Toggle Inlay Hints".to_string(),
Some(Box::new(editor::ToggleInlayHints)),
),
cx,
|this, cx| {
if let Some(editor) = this.active_editor() {
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
});
}
},
));
let mut bar = Flex::row();
if editor.read(cx).supports_inlay_hints(cx) {
bar = bar.with_child(render_quick_action_bar_button(
0,
"icons/inlay_hint.svg",
editor.read(cx).inlay_hints_enabled(),
(
"Toggle Inlay Hints".to_string(),
Some(Box::new(editor::ToggleInlayHints)),
),
cx,
|this, cx| {
if let Some(editor) = this.active_editor() {
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
});
}
},
));
}
if editor.read(cx).buffer().read(cx).is_singleton() {
let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed();
@ -163,12 +165,18 @@ impl ToolbarItemView for QuickActionBar {
if let Some(editor) = active_item.downcast::<Editor>() {
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
self._inlay_hints_enabled_subscription =
Some(cx.observe(&editor, move |_, editor, cx| {
let new_inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
if inlay_hints_enabled != new_inlay_hints_enabled {
inlay_hints_enabled = new_inlay_hints_enabled;
cx.notify();
let editor = editor.read(cx);
let new_inlay_hints_enabled = editor.inlay_hints_enabled();
let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
|| supports_inlay_hints != new_supports_inlay_hints;
inlay_hints_enabled = new_inlay_hints_enabled;
supports_inlay_hints = new_supports_inlay_hints;
if should_notify {
cx.notify()
}
}));
ToolbarItemLocation::PrimaryRight { flex: None }

View File

@ -347,9 +347,9 @@ impl View for BufferSearchBar {
Flex::row()
.with_child(query_column)
.with_child(mode_column)
.with_children(switches_column)
.with_children(replacement)
.with_child(mode_column)
.with_child(nav_column)
.contained()
.with_style(theme.search.container)
@ -539,6 +539,23 @@ impl BufferSearchBar {
.map(|searchable_item| searchable_item.query_suggestion(cx))
}
pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
if replacement.is_none() {
self.replace_enabled = false;
return;
}
self.replace_enabled = true;
self.replacement_editor
.update(cx, |replacement_editor, cx| {
replacement_editor
.buffer()
.update(cx, |replacement_buffer, cx| {
let len = replacement_buffer.len(cx);
replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
});
});
}
pub fn search(
&mut self,
query: &str,
@ -679,6 +696,22 @@ impl BufferSearchBar {
}
}
pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
if let Some(matches) = self
.searchable_items_with_matches
.get(&searchable_item.downgrade())
{
if matches.len() == 0 {
return;
}
let new_match_index = matches.len() - 1;
searchable_item.update_matches(matches, cx);
searchable_item.activate_match(new_match_index, matches, cx);
}
}
}
fn select_next_match_on_pane(
pane: &mut Pane,
action: &SelectNextMatch,
@ -946,7 +979,7 @@ impl BufferSearchBar {
cx.propagate_action();
}
}
fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
if !self.dismissed && self.active_search.is_some() {
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
if let Some(query) = self.active_search.as_ref() {

View File

@ -60,7 +60,7 @@ pub fn init(cx: &mut AppContext) {
cx.set_global(ActiveSettings::default());
cx.add_action(ProjectSearchView::deploy);
cx.add_action(ProjectSearchView::move_focus_to_results);
cx.add_action(ProjectSearchBar::search);
cx.add_action(ProjectSearchBar::confirm);
cx.add_action(ProjectSearchBar::search_in_new);
cx.add_action(ProjectSearchBar::select_next_match);
cx.add_action(ProjectSearchBar::select_prev_match);
@ -330,7 +330,7 @@ impl View for ProjectSearchView {
// If Text -> Major: "Text search all files and folders", Minor: {...}
let current_mode = self.current_mode;
let major_text = if model.pending_search.is_some() {
let mut major_text = if model.pending_search.is_some() {
Cow::Borrowed("Searching...")
} else if model.no_results.is_some_and(|v| v) {
Cow::Borrowed("No Results")
@ -344,9 +344,18 @@ impl View for ProjectSearchView {
}
};
let mut show_minor_text = true;
let semantic_status = self.semantic_state.as_ref().and_then(|semantic| {
let status = semantic.index_status;
match status {
SemanticIndexStatus::NotAuthenticated => {
major_text = Cow::Borrowed("Not Authenticated");
show_minor_text = false;
Some(
"API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables"
.to_string(),
)
}
SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()),
SemanticIndexStatus::Indexing {
remaining_files,
@ -388,10 +397,13 @@ impl View for ProjectSearchView {
let mut minor_text = Vec::new();
minor_text.push("".into());
minor_text.extend(semantic_status);
minor_text.push("Simply explain the code you are looking to find.".into());
minor_text.push(
"ex. 'prompt user for permissions to index their project'".into(),
);
if show_minor_text {
minor_text
.push("Simply explain the code you are looking to find.".into());
minor_text.push(
"ex. 'prompt user for permissions to index their project'".into(),
);
}
minor_text
}
_ => vec![
@ -1359,9 +1371,18 @@ impl ProjectSearchBar {
})
}
}
fn search(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
let mut should_propagate = true;
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| search_view.search(cx));
search_view.update(cx, |search_view, cx| {
if !search_view.replacement_editor.is_focused(cx) {
should_propagate = false;
search_view.search(cx);
}
});
}
if should_propagate {
cx.propagate_action();
}
}
@ -1666,6 +1687,28 @@ impl View for ProjectSearchBar {
"ProjectSearchBar"
}
fn update_keymap_context(
&self,
keymap: &mut gpui::keymap_matcher::KeymapContext,
cx: &AppContext,
) {
Self::reset_to_default_keymap_context(keymap);
let in_replace = self
.active_project_search
.as_ref()
.map(|search| {
search
.read(cx)
.replacement_editor
.read_with(cx, |_, cx| cx.is_self_focused())
})
.flatten()
.unwrap_or(false);
if in_replace {
keymap.add_identifier("in_replace");
}
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(_search) = self.active_project_search.as_ref() {
let search = _search.read(cx);
@ -1915,9 +1958,9 @@ impl View for ProjectSearchBar {
Flex::row()
.with_child(query_column)
.with_child(mode_column)
.with_child(switches_column)
.with_children(replacement)
.with_child(mode_column)
.with_child(nav_column)
.contained()
.with_style(theme.search.container)

View File

@ -9,6 +9,7 @@ path = "src/semantic_index.rs"
doctest = false
[dependencies]
ai = { path = "../ai" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
language = { path = "../language" }
@ -26,22 +27,19 @@ futures.workspace = true
ordered-float.workspace = true
smol.workspace = true
rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] }
isahc.workspace = true
log.workspace = true
tree-sitter.workspace = true
lazy_static.workspace = true
serde.workspace = true
serde_json.workspace = true
async-trait.workspace = true
bincode = "1.3.3"
matrixmultiply = "0.3.7"
tiktoken-rs = "0.5.0"
parking_lot.workspace = true
rand.workspace = true
schemars.workspace = true
globset.workspace = true
sha1 = "0.10.5"
parse_duration = "2.1.1"
ndarray = { version = "0.15.0" }
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }

View File

@ -1,10 +1,10 @@
use ai::embedding::OpenAIEmbeddings;
use anyhow::{anyhow, Result};
use client::{self, UserStore};
use gpui::{AsyncAppContext, ModelHandle, Task};
use language::LanguageRegistry;
use node_runtime::RealNodeRuntime;
use project::{Project, RealFs};
use semantic_index::embedding::OpenAIEmbeddings;
use semantic_index::semantic_index_settings::SemanticIndexSettings;
use semantic_index::{SearchResult, SemanticIndex};
use serde::{Deserialize, Serialize};
@ -456,7 +456,7 @@ fn main() {
let languages = Arc::new(languages);
let node_runtime = RealNodeRuntime::new(http.clone());
languages::init(languages.clone(), node_runtime.clone());
languages::init(languages.clone(), node_runtime.clone(), cx);
language::init(cx);
project::Project::init(&client, cx);

View File

@ -1,19 +1,19 @@
use crate::{
embedding::Embedding,
parsing::{Span, SpanDigest},
SEMANTIC_INDEX_VERSION,
};
use ai::embedding::Embedding;
use anyhow::{anyhow, Context, Result};
use collections::HashMap;
use futures::channel::oneshot;
use gpui::executor;
use ndarray::{Array1, Array2};
use ordered_float::OrderedFloat;
use project::{search::PathMatcher, Fs};
use rpc::proto::Timestamp;
use rusqlite::params;
use rusqlite::types::Value;
use std::{
cmp::Reverse,
future::Future,
ops::Range,
path::{Path, PathBuf},
@ -23,6 +23,13 @@ use std::{
};
use util::TryFutureExt;
pub fn argsort<T: Ord>(data: &[T]) -> Vec<usize> {
let mut indices = (0..data.len()).collect::<Vec<_>>();
indices.sort_by_key(|&i| &data[i]);
indices.reverse();
indices
}
#[derive(Debug)]
pub struct FileRecord {
pub id: usize,
@ -409,23 +416,91 @@ impl VectorDatabase {
limit: usize,
file_ids: &[i64],
) -> impl Future<Output = Result<Vec<(i64, OrderedFloat<f32>)>>> {
let query_embedding = query_embedding.clone();
let file_ids = file_ids.to_vec();
let query = query_embedding.clone().0;
let query = Array1::from_vec(query);
self.transact(move |db| {
let mut results = Vec::<(i64, OrderedFloat<f32>)>::with_capacity(limit + 1);
Self::for_each_span(db, &file_ids, |id, embedding| {
let similarity = embedding.similarity(&query_embedding);
let ix = match results
.binary_search_by_key(&Reverse(similarity), |(_, s)| Reverse(*s))
{
Ok(ix) => ix,
Err(ix) => ix,
};
results.insert(ix, (id, similarity));
results.truncate(limit);
})?;
let mut query_statement = db.prepare(
"
SELECT
id, embedding
FROM
spans
WHERE
file_id IN rarray(?)
",
)?;
anyhow::Ok(results)
let deserialized_rows = query_statement
.query_map(params![ids_to_sql(&file_ids)], |row| {
Ok((row.get::<_, usize>(0)?, row.get::<_, Embedding>(1)?))
})?
.filter_map(|row| row.ok())
.collect::<Vec<(usize, Embedding)>>();
if deserialized_rows.len() == 0 {
return Ok(Vec::new());
}
// Get Length of Embeddings Returned
let embedding_len = deserialized_rows[0].1 .0.len();
let batch_n = 1000;
let mut batches = Vec::new();
let mut batch_ids = Vec::new();
let mut batch_embeddings: Vec<f32> = Vec::new();
deserialized_rows.iter().for_each(|(id, embedding)| {
batch_ids.push(id);
batch_embeddings.extend(&embedding.0);
if batch_ids.len() == batch_n {
let embeddings = std::mem::take(&mut batch_embeddings);
let ids = std::mem::take(&mut batch_ids);
let array =
Array2::from_shape_vec((ids.len(), embedding_len.clone()), embeddings);
match array {
Ok(array) => {
batches.push((ids, array));
}
Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err),
}
}
});
if batch_ids.len() > 0 {
let array = Array2::from_shape_vec(
(batch_ids.len(), embedding_len),
batch_embeddings.clone(),
);
match array {
Ok(array) => {
batches.push((batch_ids.clone(), array));
}
Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err),
}
}
let mut ids: Vec<usize> = Vec::new();
let mut results = Vec::new();
for (batch_ids, array) in batches {
let scores = array
.dot(&query.t())
.to_vec()
.iter()
.map(|score| OrderedFloat(*score))
.collect::<Vec<OrderedFloat<f32>>>();
results.extend(scores);
ids.extend(batch_ids);
}
let sorted_idx = argsort(&results);
let mut sorted_results = Vec::new();
let last_idx = limit.min(sorted_idx.len());
for idx in &sorted_idx[0..last_idx] {
sorted_results.push((ids[*idx] as i64, results[*idx]))
}
Ok(sorted_results)
})
}
@ -468,31 +543,6 @@ impl VectorDatabase {
})
}
fn for_each_span(
db: &rusqlite::Connection,
file_ids: &[i64],
mut f: impl FnMut(i64, Embedding),
) -> Result<()> {
let mut query_statement = db.prepare(
"
SELECT
id, embedding
FROM
spans
WHERE
file_id IN rarray(?)
",
)?;
query_statement
.query_map(params![ids_to_sql(&file_ids)], |row| {
Ok((row.get(0)?, row.get::<_, Embedding>(1)?))
})?
.filter_map(|row| row.ok())
.for_each(|(id, embedding)| f(id, embedding));
Ok(())
}
pub fn spans_for_ids(
&self,
ids: &[i64],

View File

@ -1,4 +1,5 @@
use crate::{embedding::EmbeddingProvider, parsing::Span, JobHandle};
use crate::{parsing::Span, JobHandle};
use ai::embedding::EmbeddingProvider;
use gpui::executor::Background;
use parking_lot::Mutex;
use smol::channel;

View File

@ -1,4 +1,4 @@
use crate::embedding::{Embedding, EmbeddingProvider};
use ai::embedding::{Embedding, EmbeddingProvider};
use anyhow::{anyhow, Result};
use language::{Grammar, Language};
use rusqlite::{

View File

@ -1,5 +1,4 @@
mod db;
pub mod embedding;
mod embedding_queue;
mod parsing;
pub mod semantic_index_settings;
@ -8,14 +7,15 @@ pub mod semantic_index_settings;
mod semantic_index_tests;
use crate::semantic_index_settings::SemanticIndexSettings;
use ai::embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings};
use anyhow::{anyhow, Result};
use collections::{BTreeMap, HashMap, HashSet};
use db::VectorDatabase;
use embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings};
use embedding_queue::{EmbeddingQueue, FileToEmbed};
use futures::{future, FutureExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
use language::{Anchor, Bias, Buffer, Language, LanguageRegistry};
use lazy_static::lazy_static;
use ordered_float::OrderedFloat;
use parking_lot::Mutex;
use parsing::{CodeContextRetriever, Span, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES};
@ -24,6 +24,7 @@ use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, Work
use smol::channel;
use std::{
cmp::Reverse,
env,
future::Future,
mem,
ops::Range,
@ -38,6 +39,10 @@ const SEMANTIC_INDEX_VERSION: usize = 11;
const BACKGROUND_INDEXING_DELAY: Duration = Duration::from_secs(5 * 60);
const EMBEDDING_QUEUE_FLUSH_TIMEOUT: Duration = Duration::from_millis(250);
lazy_static! {
static ref OPENAI_API_KEY: Option<String> = env::var("OPENAI_API_KEY").ok();
}
pub fn init(
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
@ -100,6 +105,7 @@ pub fn init(
#[derive(Copy, Clone, Debug)]
pub enum SemanticIndexStatus {
NotAuthenticated,
NotIndexed,
Indexed,
Indexing {
@ -275,6 +281,10 @@ impl SemanticIndex {
}
pub fn status(&self, project: &ModelHandle<Project>) -> SemanticIndexStatus {
if !self.embedding_provider.is_authenticated() {
return SemanticIndexStatus::NotAuthenticated;
}
if let Some(project_state) = self.projects.get(&project.downgrade()) {
if project_state
.worktrees
@ -694,12 +704,14 @@ impl SemanticIndex {
let embedding_provider = self.embedding_provider.clone();
cx.spawn(|this, mut cx| async move {
index.await?;
let t0 = Instant::now();
let query = embedding_provider
.embed_batch(vec![query])
.await?
.pop()
.ok_or_else(|| anyhow!("could not embed query"))?;
index.await?;
log::trace!("Embedding Search Query: {:?}ms", t0.elapsed().as_millis());
let search_start = Instant::now();
let modified_buffer_results = this.update(&mut cx, |this, cx| {
@ -777,10 +789,15 @@ impl SemanticIndex {
let batch_n = cx.background().num_cpus();
let ids_len = file_ids.clone().len();
let batch_size = if ids_len <= batch_n {
ids_len
} else {
ids_len / batch_n
let minimum_batch_size = 50;
let batch_size = {
let size = ids_len / batch_n;
if size < minimum_batch_size {
minimum_batch_size
} else {
size
}
};
let mut batch_results = Vec::new();
@ -812,6 +829,7 @@ impl SemanticIndex {
Ok(ix) => ix,
Err(ix) => ix,
};
results.insert(ix, (id, similarity));
results.truncate(limit);
}
@ -846,7 +864,6 @@ impl SemanticIndex {
})?;
let buffers = futures::future::join_all(tasks).await;
Ok(buffers
.into_iter()
.zip(ranges)
@ -965,6 +982,10 @@ impl SemanticIndex {
project: ModelHandle<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if !self.embedding_provider.is_authenticated() {
return Task::ready(Err(anyhow!("user is not authenticated")));
}
if !self.projects.contains_key(&project.downgrade()) {
let subscription = cx.subscribe(&project, |this, project, event, cx| match event {
project::Event::WorktreeAdded | project::Event::WorktreeRemoved(_) => {

View File

@ -1,10 +1,10 @@
use crate::{
embedding::{DummyEmbeddings, Embedding, EmbeddingProvider},
embedding_queue::EmbeddingQueue,
parsing::{subtract_ranges, CodeContextRetriever, Span, SpanDigest},
semantic_index_settings::SemanticIndexSettings,
FileToEmbed, JobHandle, SearchResult, SemanticIndex, EMBEDDING_QUEUE_FLUSH_TIMEOUT,
};
use ai::embedding::{DummyEmbeddings, Embedding, EmbeddingProvider};
use anyhow::Result;
use async_trait::async_trait;
use gpui::{executor::Deterministic, Task, TestAppContext};
@ -1267,6 +1267,9 @@ impl FakeEmbeddingProvider {
#[async_trait]
impl EmbeddingProvider for FakeEmbeddingProvider {
fn is_authenticated(&self) -> bool {
true
}
fn truncate(&self, span: &str) -> (String, usize) {
(span.to_string(), 1)
}

View File

@ -9,14 +9,17 @@ name = "storybook"
path = "src/storybook.rs"
[dependencies]
gpui2 = { path = "../gpui2" }
anyhow.workspace = true
clap = { version = "4.4", features = ["derive", "string"] }
gpui2 = { path = "../gpui2" }
log.workspace = true
rust-embed.workspace = true
serde.workspace = true
settings = { path = "../settings" }
simplelog = "0.9"
strum = { version = "0.25.0", features = ["derive"] }
theme = { path = "../theme" }
ui = { path = "../ui" }
util = { path = "../util" }
[dev-dependencies]

View File

@ -1,10 +1,10 @@
use crate::theme::{theme, Theme};
use gpui2::{
elements::{div, div::ScrollState, img, svg},
style::{StyleHelpers, Styleable},
ArcCow, Element, IntoElement, ParentElement, ViewContext,
};
use std::marker::PhantomData;
use ui::{theme, Theme};
#[derive(Element)]
pub struct CollabPanelElement<V: 'static> {
@ -52,12 +52,12 @@ impl<V: 'static> CollabPanelElement<V> {
//:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
// .group()
// List Section Header
.child(self.list_section_header("#CRDB", true, theme))
.child(self.list_section_header("#CRDB", true, &theme))
// List Item Large
.child(self.list_item(
"http://github.com/maxbrunsfeld.png?s=50",
"maxbrunsfeld",
theme,
&theme,
)),
)
.child(
@ -65,31 +65,31 @@ impl<V: 'static> CollabPanelElement<V> {
.py_2()
.flex()
.flex_col()
.child(self.list_section_header("CHANNELS", true, theme)),
.child(self.list_section_header("CHANNELS", true, &theme)),
)
.child(
div()
.py_2()
.flex()
.flex_col()
.child(self.list_section_header("CONTACTS", true, theme))
.child(self.list_section_header("CONTACTS", true, &theme))
.children(
std::iter::repeat_with(|| {
vec![
self.list_item(
"http://github.com/as-cii.png?s=50",
"as-cii",
theme,
&theme,
),
self.list_item(
"http://github.com/nathansobo.png?s=50",
"nathansobo",
theme,
&theme,
),
self.list_item(
"http://github.com/maxbrunsfeld.png?s=50",
"maxbrunsfeld",
theme,
&theme,
),
]
})

View File

@ -0,0 +1,2 @@
pub mod components;
pub mod elements;

View File

@ -0,0 +1,4 @@
pub mod breadcrumb;
pub mod facepile;
pub mod toolbar;
pub mod traffic_lights;

View File

@ -0,0 +1,16 @@
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
use ui::breadcrumb;
use crate::story::Story;
#[derive(Element, Default)]
pub struct BreadcrumbStory {}
impl BreadcrumbStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container()
.child(Story::title_for::<_, ui::Breadcrumb>())
.child(Story::label("Default"))
.child(breadcrumb())
}
}

View File

@ -0,0 +1,50 @@
use gpui2::elements::div;
use gpui2::style::StyleHelpers;
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
use ui::prelude::*;
use ui::{avatar, facepile, theme};
use crate::story::Story;
#[derive(Element, Default)]
pub struct FacepileStory {}
impl FacepileStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let avatars = vec![
avatar("https://avatars.githubusercontent.com/u/1714999?v=4"),
avatar("https://avatars.githubusercontent.com/u/482957?v=4"),
avatar("https://avatars.githubusercontent.com/u/1789?v=4"),
];
Story::container()
.child(Story::title_for::<_, ui::Facepile>())
.child(Story::label("Default"))
.child(
div()
.flex()
.gap_3()
.child(facepile(avatars.clone().into_iter().take(1)))
.child(facepile(avatars.clone().into_iter().take(2)))
.child(facepile(avatars.clone().into_iter().take(3))),
)
.child(Story::label("Rounded rectangle avatars"))
.child({
let shape = Shape::RoundedRectangle;
let avatars = avatars
.clone()
.into_iter()
.map(|avatar| avatar.shape(Shape::RoundedRectangle));
div()
.flex()
.gap_3()
.child(facepile(avatars.clone().take(1)))
.child(facepile(avatars.clone().take(2)))
.child(facepile(avatars.clone().take(3)))
})
}
}

View File

@ -0,0 +1,16 @@
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
use ui::toolbar;
use crate::story::Story;
#[derive(Element, Default)]
pub struct ToolbarStory {}
impl ToolbarStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container()
.child(Story::title_for::<_, ui::Toolbar>())
.child(Story::label("Default"))
.child(toolbar())
}
}

View File

@ -0,0 +1,18 @@
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
use ui::{theme, traffic_lights};
use crate::story::Story;
#[derive(Element, Default)]
pub struct TrafficLightsStory {}
impl TrafficLightsStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
Story::container()
.child(Story::title_for::<_, ui::TrafficLights>())
.child(Story::label("Default"))
.child(traffic_lights())
}
}

View File

@ -0,0 +1 @@
pub mod avatar;

View File

@ -0,0 +1,26 @@
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
use ui::prelude::*;
use ui::{avatar, theme};
use crate::story::Story;
#[derive(Element, Default)]
pub struct AvatarStory {}
impl AvatarStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
Story::container()
.child(Story::title_for::<_, ui::Avatar>())
.child(Story::label("Default"))
.child(avatar(
"https://avatars.githubusercontent.com/u/1714999?v=4",
))
.child(Story::label("Rounded rectangle"))
.child(
avatar("https://avatars.githubusercontent.com/u/1714999?v=4")
.shape(Shape::RoundedRectangle),
)
}
}

View File

@ -0,0 +1,38 @@
use gpui2::elements::div;
use gpui2::style::StyleHelpers;
use gpui2::{rgb, Element, Hsla, ParentElement};
pub struct Story {}
impl Story {
pub fn container<V: 'static>() -> div::Div<V> {
div()
.size_full()
.flex()
.flex_col()
.pt_2()
.px_4()
.font("Zed Mono Extended")
.fill(rgb::<Hsla>(0x282c34))
}
pub fn title<V: 'static>(title: &str) -> impl Element<V> {
div()
.text_xl()
.text_color(rgb::<Hsla>(0xffffff))
.child(title.to_owned())
}
pub fn title_for<V: 'static, T>() -> impl Element<V> {
Self::title(std::any::type_name::<T>())
}
pub fn label<V: 'static>(label: &str) -> impl Element<V> {
div()
.mt_4()
.mb_2()
.text_xs()
.text_color(rgb::<Hsla>(0xffffff))
.child(label.to_owned())
}
}

View File

@ -0,0 +1,76 @@
use std::{str::FromStr, sync::OnceLock};
use anyhow::{anyhow, Context};
use clap::builder::PossibleValue;
use clap::ValueEnum;
use strum::{EnumIter, EnumString, IntoEnumIterator};
#[derive(Debug, Clone, Copy, strum::Display, EnumString, EnumIter)]
#[strum(serialize_all = "snake_case")]
pub enum ElementStory {
Avatar,
}
#[derive(Debug, Clone, Copy, strum::Display, EnumString, EnumIter)]
#[strum(serialize_all = "snake_case")]
pub enum ComponentStory {
Breadcrumb,
Facepile,
Toolbar,
TrafficLights,
}
#[derive(Debug, Clone, Copy)]
pub enum StorySelector {
Element(ElementStory),
Component(ComponentStory),
}
impl FromStr for StorySelector {
type Err = anyhow::Error;
fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
let story = raw_story_name.to_ascii_lowercase();
if let Some((_, story)) = story.split_once("elements/") {
let element_story = ElementStory::from_str(story)
.with_context(|| format!("story not found for element '{story}'"))?;
return Ok(Self::Element(element_story));
}
if let Some((_, story)) = story.split_once("components/") {
let component_story = ComponentStory::from_str(story)
.with_context(|| format!("story not found for component '{story}'"))?;
return Ok(Self::Component(component_story));
}
Err(anyhow!("story not found for '{raw_story_name}'"))
}
}
/// The list of all stories available in the storybook.
static ALL_STORIES: OnceLock<Vec<StorySelector>> = OnceLock::new();
impl ValueEnum for StorySelector {
fn value_variants<'a>() -> &'a [Self] {
let stories = ALL_STORIES.get_or_init(|| {
let element_stories = ElementStory::iter().map(Self::Element);
let component_stories = ComponentStory::iter().map(Self::Component);
element_stories.chain(component_stories).collect::<Vec<_>>()
});
stories
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
let value = match self {
Self::Element(story) => format!("elements/{story}"),
Self::Component(story) => format!("components/{story}"),
};
Some(PossibleValue::new(value))
}
}

View File

@ -1,31 +1,45 @@
#![allow(dead_code, unused_variables)]
use crate::theme::Theme;
mod collab_panel;
mod stories;
mod story;
mod story_selector;
mod workspace;
use ::theme as legacy_theme;
use element_ext::ElementExt;
use gpui2::{serde_json, vec2f, view, Element, RectF, ViewContext, WindowBounds};
use clap::Parser;
use gpui2::{serde_json, vec2f, view, Element, IntoElement, RectF, ViewContext, WindowBounds};
use legacy_theme::ThemeSettings;
use log::LevelFilter;
use settings::{default_settings, SettingsStore};
use simplelog::SimpleLogger;
use stories::components::breadcrumb::BreadcrumbStory;
use stories::components::facepile::FacepileStory;
use stories::components::toolbar::ToolbarStory;
use stories::components::traffic_lights::TrafficLightsStory;
use stories::elements::avatar::AvatarStory;
use ui::{ElementExt, Theme};
mod collab_panel;
mod components;
mod element_ext;
mod prelude;
mod theme;
mod ui;
mod workspace;
use crate::story_selector::{ComponentStory, ElementStory, StorySelector};
gpui2::actions! {
storybook,
[ToggleInspector]
}
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(value_enum)]
story: Option<StorySelector>,
}
fn main() {
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
gpui2::App::new(Assets).unwrap().run(|cx| {
let args = Args::parse();
gpui2::App::new(Assets).unwrap().run(move |cx| {
let mut store = SettingsStore::default();
store
.set_default_settings(default_settings().as_ref(), cx)
@ -40,19 +54,36 @@ fn main() {
center: true,
..Default::default()
},
|cx| {
view(|cx| {
// cx.enable_inspector();
storybook(&mut ViewContext::new(cx))
})
|cx| match args.story {
Some(StorySelector::Element(ElementStory::Avatar)) => {
view(|cx| render_story(&mut ViewContext::new(cx), AvatarStory::default()))
}
Some(StorySelector::Component(ComponentStory::Breadcrumb)) => {
view(|cx| render_story(&mut ViewContext::new(cx), BreadcrumbStory::default()))
}
Some(StorySelector::Component(ComponentStory::Facepile)) => {
view(|cx| render_story(&mut ViewContext::new(cx), FacepileStory::default()))
}
Some(StorySelector::Component(ComponentStory::Toolbar)) => {
view(|cx| render_story(&mut ViewContext::new(cx), ToolbarStory::default()))
}
Some(StorySelector::Component(ComponentStory::TrafficLights)) => view(|cx| {
render_story(&mut ViewContext::new(cx), TrafficLightsStory::default())
}),
None => {
view(|cx| render_story(&mut ViewContext::new(cx), WorkspaceElement::default()))
}
},
);
cx.platform().activate(true);
});
}
fn storybook<V: 'static>(cx: &mut ViewContext<V>) -> impl Element<V> {
workspace().themed(current_theme(cx))
fn render_story<V: 'static, S: IntoElement<V>>(
cx: &mut ViewContext<V>,
story: S,
) -> impl Element<V> {
story.into_element().themed(current_theme(cx))
}
// Nathan: During the transition to gpui2, we will include the base theme on the legacy Theme struct.
@ -75,7 +106,7 @@ fn current_theme<V: 'static>(cx: &mut ViewContext<V>) -> Theme {
use anyhow::{anyhow, Result};
use gpui2::AssetSource;
use rust_embed::RustEmbed;
use workspace::workspace;
use workspace::WorkspaceElement;
#[derive(RustEmbed)]
#[folder = "../../assets"]

View File

@ -1,23 +0,0 @@
mod element;
pub use element::avatar::*;
pub use element::details::*;
pub use element::icon::*;
pub use element::icon_button::*;
pub use element::indicator::*;
pub use element::input::*;
pub use element::label::*;
pub use element::text_button::*;
pub use element::tool_divider::*;
mod component;
pub use component::facepile::*;
pub use component::follow_group::*;
pub use component::list_item::*;
pub use component::tab::*;
mod module;
pub use module::chat_panel::*;
pub use module::project_panel::*;
pub use module::status_bar::*;
pub use module::tab_bar::*;
pub use module::title_bar::*;

View File

@ -1,4 +0,0 @@
pub(crate) mod facepile;
pub(crate) mod follow_group;
pub(crate) mod list_item;
pub(crate) mod tab;

View File

@ -1,9 +0,0 @@
pub(crate) mod avatar;
pub(crate) mod details;
pub(crate) mod icon;
pub(crate) mod icon_button;
pub(crate) mod indicator;
pub(crate) mod input;
pub(crate) mod label;
pub(crate) mod text_button;
pub(crate) mod tool_divider;

View File

@ -1,73 +0,0 @@
use crate::theme::theme;
use gpui2::elements::svg;
use gpui2::style::StyleHelpers;
use gpui2::IntoElement;
use gpui2::{Element, ViewContext};
// Icon::Hash
// icon(IconAsset::Hash).color(IconColor::Warning)
// Icon::new(IconAsset::Hash).color(IconColor::Warning)
#[derive(Default, PartialEq, Copy, Clone)]
pub enum IconAsset {
Ai,
ArrowLeft,
ArrowRight,
#[default]
ArrowUpRight,
Bolt,
Hash,
File,
Folder,
FolderOpen,
ChevronDown,
ChevronUp,
ChevronLeft,
ChevronRight,
}
impl IconAsset {
pub fn path(self) -> &'static str {
match self {
IconAsset::Ai => "icons/ai.svg",
IconAsset::ArrowLeft => "icons/arrow_left.svg",
IconAsset::ArrowRight => "icons/arrow_right.svg",
IconAsset::ArrowUpRight => "icons/arrow_up_right.svg",
IconAsset::Bolt => "icons/bolt.svg",
IconAsset::Hash => "icons/hash.svg",
IconAsset::ChevronDown => "icons/chevron_down.svg",
IconAsset::ChevronUp => "icons/chevron_up.svg",
IconAsset::ChevronLeft => "icons/chevron_left.svg",
IconAsset::ChevronRight => "icons/chevron_right.svg",
IconAsset::File => "icons/file_icons/file.svg",
IconAsset::Folder => "icons/file_icons/folder.svg",
IconAsset::FolderOpen => "icons/file_icons/folder_open.svg",
}
}
}
#[derive(Element, Clone)]
pub struct Icon {
asset: IconAsset,
}
pub fn icon(asset: IconAsset) -> Icon {
Icon { asset }
}
// impl Icon {
// pub fn new(asset: IconAsset) -> Icon {
// Icon { asset }
// }
// }
impl Icon {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
svg()
.path(self.asset.path())
.size_4()
.fill(theme.lowest.base.default.foreground)
}
}

View File

@ -1,5 +0,0 @@
pub(crate) mod chat_panel;
pub(crate) mod project_panel;
pub(crate) mod status_bar;
pub(crate) mod tab_bar;
pub(crate) mod title_bar;

View File

@ -1,97 +0,0 @@
use crate::{
prelude::{InteractionState, ToggleState},
theme::theme,
ui::{details, input, label, list_item, IconAsset, LabelColor},
};
use gpui2::{
elements::{div, div::ScrollState},
style::StyleHelpers,
ParentElement, ViewContext,
};
use gpui2::{Element, IntoElement};
use std::marker::PhantomData;
#[derive(Element)]
pub struct ProjectPanel<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
}
pub fn project_panel<V: 'static>(scroll_state: ScrollState) -> ProjectPanel<V> {
ProjectPanel {
view_type: PhantomData,
scroll_state,
}
}
impl<V: 'static> ProjectPanel<V> {
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.w_56()
.h_full()
.flex()
.flex_col()
.fill(theme.middle.base.default.background)
.child(
div()
.w_56()
.flex()
.flex_col()
.overflow_y_scroll(self.scroll_state.clone())
.child(details("This is a long string that should wrap when it keeps going for a long time.").meta_text("6 h ago)"))
.child(
div().flex().flex_col().children(
std::iter::repeat_with(|| {
vec![
list_item(label("sqlez").color(LabelColor::Modified))
.left_icon(IconAsset::FolderOpen.into())
.indent_level(0)
.set_toggle(ToggleState::NotToggled),
list_item(label("storybook").color(LabelColor::Modified))
.left_icon(IconAsset::FolderOpen.into())
.indent_level(0)
.set_toggle(ToggleState::Toggled),
list_item(label("docs").color(LabelColor::Default))
.left_icon(IconAsset::Folder.into())
.indent_level(1)
.set_toggle(ToggleState::Toggled),
list_item(label("src").color(LabelColor::Modified))
.left_icon(IconAsset::FolderOpen.into())
.indent_level(2)
.set_toggle(ToggleState::Toggled),
list_item(label("ui").color(LabelColor::Modified))
.left_icon(IconAsset::FolderOpen.into())
.indent_level(3)
.set_toggle(ToggleState::Toggled),
list_item(label("component").color(LabelColor::Created))
.left_icon(IconAsset::FolderOpen.into())
.indent_level(4)
.set_toggle(ToggleState::Toggled),
list_item(label("facepile.rs").color(LabelColor::Default))
.left_icon(IconAsset::File.into())
.indent_level(5),
list_item(label("follow_group.rs").color(LabelColor::Default))
.left_icon(IconAsset::File.into())
.indent_level(5),
list_item(label("list_item.rs").color(LabelColor::Created))
.left_icon(IconAsset::File.into())
.indent_level(5),
list_item(label("tab.rs").color(LabelColor::Default))
.left_icon(IconAsset::File.into())
.indent_level(5),
]
})
.take(10)
.flatten(),
),
),
)
.child(
input("Find something...")
.value("buffe".to_string())
.state(InteractionState::Focused),
)
}
}

View File

@ -1,24 +1,17 @@
use crate::{
theme::theme,
ui::{chat_panel, project_panel, status_bar, tab_bar, title_bar},
};
use gpui2::{
elements::{div, div::ScrollState},
style::StyleHelpers,
Element, IntoElement, ParentElement, ViewContext,
};
use ui::{chat_panel, project_panel, status_bar, tab_bar, theme, title_bar, toolbar};
#[derive(Element, Default)]
struct WorkspaceElement {
pub struct WorkspaceElement {
left_scroll_state: ScrollState,
right_scroll_state: ScrollState,
tab_bar_scroll_state: ScrollState,
}
pub fn workspace<V: 'static>() -> impl Element<V> {
WorkspaceElement::default()
}
impl WorkspaceElement {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
@ -52,7 +45,8 @@ impl WorkspaceElement {
.flex()
.flex_col()
.flex_1()
.child(tab_bar(self.tab_bar_scroll_state.clone())),
.child(tab_bar(self.tab_bar_scroll_state.clone()))
.child(toolbar()),
),
)
.child(chat_panel(self.right_scroll_state.clone())),

View File

@ -284,12 +284,7 @@ impl TerminalView {
pub fn deploy_context_menu(&mut self, position: Vector2F, cx: &mut ViewContext<Self>) {
let menu_entries = vec![
ContextMenuItem::action("Clear", Clear),
ContextMenuItem::action(
"Close",
pane::CloseActiveItem {
save_behavior: None,
},
),
ContextMenuItem::action("Close", pane::CloseActiveItem { save_intent: None }),
];
self.context_menu.update(cx, |menu, cx| {

12
crates/ui/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "ui"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
anyhow.workspace = true
gpui2 = { path = "../gpui2" }
serde.workspace = true
settings = { path = "../settings" }
theme = { path = "../theme" }

View File

@ -0,0 +1,57 @@
# Elevation
Elevation in Zed applies to all surfaces and components. Elevation is categorized into levels.
Elevation accomplishes the following:
- Allows surfaces to move in front of or behind others, such as content scrolling beneath app top bars.
- Reflects spatial relationships, for instance, how a floating action buttons shadow intimates its disconnection from a collection of cards.
- Directs attention to structures at the highest elevation, like a temporary dialog arising in front of other surfaces.
Elevations are the initial elevation values assigned to components by default.
Components may transition to a higher elevation in some cases, like user interations.
On such occasions, components transition to predetermined dynamic elevation offsets. These are the typical elevations to which components move when they are not at rest.
## Understanding Elevation
Elevation can be thought of as the physical closeness of an element to the user. Elements with lower elevations are physically further away from the user on the z-axis and appear to be underneath elements with higher elevations.
Material Design 3 has a some great visualizations of elevation that may be helpful to understanding the mental modal of elevation. [Material Design Elevation](https://m3.material.io/styles/elevation/overview)
## Elevation Levels
Zed integrates six unique elevation levels in its design system. The elevation of a surface is expressed as a whole number ranging from 0 to 5, both numbers inclusive. A components elevation is ascertained by combining the components resting elevation with any dynamic elevation offsets.
The levels are detailed as follows:
0. App Background
1. UI Surface
2. Elevated Elements
3. Wash
4. Focused Element
5. Dragged Element
### 0. App Background
The app background constitutes the lowest elevation layer, appearing behind all other surfaces and components. It is predominantly used for the background color of the app.
### 1. UI Surface
The UI Surface is the standard elevation for components and is placed above the app background. It is generally used for the background color of the app bar, card, and sheet.
### 2. Elevated Elements
Elevated elements appear above the UI surface layer surfaces and components. Elevated elements are predominantly used for creating popovers, context menus, and tooltips.
### 3. Wash
Wash denotes a distinct elevation reserved to isolate app UI layers from high elevation components such as modals, notifications, and overlaid panels. The wash may not consistently be visible when these components are active. This layer is often referred to as a scrim or overlay and the background color of the wash is typically deployed in its design.
### 4. Focused Element
Focused elements obtain a higher elevation above surfaces and components at wash elevation. They are often used for modals, notifications, and overlaid panels and indicate that they are the sole element the user is interacting with at the moment.
### 5. Dragged Element
Dragged elements gain the highest elevation, thus appearing above surfaces and components at the elevation of focused elements. These are typically used for elements that are being dragged, following the cursor

View File

@ -1,8 +1,53 @@
use gpui2::{
elements::div, interactive::Interactive, platform::MouseButton, style::StyleHelpers, ArcCow,
Element, EventContext, IntoElement, ParentElement, ViewContext,
};
use std::{marker::PhantomData, rc::Rc};
mod breadcrumb;
mod chat_panel;
mod collab_panel;
mod command_palette;
mod facepile;
mod follow_group;
mod icon_button;
mod list;
mod list_item;
mod list_section_header;
mod palette;
mod palette_item;
mod project_panel;
mod status_bar;
mod tab;
mod tab_bar;
mod title_bar;
mod toolbar;
mod traffic_lights;
mod workspace;
pub use breadcrumb::*;
pub use chat_panel::*;
pub use collab_panel::*;
pub use command_palette::*;
pub use facepile::*;
pub use follow_group::*;
pub use icon_button::*;
pub use list::*;
pub use list_item::*;
pub use list_section_header::*;
pub use palette::*;
pub use palette_item::*;
pub use project_panel::*;
pub use status_bar::*;
pub use tab::*;
pub use tab_bar::*;
pub use title_bar::*;
pub use toolbar::*;
pub use traffic_lights::*;
pub use workspace::*;
use std::marker::PhantomData;
use std::rc::Rc;
use gpui2::elements::div;
use gpui2::interactive::Interactive;
use gpui2::platform::MouseButton;
use gpui2::style::StyleHelpers;
use gpui2::{ArcCow, Element, EventContext, IntoElement, ParentElement, ViewContext};
struct ButtonHandlers<V, D> {
click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,

View File

@ -0,0 +1,36 @@
use gpui2::elements::div;
use gpui2::style::{StyleHelpers, Styleable};
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
use crate::theme;
#[derive(Element)]
pub struct Breadcrumb {}
pub fn breadcrumb() -> Breadcrumb {
Breadcrumb {}
}
impl Breadcrumb {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.px_1()
.flex()
.flex_row()
// TODO: Read font from theme (or settings?).
.font("Zed Mono Extended")
.text_sm()
.text_color(theme.middle.base.default.foreground)
.rounded_md()
.hover()
.fill(theme.highest.base.hovered.background)
// TODO: Replace hardcoded breadcrumbs.
.child("crates/ui/src/components/toolbar.rs")
.child(" ")
.child("impl Breadcrumb")
.child(" ")
.child("fn render")
}
}

View File

@ -1,12 +1,13 @@
use std::marker::PhantomData;
use crate::theme::theme;
use crate::ui::icon_button;
use gpui2::elements::div::ScrollState;
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
use crate::theme::theme;
use crate::{icon_button, IconAsset};
#[derive(Element)]
pub struct ChatPanel<V: 'static> {
view_type: PhantomData<V>,
@ -57,8 +58,8 @@ impl<V: 'static> ChatPanel<V> {
.flex()
.items_center()
.gap_px()
.child(icon_button("icons/plus.svg"))
.child(icon_button("icons/split.svg")),
.child(icon_button().icon(IconAsset::Plus))
.child(icon_button().icon(IconAsset::Split)),
),
)
}

View File

@ -0,0 +1,177 @@
use crate::theme::{theme, Theme};
use gpui2::{
elements::{div, div::ScrollState, img, svg},
style::{StyleHelpers, Styleable},
ArcCow, Element, IntoElement, ParentElement, ViewContext,
};
use std::marker::PhantomData;
#[derive(Element)]
pub struct CollabPanelElement<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
}
// When I improve child view rendering, I'd like to have V implement a trait that
// provides the scroll state, among other things.
pub fn collab_panel<V: 'static>(scroll_state: ScrollState) -> CollabPanelElement<V> {
CollabPanelElement {
view_type: PhantomData,
scroll_state,
}
}
impl<V: 'static> CollabPanelElement<V> {
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
// Panel
div()
.w_64()
.h_full()
.flex()
.flex_col()
.font("Zed Sans Extended")
.text_color(theme.middle.base.default.foreground)
.border_color(theme.middle.base.default.border)
.border()
.fill(theme.middle.base.default.background)
.child(
div()
.w_full()
.flex()
.flex_col()
.overflow_y_scroll(self.scroll_state.clone())
// List Container
.child(
div()
.fill(theme.lowest.base.default.background)
.pb_1()
.border_color(theme.lowest.base.default.border)
.border_b()
//:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
// .group()
// List Section Header
.child(self.list_section_header("#CRDB", true, &theme))
// List Item Large
.child(self.list_item(
"http://github.com/maxbrunsfeld.png?s=50",
"maxbrunsfeld",
&theme,
)),
)
.child(
div()
.py_2()
.flex()
.flex_col()
.child(self.list_section_header("CHANNELS", true, &theme)),
)
.child(
div()
.py_2()
.flex()
.flex_col()
.child(self.list_section_header("CONTACTS", true, &theme))
.children(
std::iter::repeat_with(|| {
vec![
self.list_item(
"http://github.com/as-cii.png?s=50",
"as-cii",
&theme,
),
self.list_item(
"http://github.com/nathansobo.png?s=50",
"nathansobo",
&theme,
),
self.list_item(
"http://github.com/maxbrunsfeld.png?s=50",
"maxbrunsfeld",
&theme,
),
]
})
.take(3)
.flatten(),
),
),
)
.child(
div()
.h_7()
.px_2()
.border_t()
.border_color(theme.middle.variant.default.border)
.flex()
.items_center()
.child(
div()
.text_sm()
.text_color(theme.middle.variant.default.foreground)
.child("Find..."),
),
)
}
fn list_section_header(
&self,
label: impl Into<ArcCow<'static, str>>,
expanded: bool,
theme: &Theme,
) -> impl Element<V> {
div()
.h_7()
.px_2()
.flex()
.justify_between()
.items_center()
.child(div().flex().gap_1().text_sm().child(label))
.child(
div().flex().h_full().gap_1().items_center().child(
svg()
.path(if expanded {
"icons/caret_down.svg"
} else {
"icons/caret_up.svg"
})
.w_3p5()
.h_3p5()
.fill(theme.middle.variant.default.foreground),
),
)
}
fn list_item(
&self,
avatar_uri: impl Into<ArcCow<'static, str>>,
label: impl Into<ArcCow<'static, str>>,
theme: &Theme,
) -> impl Element<V> {
div()
.h_7()
.px_2()
.flex()
.items_center()
.hover()
.fill(theme.lowest.variant.hovered.background)
.active()
.fill(theme.lowest.variant.pressed.background)
.child(
div()
.flex()
.items_center()
.gap_1()
.text_sm()
.child(
img()
.uri(avatar_uri)
.size_3p5()
.rounded_full()
.fill(theme.middle.positive.default.foreground),
)
.child(label),
)
}
}

View File

@ -0,0 +1,31 @@
use gpui2::elements::div;
use gpui2::{elements::div::ScrollState, ViewContext};
use gpui2::{Element, IntoElement, ParentElement};
use std::marker::PhantomData;
use crate::{example_editor_actions, palette, OrderMethod};
#[derive(Element)]
pub struct CommandPalette<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
}
pub fn command_palette<V: 'static>(scroll_state: ScrollState) -> CommandPalette<V> {
CommandPalette {
view_type: PhantomData,
scroll_state,
}
}
impl<V: 'static> CommandPalette<V> {
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
div().child(
palette(self.scroll_state.clone())
.items(example_editor_actions())
.placeholder("Execute a command...")
.empty_string("No items found.")
.default_order(OrderMethod::Ascending),
)
}
}

View File

@ -1,15 +1,18 @@
use crate::{theme::theme, ui::Avatar};
use gpui2::elements::div;
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
use crate::{theme, Avatar};
#[derive(Element)]
pub struct Facepile {
players: Vec<Avatar>,
}
pub fn facepile(players: Vec<Avatar>) -> Facepile {
Facepile { players }
pub fn facepile<P: Iterator<Item = Avatar>>(players: P) -> Facepile {
Facepile {
players: players.collect(),
}
}
impl Facepile {

View File

@ -1,8 +1,8 @@
use crate::theme::theme;
use crate::ui::{facepile, indicator, Avatar};
use gpui2::elements::div;
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
use crate::{facepile, indicator, theme, Avatar};
#[derive(Element)]
pub struct FollowGroup {
@ -46,7 +46,7 @@ impl FollowGroup {
.px_1()
.rounded_lg()
.fill(player_bg)
.child(facepile(self.players.clone())),
.child(facepile(self.players.clone().into_iter())),
)
}
}

View File

@ -1,26 +1,47 @@
use crate::prelude::{ButtonVariant, InteractionState};
use crate::theme::theme;
use gpui2::elements::svg;
use gpui2::elements::div;
use gpui2::style::{StyleHelpers, Styleable};
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
use crate::{icon, theme, IconColor};
use crate::{prelude::*, IconAsset};
#[derive(Element)]
pub struct IconButton {
path: &'static str,
icon: IconAsset,
color: IconColor,
variant: ButtonVariant,
state: InteractionState,
}
pub fn icon_button(path: &'static str) -> IconButton {
pub fn icon_button() -> IconButton {
IconButton {
path,
icon: IconAsset::default(),
color: IconColor::default(),
variant: ButtonVariant::default(),
state: InteractionState::default(),
}
}
impl IconButton {
pub fn new(icon: IconAsset) -> Self {
Self {
icon,
color: IconColor::default(),
variant: ButtonVariant::default(),
state: InteractionState::default(),
}
}
pub fn icon(mut self, icon: IconAsset) -> Self {
self.icon = icon;
self
}
pub fn color(mut self, color: IconColor) -> Self {
self.color = color;
self
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self
@ -34,13 +55,10 @@ impl IconButton {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let icon_color;
if self.state == InteractionState::Disabled {
icon_color = theme.highest.base.disabled.foreground;
} else {
icon_color = theme.highest.base.default.foreground;
}
let icon_color = match (self.state, self.color) {
(InteractionState::Disabled, _) => IconColor::Disabled,
_ => self.color,
};
let mut div = div();
if self.variant == ButtonVariant::Filled {
@ -57,6 +75,6 @@ impl IconButton {
.fill(theme.highest.base.hovered.background)
.active()
.fill(theme.highest.base.pressed.background)
.child(svg().path(self.path).w_4().h_4().fill(icon_color))
.child(icon(self.icon).color(icon_color))
}
}

View File

@ -0,0 +1,64 @@
use crate::theme::theme;
use crate::tokens::token;
use crate::{icon, label, prelude::*, IconAsset, LabelColor, ListItem, ListSectionHeader};
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub struct List {
header: Option<ListSectionHeader>,
items: Vec<ListItem>,
empty_message: &'static str,
toggle: Option<ToggleState>,
// footer: Option<ListSectionFooter>,
}
pub fn list(items: Vec<ListItem>) -> List {
List {
header: None,
items,
empty_message: "No items",
toggle: None,
}
}
impl List {
pub fn header(mut self, header: ListSectionHeader) -> Self {
self.header = Some(header);
self
}
pub fn empty_message(mut self, empty_message: &'static str) -> Self {
self.empty_message = empty_message;
self
}
pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
self.toggle = Some(toggle);
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let token = token();
let disclosure_control = match self.toggle {
Some(ToggleState::NotToggled) => Some(icon(IconAsset::ChevronRight)),
Some(ToggleState::Toggled) => Some(icon(IconAsset::ChevronDown)),
None => None,
};
div()
.py_1()
.flex()
.flex_col()
.children(self.header.map(|h| h))
.children(
self.items
.is_empty()
.then(|| label(self.empty_message).color(LabelColor::Muted)),
)
.children(self.items.iter().cloned())
}
}

View File

@ -1,17 +1,18 @@
use crate::prelude::{InteractionState, ToggleState};
use crate::prelude::{DisclosureControlVisibility, InteractionState, ToggleState};
use crate::theme::theme;
use crate::ui::{icon, IconAsset, Label};
use gpui2::geometry::rems;
use crate::tokens::token;
use crate::{icon, IconAsset, Label};
use gpui2::style::{StyleHelpers, Styleable};
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
#[derive(Element, Clone)]
pub struct ListItem {
label: Label,
left_icon: Option<IconAsset>,
indent_level: u32,
state: InteractionState,
disclosure_control_style: DisclosureControlVisibility,
toggle: Option<ToggleState>,
}
@ -20,6 +21,7 @@ pub fn list_item(label: Label) -> ListItem {
label,
indent_level: 0,
left_icon: None,
disclosure_control_style: DisclosureControlVisibility::default(),
state: InteractionState::default(),
toggle: None,
}
@ -46,8 +48,30 @@ impl ListItem {
self
}
pub fn disclosure_control_style(
mut self,
disclosure_control_style: DisclosureControlVisibility,
) -> Self {
self.disclosure_control_style = disclosure_control_style;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let token = token();
let mut disclosure_control = match self.toggle {
Some(ToggleState::NotToggled) => Some(div().child(icon(IconAsset::ChevronRight))),
Some(ToggleState::Toggled) => Some(div().child(icon(IconAsset::ChevronDown))),
None => Some(div()),
};
match self.disclosure_control_style {
DisclosureControlVisibility::OnHover => {
disclosure_control =
disclosure_control.map(|c| div().absolute().neg_left_5().child(c));
}
DisclosureControlVisibility::Always => {}
}
div()
.fill(theme.middle.base.default.background)
@ -56,31 +80,31 @@ impl ListItem {
.active()
.fill(theme.middle.base.pressed.background)
.relative()
.py_1()
.child(
div()
.h_7()
.h_6()
.px_2()
// .ml(rems(0.75 * self.indent_level as f32))
.children((0..self.indent_level).map(|_| {
div().w(rems(0.75)).h_full().flex().justify_center().child(
div()
.w_px()
.h_full()
.fill(theme.middle.base.default.border)
.hover()
.fill(theme.middle.warning.default.border)
.active()
.fill(theme.middle.negative.default.border),
)
div()
.w(token.list_indent_depth)
.h_full()
.flex()
.justify_center()
.child(
div()
.ml_px()
.w_px()
.h_full()
.fill(theme.middle.base.default.border),
)
}))
.flex()
.gap_2()
.gap_1()
.items_center()
.children(match self.toggle {
Some(ToggleState::NotToggled) => Some(icon(IconAsset::ChevronRight)),
Some(ToggleState::Toggled) => Some(icon(IconAsset::ChevronDown)),
None => None,
})
.relative()
.children(disclosure_control)
.children(self.left_icon.map(|i| icon(i)))
.child(self.label.clone()),
)

View File

@ -0,0 +1,88 @@
use crate::prelude::{InteractionState, ToggleState};
use crate::theme::theme;
use crate::tokens::token;
use crate::{icon, label, IconAsset, LabelColor, LabelSize};
use gpui2::style::{StyleHelpers, Styleable};
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element, Clone, Copy)]
pub struct ListSectionHeader {
label: &'static str,
left_icon: Option<IconAsset>,
state: InteractionState,
toggle: Option<ToggleState>,
}
pub fn list_section_header(label: &'static str) -> ListSectionHeader {
ListSectionHeader {
label,
left_icon: None,
state: InteractionState::default(),
toggle: None,
}
}
impl ListSectionHeader {
pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
self.toggle = Some(toggle);
self
}
pub fn left_icon(mut self, left_icon: Option<IconAsset>) -> Self {
self.left_icon = left_icon;
self
}
pub fn state(mut self, state: InteractionState) -> Self {
self.state = state;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let token = token();
let disclosure_control = match self.toggle {
Some(ToggleState::NotToggled) => Some(div().child(icon(IconAsset::ChevronRight))),
Some(ToggleState::Toggled) => Some(div().child(icon(IconAsset::ChevronDown))),
None => Some(div()),
};
div()
.flex()
.flex_1()
.w_full()
.fill(theme.middle.base.default.background)
.hover()
.fill(theme.middle.base.hovered.background)
.active()
.fill(theme.middle.base.pressed.background)
.relative()
.py_1()
.child(
div()
.h_6()
.px_2()
.flex()
.flex_1()
.w_full()
.gap_1()
.items_center()
.justify_between()
.child(
div()
.flex()
.gap_1()
.items_center()
.children(self.left_icon.map(|i| icon(i)))
.child(
label(self.label.clone())
.color(LabelColor::Muted)
.size(LabelSize::Small),
),
)
.children(disclosure_control),
)
}
}

View File

@ -0,0 +1,124 @@
use std::marker::PhantomData;
use crate::prelude::OrderMethod;
use crate::theme::theme;
use crate::{label, palette_item, LabelColor, PaletteItem};
use gpui2::elements::div::ScrollState;
use gpui2::style::{StyleHelpers, Styleable};
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub struct Palette<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
input_placeholder: &'static str,
empty_string: &'static str,
items: Vec<PaletteItem>,
default_order: OrderMethod,
}
pub fn palette<V: 'static>(scroll_state: ScrollState) -> Palette<V> {
Palette {
view_type: PhantomData,
scroll_state,
input_placeholder: "Find something...",
empty_string: "No items found.",
items: vec![],
default_order: OrderMethod::default(),
}
}
impl<V: 'static> Palette<V> {
pub fn items(mut self, mut items: Vec<PaletteItem>) -> Self {
items.sort_by_key(|item| item.label);
self.items = items;
self
}
pub fn placeholder(mut self, input_placeholder: &'static str) -> Self {
self.input_placeholder = input_placeholder;
self
}
pub fn empty_string(mut self, empty_string: &'static str) -> Self {
self.empty_string = empty_string;
self
}
// TODO: Hook up sort order
pub fn default_order(mut self, default_order: OrderMethod) -> Self {
self.default_order = default_order;
self
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.w_96()
.rounded_lg()
.fill(theme.lowest.base.default.background)
.border()
.border_color(theme.lowest.base.default.border)
.flex()
.flex_col()
.child(
div()
.flex()
.flex_col()
.gap_px()
.child(
div().py_0p5().px_1().flex().flex_col().child(
div().px_2().py_0p5().child(
label(self.input_placeholder).color(LabelColor::Placeholder),
),
),
)
.child(div().h_px().w_full().fill(theme.lowest.base.default.border))
.child(
div()
.py_0p5()
.px_1()
.flex()
.flex_col()
.grow()
.max_h_96()
.overflow_y_scroll(self.scroll_state.clone())
.children(
vec![if self.items.is_empty() {
Some(
div()
.flex()
.flex_row()
.justify_between()
.px_2()
.py_1()
.child(
label(self.empty_string).color(LabelColor::Muted),
),
)
} else {
None
}]
.into_iter()
.flatten(),
)
.children(self.items.iter().map(|item| {
div()
.flex()
.flex_row()
.justify_between()
.px_2()
.py_0p5()
.rounded_lg()
.hover()
.fill(theme.lowest.base.hovered.background)
.active()
.fill(theme.lowest.base.pressed.background)
.child(palette_item(item.label, item.keybinding))
})),
),
)
}
}

View File

@ -0,0 +1,63 @@
use crate::theme::theme;
use crate::{label, LabelColor, LabelSize};
use gpui2::elements::div;
use gpui2::style::StyleHelpers;
use gpui2::{Element, IntoElement};
use gpui2::{ParentElement, ViewContext};
#[derive(Element)]
pub struct PaletteItem {
pub label: &'static str,
pub keybinding: Option<&'static str>,
}
pub fn palette_item(label: &'static str, keybinding: Option<&'static str>) -> PaletteItem {
PaletteItem { label, keybinding }
}
impl PaletteItem {
pub fn label(mut self, label: &'static str) -> Self {
self.label = label;
self
}
pub fn keybinding(mut self, keybinding: Option<&'static str>) -> Self {
self.keybinding = keybinding;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let keybinding_label = match self.keybinding {
Some(keybind) => label(keybind)
.color(LabelColor::Muted)
.size(LabelSize::Small),
None => label(""),
};
div()
.flex()
.flex_row()
.grow()
.justify_between()
.child(label(self.label))
.child(
self.keybinding
.map(|_| {
div()
.flex()
.items_center()
.justify_center()
.px_1()
.py_0()
.my_0p5()
.rounded_md()
.text_sm()
.fill(theme.lowest.on.default.background)
.child(keybinding_label)
})
.unwrap_or_else(|| div()),
)
}
}

View File

@ -0,0 +1,62 @@
use crate::{
input, list, list_section_header, prelude::*, static_project_panel_project_items,
static_project_panel_single_items, theme,
};
use gpui2::{
elements::{div, div::ScrollState},
style::StyleHelpers,
ParentElement, ViewContext,
};
use gpui2::{Element, IntoElement};
use std::marker::PhantomData;
#[derive(Element)]
pub struct ProjectPanel<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
}
pub fn project_panel<V: 'static>(scroll_state: ScrollState) -> ProjectPanel<V> {
ProjectPanel {
view_type: PhantomData,
scroll_state,
}
}
impl<V: 'static> ProjectPanel<V> {
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.w_56()
.h_full()
.flex()
.flex_col()
.fill(theme.middle.base.default.background)
.child(
div()
.w_56()
.flex()
.flex_col()
.overflow_y_scroll(self.scroll_state.clone())
.child(
list(static_project_panel_single_items())
.header(list_section_header("FILES").set_toggle(ToggleState::Toggled))
.empty_message("No files in directory")
.set_toggle(ToggleState::Toggled),
)
.child(
list(static_project_panel_project_items())
.header(list_section_header("PROJECT").set_toggle(ToggleState::Toggled))
.empty_message("No folders in directory")
.set_toggle(ToggleState::Toggled),
),
)
.child(
input("Find something...")
.value("buffe".to_string())
.state(InteractionState::Focused),
)
}
}

View File

@ -1,11 +1,12 @@
use std::marker::PhantomData;
use crate::theme::{theme, Theme};
use crate::ui::{icon_button, text_button, tool_divider};
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
use crate::theme::{theme, Theme};
use crate::{icon_button, text_button, tool_divider, IconAsset};
#[derive(Default, PartialEq)]
pub enum Tool {
#[default]
@ -96,8 +97,8 @@ impl<V: 'static> StatusBar<V> {
.justify_between()
.w_full()
.fill(theme.lowest.base.default.background)
.child(self.left_tools(theme))
.child(self.right_tools(theme))
.child(self.left_tools(&theme))
.child(self.right_tools(&theme))
}
fn left_tools(&self, theme: &Theme) -> impl Element<V> {
@ -105,10 +106,10 @@ impl<V: 'static> StatusBar<V> {
.flex()
.items_center()
.gap_1()
.child(icon_button("icons/project.svg"))
.child(icon_button("icons/hash.svg"))
.child(icon_button().icon(IconAsset::FileTree))
.child(icon_button().icon(IconAsset::Hash))
.child(tool_divider())
.child(icon_button("icons/error.svg"))
.child(icon_button().icon(IconAsset::XCircle))
}
fn right_tools(&self, theme: &Theme) -> impl Element<V> {
div()
@ -129,8 +130,8 @@ impl<V: 'static> StatusBar<V> {
.flex()
.items_center()
.gap_1()
.child(icon_button("icons/copilot.svg"))
.child(icon_button("icons/feedback.svg")),
.child(icon_button().icon(IconAsset::Copilot))
.child(icon_button().icon(IconAsset::Envelope)),
)
.child(tool_divider())
.child(
@ -138,9 +139,9 @@ impl<V: 'static> StatusBar<V> {
.flex()
.items_center()
.gap_1()
.child(icon_button("icons/terminal.svg"))
.child(icon_button("icons/conversations.svg"))
.child(icon_button("icons/ai.svg")),
.child(icon_button().icon(IconAsset::Terminal))
.child(icon_button().icon(IconAsset::MessageBubbles))
.child(icon_button().icon(IconAsset::Ai)),
)
}
}

View File

@ -1,7 +1,8 @@
use crate::theme::theme;
use gpui2::elements::div;
use gpui2::style::{StyleHelpers, Styleable};
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
use crate::theme;
#[derive(Element)]
pub struct Tab {

View File

@ -1,13 +1,14 @@
use std::marker::PhantomData;
use crate::prelude::InteractionState;
use crate::theme::theme;
use crate::ui::{icon_button, tab};
use gpui2::elements::div::ScrollState;
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
use crate::prelude::InteractionState;
use crate::theme::theme;
use crate::{icon_button, tab, IconAsset};
#[derive(Element)]
pub struct TabBar<V: 'static> {
view_type: PhantomData<V>,
@ -43,11 +44,12 @@ impl<V: 'static> TabBar<V> {
.items_center()
.gap_px()
.child(
icon_button("icons/arrow_left.svg")
icon_button()
.icon(IconAsset::ArrowLeft)
.state(InteractionState::Enabled.if_enabled(can_navigate_back)),
)
.child(
icon_button("icons/arrow_right.svg").state(
icon_button().icon(IconAsset::ArrowRight).state(
InteractionState::Enabled.if_enabled(can_navigate_forward),
),
),
@ -83,8 +85,8 @@ impl<V: 'static> TabBar<V> {
.flex()
.items_center()
.gap_px()
.child(icon_button("icons/plus.svg"))
.child(icon_button("icons/split.svg")),
.child(icon_button().icon(IconAsset::Plus))
.child(icon_button().icon(IconAsset::Split)),
),
)
}

View File

@ -1,11 +1,14 @@
use std::marker::PhantomData;
use crate::prelude::Shape;
use crate::theme::theme;
use crate::ui::{avatar, follow_group, icon_button, text_button, tool_divider};
use gpui2::elements::div;
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
use crate::prelude::Shape;
use crate::{
avatar, follow_group, icon_button, text_button, theme, tool_divider, traffic_lights, IconAsset,
IconColor,
};
#[derive(Element)]
pub struct TitleBar<V: 'static> {
@ -40,34 +43,7 @@ impl<V: 'static> TitleBar<V> {
.h_full()
.gap_4()
.px_2()
// === Traffic Lights === //
.child(
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.w_3()
.h_3()
.rounded_full()
.fill(theme.lowest.positive.default.foreground),
)
.child(
div()
.w_3()
.h_3()
.rounded_full()
.fill(theme.lowest.warning.default.foreground),
)
.child(
div()
.w_3()
.h_3()
.rounded_full()
.fill(theme.lowest.negative.default.foreground),
),
)
.child(traffic_lights())
// === Project Info === //
.child(
div()
@ -92,8 +68,8 @@ impl<V: 'static> TitleBar<V> {
.flex()
.items_center()
.gap_1()
.child(icon_button("icons/stop_sharing.svg"))
.child(icon_button("icons/exit.svg")),
.child(icon_button().icon(IconAsset::FolderX))
.child(icon_button().icon(IconAsset::Close)),
)
.child(tool_divider())
.child(
@ -102,9 +78,13 @@ impl<V: 'static> TitleBar<V> {
.flex()
.items_center()
.gap_1()
.child(icon_button("icons/mic.svg"))
.child(icon_button("icons/speaker-loud.svg"))
.child(icon_button("icons/desktop.svg")),
.child(icon_button().icon(IconAsset::Mic))
.child(icon_button().icon(IconAsset::AudioOn))
.child(
icon_button()
.icon(IconAsset::Screen)
.color(IconColor::Accent),
),
)
.child(
div().px_2().flex().items_center().child(

View File

@ -0,0 +1,35 @@
use gpui2::elements::div;
use gpui2::style::StyleHelpers;
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
use crate::{breadcrumb, theme, IconAsset, IconButton};
pub struct ToolbarItem {}
#[derive(Element)]
pub struct Toolbar {
items: Vec<ToolbarItem>,
}
pub fn toolbar() -> Toolbar {
Toolbar { items: Vec::new() }
}
impl Toolbar {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.p_2()
.flex()
.justify_between()
.child(breadcrumb())
.child(
div()
.flex()
.child(IconButton::new(IconAsset::InlayHint))
.child(IconButton::new(IconAsset::MagnifyingGlass))
.child(IconButton::new(IconAsset::MagicWand)),
)
}
}

View File

@ -0,0 +1,30 @@
use gpui2::elements::div;
use gpui2::style::StyleHelpers;
use gpui2::{Element, Hsla, IntoElement, ParentElement, ViewContext};
use crate::theme;
#[derive(Element)]
pub struct TrafficLights {}
pub fn traffic_lights() -> TrafficLights {
TrafficLights {}
}
impl TrafficLights {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.flex()
.items_center()
.gap_2()
.child(traffic_light(theme.lowest.negative.default.foreground))
.child(traffic_light(theme.lowest.warning.default.foreground))
.child(traffic_light(theme.lowest.positive.default.foreground))
}
}
fn traffic_light<V: 'static, C: Into<Hsla>>(fill: C) -> div::Div<V> {
div().w_3().h_3().rounded_full().fill(fill.into())
}

View File

@ -0,0 +1,80 @@
use crate::{chat_panel, collab_panel, project_panel, status_bar, tab_bar, theme, title_bar};
use gpui2::{
elements::{div, div::ScrollState},
style::StyleHelpers,
Element, IntoElement, ParentElement, ViewContext,
};
#[derive(Element, Default)]
struct WorkspaceElement {
project_panel_scroll_state: ScrollState,
collab_panel_scroll_state: ScrollState,
right_scroll_state: ScrollState,
tab_bar_scroll_state: ScrollState,
palette_scroll_state: ScrollState,
}
pub fn workspace<V: 'static>() -> impl Element<V> {
WorkspaceElement::default()
}
impl WorkspaceElement {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
// Elevation Level 0
.size_full()
.flex()
.flex_col()
.font("Zed Sans Extended")
.gap_0()
.justify_start()
.items_start()
.text_color(theme.lowest.base.default.foreground)
.fill(theme.lowest.base.default.background)
.relative()
// Elevation Level 1
.child(title_bar())
.child(
div()
.flex_1()
.w_full()
.flex()
.flex_row()
.overflow_hidden()
.child(project_panel(self.project_panel_scroll_state.clone()))
.child(collab_panel(self.collab_panel_scroll_state.clone()))
.child(
div()
.h_full()
.flex_1()
.fill(theme.highest.base.default.background)
.child(
div()
.flex()
.flex_col()
.flex_1()
.child(tab_bar(self.tab_bar_scroll_state.clone())),
),
)
.child(chat_panel(self.right_scroll_state.clone())),
)
.child(status_bar())
// Elevation Level 3
// .child(
// div()
// .absolute()
// .top_0()
// .left_0()
// .size_full()
// .flex()
// .justify_center()
// .items_center()
// // .fill(theme.lowest.base.default.background)
// // Elevation Level 4
// .child(command_palette(self.palette_scroll_state.clone())),
// )
}
}

View File

@ -1,7 +1,9 @@
use crate::theme::{Theme, Themed};
use gpui2::Element;
use std::marker::PhantomData;
use gpui2::Element;
use crate::theme::{Theme, Themed};
pub trait ElementExt<V: 'static>: Element<V> {
fn themed(self, theme: Theme) -> Themed<V, Self>
where

17
crates/ui/src/elements.rs Normal file
View File

@ -0,0 +1,17 @@
mod avatar;
mod details;
mod icon;
mod indicator;
mod input;
mod label;
mod text_button;
mod tool_divider;
pub use avatar::*;
pub use details::*;
pub use icon::*;
pub use indicator::*;
pub use input::*;
pub use label::*;
pub use text_button::*;
pub use tool_divider::*;

Some files were not shown because too many files have changed in this diff Show More