Compare commits
69 Commits
show-lua-s
...
icon_previ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d64e7f73c1 | ||
|
|
cfb9a4beb0 | ||
|
|
9902cd54ce | ||
|
|
96510b72b8 | ||
|
|
a364a13458 | ||
|
|
09a4cfd307 | ||
|
|
5d66c3db85 | ||
|
|
28f33d0103 | ||
|
|
55a90f576a | ||
|
|
8d6abf6537 | ||
|
|
04961a0186 | ||
|
|
fd7ab20ea4 | ||
|
|
7019aca59d | ||
|
|
d43bcc04db | ||
|
|
2b94a35aaa | ||
|
|
e8208643bb | ||
|
|
a90f80725f | ||
|
|
4e6c37d23b | ||
|
|
0cf6259fec | ||
|
|
5cb5e92185 | ||
|
|
da61a28839 | ||
|
|
efdb769f9b | ||
|
|
9cce5a650e | ||
|
|
2021ca5bff | ||
|
|
1771250b04 | ||
|
|
18259c0fd4 | ||
|
|
41ddd1cc97 | ||
|
|
e175878008 | ||
|
|
1cfbfc199c | ||
|
|
f59f2caf7e | ||
|
|
401342c6ec | ||
|
|
0df1e4a489 | ||
|
|
9bd3e156f5 | ||
|
|
42c655751b | ||
|
|
ff1d78df3b | ||
|
|
c2e4fdf63d | ||
|
|
bf11b888c3 | ||
|
|
d562f58e76 | ||
|
|
94e4aa626d | ||
|
|
8ceba89d81 | ||
|
|
c37d6d5fed | ||
|
|
1a3597d726 | ||
|
|
c747cccde3 | ||
|
|
d81e7683ea | ||
|
|
8b29ee6033 | ||
|
|
96a75e08af | ||
|
|
06cbff6714 | ||
|
|
ce05813e7c | ||
|
|
4d1d8d6d78 | ||
|
|
1f8b14f4f1 | ||
|
|
082cc6184c | ||
|
|
6cfc4dc857 | ||
|
|
b9c48685e8 | ||
|
|
570c396e84 | ||
|
|
5fd034e604 | ||
|
|
63dab5f891 | ||
|
|
a2d6df3ed6 | ||
|
|
30e86ac939 | ||
|
|
976fc3ee97 | ||
|
|
63091459d8 | ||
|
|
659fae70f8 | ||
|
|
02e970192f | ||
|
|
5ecc67f2ef | ||
|
|
73dfb10c16 | ||
|
|
e513e81046 | ||
|
|
2fc4dec58f | ||
|
|
3891381d3e | ||
|
|
b91e929086 | ||
|
|
013a646799 |
218
Cargo.lock
generated
218
Cargo.lock
generated
@@ -450,7 +450,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assistant_context_editor",
|
"assistant_context_editor",
|
||||||
"assistant_scripting",
|
|
||||||
"assistant_settings",
|
"assistant_settings",
|
||||||
"assistant_slash_command",
|
"assistant_slash_command",
|
||||||
"assistant_tool",
|
"assistant_tool",
|
||||||
@@ -490,8 +489,8 @@ dependencies = [
|
|||||||
"prompt_store",
|
"prompt_store",
|
||||||
"proto",
|
"proto",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rich_text",
|
|
||||||
"rope",
|
"rope",
|
||||||
|
"scripting_tool",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
@@ -565,26 +564,6 @@ dependencies = [
|
|||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "assistant_scripting"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"collections",
|
|
||||||
"futures 0.3.31",
|
|
||||||
"gpui",
|
|
||||||
"log",
|
|
||||||
"mlua",
|
|
||||||
"parking_lot",
|
|
||||||
"project",
|
|
||||||
"rand 0.8.5",
|
|
||||||
"regex",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"settings",
|
|
||||||
"util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assistant_settings"
|
name = "assistant_settings"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1255,27 +1234,25 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-rs"
|
name = "aws-lc-rs"
|
||||||
version = "1.12.2"
|
version = "1.12.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c2b7ddaa2c56a367ad27a094ad8ef4faacf8a617c2575acb2ba88949df999ca"
|
checksum = "dabb68eb3a7aa08b46fddfd59a3d55c978243557a90ab804769f7e20e67d2b01"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-sys",
|
"aws-lc-sys",
|
||||||
"paste",
|
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-sys"
|
name = "aws-lc-sys"
|
||||||
version = "0.25.0"
|
version = "0.27.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "71b2ddd3ada61a305e1d8bb6c005d1eaa7d14d903681edfc400406d523a9b491"
|
checksum = "6bbe221bbf523b625a4dd8585c7f38166e31167ec2ca98051dbcb4c3b6e825d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen 0.69.5",
|
"bindgen 0.69.5",
|
||||||
"cc",
|
"cc",
|
||||||
"cmake",
|
"cmake",
|
||||||
"dunce",
|
"dunce",
|
||||||
"fs_extra",
|
"fs_extra",
|
||||||
"paste",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1845,7 +1822,7 @@ dependencies = [
|
|||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.12.1",
|
"itertools 0.10.5",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"lazycell",
|
"lazycell",
|
||||||
"log",
|
"log",
|
||||||
@@ -1868,7 +1845,7 @@ dependencies = [
|
|||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.12.1",
|
"itertools 0.10.5",
|
||||||
"log",
|
"log",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2322,7 +2299,7 @@ dependencies = [
|
|||||||
"cap-primitives",
|
"cap-primitives",
|
||||||
"cap-std",
|
"cap-std",
|
||||||
"io-lifetimes",
|
"io-lifetimes",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2350,7 +2327,7 @@ dependencies = [
|
|||||||
"ipnet",
|
"ipnet",
|
||||||
"maybe-owned",
|
"maybe-owned",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
"winx",
|
"winx",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2691,7 +2668,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"util",
|
"util",
|
||||||
"windows 0.58.0",
|
"windows 0.60.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2733,7 +2710,7 @@ dependencies = [
|
|||||||
"tokio-socks",
|
"tokio-socks",
|
||||||
"url",
|
"url",
|
||||||
"util",
|
"util",
|
||||||
"windows 0.58.0",
|
"windows 0.60.0",
|
||||||
"worktree",
|
"worktree",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3074,6 +3051,7 @@ dependencies = [
|
|||||||
"languages",
|
"languages",
|
||||||
"notifications",
|
"notifications",
|
||||||
"project",
|
"project",
|
||||||
|
"strum",
|
||||||
"ui",
|
"ui",
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
@@ -4424,7 +4402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4479,7 +4457,6 @@ dependencies = [
|
|||||||
"env_logger 0.11.6",
|
"env_logger 0.11.6",
|
||||||
"feature_flags",
|
"feature_flags",
|
||||||
"fs",
|
"fs",
|
||||||
"git",
|
|
||||||
"gpui",
|
"gpui",
|
||||||
"http_client",
|
"http_client",
|
||||||
"language",
|
"language",
|
||||||
@@ -5075,7 +5052,7 @@ dependencies = [
|
|||||||
"text",
|
"text",
|
||||||
"time",
|
"time",
|
||||||
"util",
|
"util",
|
||||||
"windows 0.58.0",
|
"windows 0.60.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5086,7 +5063,7 @@ checksum = "5e2e6123af26f0f2c51cc66869137080199406754903cc926a7690401ce09cb4"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"io-lifetimes",
|
"io-lifetimes",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5472,7 +5449,6 @@ dependencies = [
|
|||||||
"db",
|
"db",
|
||||||
"editor",
|
"editor",
|
||||||
"env_logger 0.11.6",
|
"env_logger 0.11.6",
|
||||||
"feature_flags",
|
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"git",
|
"git",
|
||||||
@@ -5485,6 +5461,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"menu",
|
"menu",
|
||||||
"multi_buffer",
|
"multi_buffer",
|
||||||
|
"notifications",
|
||||||
"panel",
|
"panel",
|
||||||
"picker",
|
"picker",
|
||||||
"postage",
|
"postage",
|
||||||
@@ -5500,10 +5477,11 @@ dependencies = [
|
|||||||
"telemetry",
|
"telemetry",
|
||||||
"theme",
|
"theme",
|
||||||
"time",
|
"time",
|
||||||
|
"time_format",
|
||||||
"ui",
|
"ui",
|
||||||
"unindent",
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
"windows 0.58.0",
|
"windows 0.60.0",
|
||||||
"workspace",
|
"workspace",
|
||||||
"zed_actions",
|
"zed_actions",
|
||||||
]
|
]
|
||||||
@@ -5703,8 +5681,8 @@ dependencies = [
|
|||||||
"wayland-cursor",
|
"wayland-cursor",
|
||||||
"wayland-protocols",
|
"wayland-protocols",
|
||||||
"wayland-protocols-plasma",
|
"wayland-protocols-plasma",
|
||||||
"windows 0.58.0",
|
"windows 0.60.0",
|
||||||
"windows-core 0.58.0",
|
"windows-core 0.60.1",
|
||||||
"x11-clipboard",
|
"x11-clipboard",
|
||||||
"x11rb",
|
"x11rb",
|
||||||
"xim",
|
"xim",
|
||||||
@@ -6731,7 +6709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65"
|
checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"io-lifetimes",
|
"io-lifetimes",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7351,7 +7329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10529,7 +10507,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 1.10.1",
|
"bytes 1.10.1",
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"itertools 0.12.1",
|
"itertools 0.10.5",
|
||||||
"log",
|
"log",
|
||||||
"multimap 0.10.0",
|
"multimap 0.10.0",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -10562,7 +10540,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
|
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools 0.12.1",
|
"itertools 0.10.5",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.90",
|
"syn 2.0.90",
|
||||||
@@ -10756,7 +10734,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -11678,7 +11656,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -11932,6 +11910,28 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
|
checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scripting_tool"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"collections",
|
||||||
|
"futures 0.3.31",
|
||||||
|
"gpui",
|
||||||
|
"language",
|
||||||
|
"log",
|
||||||
|
"mlua",
|
||||||
|
"parking_lot",
|
||||||
|
"project",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"regex",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"settings",
|
||||||
|
"util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scrypt"
|
name = "scrypt"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -13428,7 +13428,7 @@ dependencies = [
|
|||||||
"fd-lock",
|
"fd-lock",
|
||||||
"io-lifetimes",
|
"io-lifetimes",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
"winx",
|
"winx",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -13568,7 +13568,7 @@ dependencies = [
|
|||||||
"getrandom 0.3.1",
|
"getrandom 0.3.1",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -13615,7 +13615,7 @@ dependencies = [
|
|||||||
"theme",
|
"theme",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"util",
|
"util",
|
||||||
"windows 0.58.0",
|
"windows 0.60.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -13992,7 +13992,7 @@ dependencies = [
|
|||||||
"tree-sitter-md",
|
"tree-sitter-md",
|
||||||
"ui",
|
"ui",
|
||||||
"util",
|
"util",
|
||||||
"windows 0.58.0",
|
"windows 0.60.0",
|
||||||
"workspace",
|
"workspace",
|
||||||
"zed_actions",
|
"zed_actions",
|
||||||
"zeta",
|
"zeta",
|
||||||
@@ -14699,7 +14699,7 @@ dependencies = [
|
|||||||
"theme",
|
"theme",
|
||||||
"ui_macros",
|
"ui_macros",
|
||||||
"util",
|
"util",
|
||||||
"windows 0.58.0",
|
"windows 0.60.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -15914,7 +15914,7 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -15971,6 +15971,28 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.60.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529"
|
||||||
|
dependencies = [
|
||||||
|
"windows-collections",
|
||||||
|
"windows-core 0.60.1",
|
||||||
|
"windows-future",
|
||||||
|
"windows-link",
|
||||||
|
"windows-numerics",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-collections"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.60.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
@@ -16011,10 +16033,33 @@ dependencies = [
|
|||||||
"windows-implement 0.58.0",
|
"windows-implement 0.58.0",
|
||||||
"windows-interface 0.58.0",
|
"windows-interface 0.58.0",
|
||||||
"windows-result 0.2.0",
|
"windows-result 0.2.0",
|
||||||
"windows-strings",
|
"windows-strings 0.1.0",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.60.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement 0.59.0",
|
||||||
|
"windows-interface 0.59.0",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result 0.3.1",
|
||||||
|
"windows-strings 0.3.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-future"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.60.1",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-implement"
|
name = "windows-implement"
|
||||||
version = "0.57.0"
|
version = "0.57.0"
|
||||||
@@ -16037,6 +16082,17 @@ dependencies = [
|
|||||||
"syn 2.0.90",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.90",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-interface"
|
name = "windows-interface"
|
||||||
version = "0.57.0"
|
version = "0.57.0"
|
||||||
@@ -16059,12 +16115,33 @@ dependencies = [
|
|||||||
"syn 2.0.90",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.90",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
|
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-numerics"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.60.1",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-registry"
|
name = "windows-registry"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -16072,7 +16149,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
|
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-result 0.2.0",
|
"windows-result 0.2.0",
|
||||||
"windows-strings",
|
"windows-strings 0.1.0",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -16094,6 +16171,15 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-strings"
|
name = "windows-strings"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -16104,6 +16190,15 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.45.0"
|
version = "0.45.0"
|
||||||
@@ -16379,7 +16474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
|
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -17020,7 +17115,7 @@ dependencies = [
|
|||||||
"vim",
|
"vim",
|
||||||
"vim_mode_setting",
|
"vim_mode_setting",
|
||||||
"welcome",
|
"welcome",
|
||||||
"windows 0.58.0",
|
"windows 0.60.0",
|
||||||
"winresource",
|
"winresource",
|
||||||
"workspace",
|
"workspace",
|
||||||
"zed_actions",
|
"zed_actions",
|
||||||
@@ -17117,13 +17212,6 @@ dependencies = [
|
|||||||
"zed_extension_api 0.1.0",
|
"zed_extension_api 0.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zed_terraform"
|
|
||||||
version = "0.1.2"
|
|
||||||
dependencies = [
|
|
||||||
"zed_extension_api 0.1.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed_test_extension"
|
name = "zed_test_extension"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
17
Cargo.toml
17
Cargo.toml
@@ -8,7 +8,6 @@ members = [
|
|||||||
"crates/assistant",
|
"crates/assistant",
|
||||||
"crates/assistant2",
|
"crates/assistant2",
|
||||||
"crates/assistant_context_editor",
|
"crates/assistant_context_editor",
|
||||||
"crates/assistant_scripting",
|
|
||||||
"crates/assistant_settings",
|
"crates/assistant_settings",
|
||||||
"crates/assistant_slash_command",
|
"crates/assistant_slash_command",
|
||||||
"crates/assistant_slash_commands",
|
"crates/assistant_slash_commands",
|
||||||
@@ -119,6 +118,7 @@ members = [
|
|||||||
"crates/rope",
|
"crates/rope",
|
||||||
"crates/rpc",
|
"crates/rpc",
|
||||||
"crates/schema_generator",
|
"crates/schema_generator",
|
||||||
|
"crates/scripting_tool",
|
||||||
"crates/search",
|
"crates/search",
|
||||||
"crates/semantic_index",
|
"crates/semantic_index",
|
||||||
"crates/semantic_version",
|
"crates/semantic_version",
|
||||||
@@ -178,7 +178,6 @@ members = [
|
|||||||
"extensions/ruff",
|
"extensions/ruff",
|
||||||
"extensions/slash-commands-example",
|
"extensions/slash-commands-example",
|
||||||
"extensions/snippets",
|
"extensions/snippets",
|
||||||
"extensions/terraform",
|
|
||||||
"extensions/test-extension",
|
"extensions/test-extension",
|
||||||
"extensions/toml",
|
"extensions/toml",
|
||||||
"extensions/uiua",
|
"extensions/uiua",
|
||||||
@@ -318,7 +317,7 @@ reqwest_client = { path = "crates/reqwest_client" }
|
|||||||
rich_text = { path = "crates/rich_text" }
|
rich_text = { path = "crates/rich_text" }
|
||||||
rope = { path = "crates/rope" }
|
rope = { path = "crates/rope" }
|
||||||
rpc = { path = "crates/rpc" }
|
rpc = { path = "crates/rpc" }
|
||||||
assistant_scripting = { path = "crates/assistant_scripting" }
|
scripting_tool = { path = "crates/scripting_tool" }
|
||||||
search = { path = "crates/search" }
|
search = { path = "crates/search" }
|
||||||
semantic_index = { path = "crates/semantic_index" }
|
semantic_index = { path = "crates/semantic_index" }
|
||||||
semantic_version = { path = "crates/semantic_version" }
|
semantic_version = { path = "crates/semantic_version" }
|
||||||
@@ -597,12 +596,12 @@ features = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies.windows]
|
[workspace.dependencies.windows]
|
||||||
version = "0.58"
|
version = "0.60"
|
||||||
features = [
|
features = [
|
||||||
"implement",
|
|
||||||
"Foundation_Collections",
|
"Foundation_Collections",
|
||||||
"Foundation_Numerics",
|
"Foundation_Numerics",
|
||||||
"Storage",
|
"Storage_Search",
|
||||||
|
"Storage_Streams",
|
||||||
"System_Threading",
|
"System_Threading",
|
||||||
"UI_StartScreen",
|
"UI_StartScreen",
|
||||||
"UI_ViewManagement",
|
"UI_ViewManagement",
|
||||||
@@ -623,9 +622,11 @@ features = [
|
|||||||
"Win32_System_Com_StructuredStorage",
|
"Win32_System_Com_StructuredStorage",
|
||||||
"Win32_System_Console",
|
"Win32_System_Console",
|
||||||
"Win32_System_DataExchange",
|
"Win32_System_DataExchange",
|
||||||
|
"Win32_System_IO",
|
||||||
"Win32_System_LibraryLoader",
|
"Win32_System_LibraryLoader",
|
||||||
"Win32_System_Memory",
|
"Win32_System_Memory",
|
||||||
"Win32_System_Ole",
|
"Win32_System_Ole",
|
||||||
|
"Win32_System_Pipes",
|
||||||
"Win32_System_SystemInformation",
|
"Win32_System_SystemInformation",
|
||||||
"Win32_System_SystemServices",
|
"Win32_System_SystemServices",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
@@ -751,5 +752,9 @@ new_ret_no_self = { level = "allow" }
|
|||||||
should_implement_trait = { level = "allow" }
|
should_implement_trait = { level = "allow" }
|
||||||
let_underscore_future = "allow"
|
let_underscore_future = "allow"
|
||||||
|
|
||||||
|
# in Rust it can be very tedious to reduce argument count without
|
||||||
|
# running afoul of the borrow checker.
|
||||||
|
too_many_arguments = "allow"
|
||||||
|
|
||||||
[workspace.metadata.cargo-machete]
|
[workspace.metadata.cargo-machete]
|
||||||
ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme"]
|
ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme"]
|
||||||
|
|||||||
@@ -393,6 +393,7 @@
|
|||||||
"alt-shift-open": "projects::OpenRemote",
|
"alt-shift-open": "projects::OpenRemote",
|
||||||
"alt-ctrl-shift-o": "projects::OpenRemote",
|
"alt-ctrl-shift-o": "projects::OpenRemote",
|
||||||
"alt-ctrl-shift-b": "branches::OpenRecent",
|
"alt-ctrl-shift-b": "branches::OpenRecent",
|
||||||
|
"alt-shift-enter": "toast::RunAction",
|
||||||
"ctrl-~": "workspace::NewTerminal",
|
"ctrl-~": "workspace::NewTerminal",
|
||||||
"save": "workspace::Save",
|
"save": "workspace::Save",
|
||||||
"ctrl-s": "workspace::Save",
|
"ctrl-s": "workspace::Save",
|
||||||
@@ -731,28 +732,48 @@
|
|||||||
"up": "menu::SelectPrevious",
|
"up": "menu::SelectPrevious",
|
||||||
"down": "menu::SelectNext",
|
"down": "menu::SelectNext",
|
||||||
"enter": "menu::Confirm",
|
"enter": "menu::Confirm",
|
||||||
|
"alt-y": "git::StageFile",
|
||||||
|
"alt-shift-y": "git::UnstageFile",
|
||||||
|
"ctrl-alt-y": "git::ToggleStaged",
|
||||||
"space": "git::ToggleStaged",
|
"space": "git::ToggleStaged",
|
||||||
"ctrl-space": "git::StageAll",
|
|
||||||
"ctrl-shift-space": "git::UnstageAll",
|
|
||||||
"tab": "git_panel::FocusEditor",
|
"tab": "git_panel::FocusEditor",
|
||||||
"shift-tab": "git_panel::FocusEditor",
|
"shift-tab": "git_panel::FocusEditor",
|
||||||
"escape": "git_panel::ToggleFocus",
|
"escape": "git_panel::ToggleFocus",
|
||||||
"ctrl-enter": "git::Commit",
|
"ctrl-enter": "git::Commit",
|
||||||
"alt-enter": "menu::SecondaryConfirm"
|
"alt-enter": "menu::SecondaryConfirm",
|
||||||
|
"backspace": "git::RestoreFile"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "GitCommit > Editor",
|
"context": "GitCommit > Editor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
"escape": "menu::Cancel",
|
||||||
"enter": "editor::Newline",
|
"enter": "editor::Newline",
|
||||||
"ctrl-enter": "git::Commit",
|
"ctrl-enter": "git::Commit",
|
||||||
"alt-l": "git::GenerateCommitMessage"
|
"alt-l": "git::GenerateCommitMessage"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "GitPanel",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-g ctrl-g": "git::Fetch",
|
||||||
|
"ctrl-g up": "git::Push",
|
||||||
|
"ctrl-g down": "git::Pull",
|
||||||
|
"ctrl-g shift-up": "git::ForcePush",
|
||||||
|
"ctrl-g d": "git::Diff",
|
||||||
|
"ctrl-g backspace": "git::RestoreTrackedFiles",
|
||||||
|
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
|
||||||
|
"ctrl-space": "git::StageAll",
|
||||||
|
"ctrl-shift-space": "git::UnstageAll"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "GitDiff > Editor",
|
"context": "GitDiff > Editor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-enter": "git::Commit"
|
"ctrl-enter": "git::Commit",
|
||||||
|
"ctrl-space": "git::StageAll",
|
||||||
|
"ctrl-shift-space": "git::UnstageAll"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -767,6 +788,7 @@
|
|||||||
"escape": "git_panel::FocusChanges",
|
"escape": "git_panel::FocusChanges",
|
||||||
"tab": "git_panel::FocusChanges",
|
"tab": "git_panel::FocusChanges",
|
||||||
"shift-tab": "git_panel::FocusChanges",
|
"shift-tab": "git_panel::FocusChanges",
|
||||||
|
"enter": "editor::Newline",
|
||||||
"ctrl-enter": "git::Commit",
|
"ctrl-enter": "git::Commit",
|
||||||
"alt-up": "git_panel::FocusChanges",
|
"alt-up": "git_panel::FocusChanges",
|
||||||
"alt-l": "git::GenerateCommitMessage"
|
"alt-l": "git::GenerateCommitMessage"
|
||||||
@@ -840,21 +862,22 @@
|
|||||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||||
// Overrides for conflicting keybindings
|
// Overrides for conflicting keybindings
|
||||||
|
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
|
||||||
|
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||||
|
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
|
||||||
|
"ctrl-o": ["terminal::SendKeystroke", "ctrl-o"],
|
||||||
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
||||||
"ctrl-shift-a": "editor::SelectAll",
|
"ctrl-shift-a": "editor::SelectAll",
|
||||||
"find": "buffer_search::Deploy",
|
"find": "buffer_search::Deploy",
|
||||||
"ctrl-shift-f": "buffer_search::Deploy",
|
"ctrl-shift-f": "buffer_search::Deploy",
|
||||||
"ctrl-shift-l": "terminal::Clear",
|
"ctrl-shift-l": "terminal::Clear",
|
||||||
"ctrl-shift-w": "pane::CloseActiveItem",
|
"ctrl-shift-w": "pane::CloseActiveItem",
|
||||||
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
|
|
||||||
"up": ["terminal::SendKeystroke", "up"],
|
"up": ["terminal::SendKeystroke", "up"],
|
||||||
"pageup": ["terminal::SendKeystroke", "pageup"],
|
"pageup": ["terminal::SendKeystroke", "pageup"],
|
||||||
"down": ["terminal::SendKeystroke", "down"],
|
"down": ["terminal::SendKeystroke", "down"],
|
||||||
"pagedown": ["terminal::SendKeystroke", "pagedown"],
|
"pagedown": ["terminal::SendKeystroke", "pagedown"],
|
||||||
"escape": ["terminal::SendKeystroke", "escape"],
|
"escape": ["terminal::SendKeystroke", "escape"],
|
||||||
"enter": ["terminal::SendKeystroke", "enter"],
|
"enter": ["terminal::SendKeystroke", "enter"],
|
||||||
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
|
|
||||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
|
||||||
"shift-pageup": "terminal::ScrollPageUp",
|
"shift-pageup": "terminal::ScrollPageUp",
|
||||||
"shift-pagedown": "terminal::ScrollPageDown",
|
"shift-pagedown": "terminal::ScrollPageDown",
|
||||||
"shift-up": "terminal::ScrollLineUp",
|
"shift-up": "terminal::ScrollLineUp",
|
||||||
|
|||||||
@@ -31,13 +31,13 @@
|
|||||||
"enter": "menu::Confirm",
|
"enter": "menu::Confirm",
|
||||||
"ctrl-enter": "menu::SecondaryConfirm",
|
"ctrl-enter": "menu::SecondaryConfirm",
|
||||||
"cmd-enter": "menu::SecondaryConfirm",
|
"cmd-enter": "menu::SecondaryConfirm",
|
||||||
|
"cmd-escape": "menu::Cancel",
|
||||||
"ctrl-escape": "menu::Cancel",
|
"ctrl-escape": "menu::Cancel",
|
||||||
"ctrl-c": "menu::Cancel",
|
"ctrl-c": "menu::Cancel",
|
||||||
"escape": "menu::Cancel",
|
"escape": "menu::Cancel",
|
||||||
"alt-shift-enter": "menu::Restart",
|
"alt-shift-enter": "menu::Restart",
|
||||||
"cmd-shift-w": "workspace::CloseWindow",
|
"cmd-shift-w": "workspace::CloseWindow",
|
||||||
"shift-escape": "workspace::ToggleZoom",
|
"shift-escape": "workspace::ToggleZoom",
|
||||||
"cmd-escape": "menu::Cancel",
|
|
||||||
"cmd-o": "workspace::Open",
|
"cmd-o": "workspace::Open",
|
||||||
"cmd-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
"cmd-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||||
"cmd-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
"cmd-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||||
@@ -514,6 +514,7 @@
|
|||||||
"ctrl-~": "workspace::NewTerminal",
|
"ctrl-~": "workspace::NewTerminal",
|
||||||
"cmd-s": "workspace::Save",
|
"cmd-s": "workspace::Save",
|
||||||
"cmd-k s": "workspace::SaveWithoutFormat",
|
"cmd-k s": "workspace::SaveWithoutFormat",
|
||||||
|
"alt-shift-enter": "toast::RunAction",
|
||||||
"cmd-shift-s": "workspace::SaveAs",
|
"cmd-shift-s": "workspace::SaveAs",
|
||||||
"cmd-shift-n": "workspace::NewWindow",
|
"cmd-shift-n": "workspace::NewWindow",
|
||||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||||
@@ -763,28 +764,25 @@
|
|||||||
"cmd-up": "menu::SelectFirst",
|
"cmd-up": "menu::SelectFirst",
|
||||||
"cmd-down": "menu::SelectLast",
|
"cmd-down": "menu::SelectLast",
|
||||||
"enter": "menu::Confirm",
|
"enter": "menu::Confirm",
|
||||||
|
"cmd-alt-y": "git::ToggleStaged",
|
||||||
"space": "git::ToggleStaged",
|
"space": "git::ToggleStaged",
|
||||||
"cmd-shift-space": "git::StageAll",
|
"cmd-y": "git::StageFile",
|
||||||
"ctrl-shift-space": "git::UnstageAll",
|
"cmd-shift-y": "git::UnstageFile",
|
||||||
"alt-down": "git_panel::FocusEditor",
|
"alt-down": "git_panel::FocusEditor",
|
||||||
"tab": "git_panel::FocusEditor",
|
"tab": "git_panel::FocusEditor",
|
||||||
"shift-tab": "git_panel::FocusEditor",
|
"shift-tab": "git_panel::FocusEditor",
|
||||||
"escape": "git_panel::ToggleFocus",
|
"escape": "git_panel::ToggleFocus",
|
||||||
"cmd-enter": "git::Commit"
|
"cmd-enter": "git::Commit",
|
||||||
|
"backspace": "git::RestoreFile"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "GitDiff > Editor",
|
"context": "GitDiff > Editor",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-enter": "git::Commit"
|
"cmd-enter": "git::Commit",
|
||||||
}
|
"cmd-ctrl-y": "git::StageAll",
|
||||||
},
|
"cmd-ctrl-shift-y": "git::UnstageAll"
|
||||||
{
|
|
||||||
"context": "AskPass > Editor",
|
|
||||||
"use_key_equivalents": true,
|
|
||||||
"bindings": {
|
|
||||||
"enter": "menu::Confirm"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -800,11 +798,27 @@
|
|||||||
"alt-tab": "git::GenerateCommitMessage"
|
"alt-tab": "git::GenerateCommitMessage"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "GitPanel",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-g ctrl-g": "git::Fetch",
|
||||||
|
"ctrl-g up": "git::Push",
|
||||||
|
"ctrl-g down": "git::Pull",
|
||||||
|
"ctrl-g shift-up": "git::ForcePush",
|
||||||
|
"ctrl-g d": "git::Diff",
|
||||||
|
"ctrl-g backspace": "git::RestoreTrackedFiles",
|
||||||
|
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
|
||||||
|
"cmd-ctrl-y": "git::StageAll",
|
||||||
|
"cmd-ctrl-shift-y": "git::UnstageAll"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "GitCommit > Editor",
|
"context": "GitCommit > Editor",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"enter": "editor::Newline",
|
"enter": "editor::Newline",
|
||||||
|
"escape": "menu::Cancel",
|
||||||
"cmd-enter": "git::Commit",
|
"cmd-enter": "git::Commit",
|
||||||
"alt-tab": "git::GenerateCommitMessage"
|
"alt-tab": "git::GenerateCommitMessage"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1300,8 +1300,7 @@
|
|||||||
},
|
},
|
||||||
// Settings for auto-closing of JSX tags.
|
// Settings for auto-closing of JSX tags.
|
||||||
"jsx_tag_auto_close": {
|
"jsx_tag_auto_close": {
|
||||||
// // Whether to auto-close JSX tags.
|
"enabled": true
|
||||||
// "enabled": true
|
|
||||||
},
|
},
|
||||||
// LSP Specific settings.
|
// LSP Specific settings.
|
||||||
"lsp": {
|
"lsp": {
|
||||||
|
|||||||
@@ -383,6 +383,11 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
|
"variable.special": {
|
||||||
|
"color": "#83a598ff",
|
||||||
|
"font_style": null,
|
||||||
|
"font_weight": null
|
||||||
|
},
|
||||||
"variant": {
|
"variant": {
|
||||||
"color": "#83a598ff",
|
"color": "#83a598ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -771,6 +776,11 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
|
"variable.special": {
|
||||||
|
"color": "#83a598ff",
|
||||||
|
"font_style": null,
|
||||||
|
"font_weight": null
|
||||||
|
},
|
||||||
"variant": {
|
"variant": {
|
||||||
"color": "#83a598ff",
|
"color": "#83a598ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -1159,6 +1169,11 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
|
"variable.special": {
|
||||||
|
"color": "#83a598ff",
|
||||||
|
"font_style": null,
|
||||||
|
"font_weight": null
|
||||||
|
},
|
||||||
"variant": {
|
"variant": {
|
||||||
"color": "#83a598ff",
|
"color": "#83a598ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -1547,6 +1562,11 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
|
"variable.special": {
|
||||||
|
"color": "#066578ff",
|
||||||
|
"font_style": null,
|
||||||
|
"font_weight": null
|
||||||
|
},
|
||||||
"variant": {
|
"variant": {
|
||||||
"color": "#0b6678ff",
|
"color": "#0b6678ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -1935,6 +1955,11 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
|
"variable.special": {
|
||||||
|
"color": "#066578ff",
|
||||||
|
"font_style": null,
|
||||||
|
"font_weight": null
|
||||||
|
},
|
||||||
"variant": {
|
"variant": {
|
||||||
"color": "#0b6678ff",
|
"color": "#0b6678ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -2323,6 +2348,11 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
|
"variable.special": {
|
||||||
|
"color": "#066578ff",
|
||||||
|
"font_style": null,
|
||||||
|
"font_weight": null
|
||||||
|
},
|
||||||
"variant": {
|
"variant": {
|
||||||
"color": "#0b6678ff",
|
"color": "#0b6678ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
|
|||||||
@@ -386,7 +386,6 @@ impl InlineAssistant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn suggest_assist(
|
pub fn suggest_assist(
|
||||||
&mut self,
|
&mut self,
|
||||||
editor: &Entity<Editor>,
|
editor: &Entity<Editor>,
|
||||||
@@ -1674,7 +1673,6 @@ impl Focusable for PromptEditor {
|
|||||||
impl PromptEditor {
|
impl PromptEditor {
|
||||||
const MAX_LINES: u8 = 8;
|
const MAX_LINES: u8 = 8;
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn new(
|
fn new(
|
||||||
id: InlineAssistId,
|
id: InlineAssistId,
|
||||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||||
@@ -2333,7 +2331,6 @@ struct InlineAssist {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InlineAssist {
|
impl InlineAssist {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn new(
|
fn new(
|
||||||
assist_id: InlineAssistId,
|
assist_id: InlineAssistId,
|
||||||
group_id: InlineAssistGroupId,
|
group_id: InlineAssistGroupId,
|
||||||
|
|||||||
@@ -702,7 +702,6 @@ impl Focusable for PromptEditor {
|
|||||||
impl PromptEditor {
|
impl PromptEditor {
|
||||||
const MAX_LINES: u8 = 8;
|
const MAX_LINES: u8 = 8;
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn new(
|
fn new(
|
||||||
id: TerminalInlineAssistId,
|
id: TerminalInlineAssistId,
|
||||||
prompt_history: VecDeque<String>,
|
prompt_history: VecDeque<String>,
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ test-support = [
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
assistant_context_editor.workspace = true
|
assistant_context_editor.workspace = true
|
||||||
assistant_scripting.workspace = true
|
|
||||||
assistant_settings.workspace = true
|
assistant_settings.workspace = true
|
||||||
assistant_slash_command.workspace = true
|
assistant_slash_command.workspace = true
|
||||||
assistant_tool.workspace = true
|
assistant_tool.workspace = true
|
||||||
@@ -60,11 +59,11 @@ prompt_library.workspace = true
|
|||||||
prompt_store.workspace = true
|
prompt_store.workspace = true
|
||||||
proto.workspace = true
|
proto.workspace = true
|
||||||
rope.workspace = true
|
rope.workspace = true
|
||||||
|
scripting_tool.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
rich_text.workspace = true
|
|
||||||
streaming_diff.workspace = true
|
streaming_diff.workspace = true
|
||||||
telemetry_events.workspace = true
|
telemetry_events.workspace = true
|
||||||
terminal.workspace = true
|
terminal.workspace = true
|
||||||
|
|||||||
@@ -1,32 +1,27 @@
|
|||||||
use assistant_scripting::{ScriptId, ScriptState};
|
use std::sync::Arc;
|
||||||
use collections::{HashMap, HashSet};
|
|
||||||
|
use collections::HashMap;
|
||||||
use editor::{Editor, MultiBuffer};
|
use editor::{Editor, MultiBuffer};
|
||||||
use futures::FutureExt;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
||||||
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
|
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
|
||||||
Task, TextStyleRefinement, UnderlineStyle, WeakEntity,
|
Task, TextStyleRefinement, UnderlineStyle,
|
||||||
};
|
};
|
||||||
use language::{Buffer, LanguageRegistry};
|
use language::{Buffer, LanguageRegistry};
|
||||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||||
use markdown::{Markdown, MarkdownStyle};
|
use markdown::{Markdown, MarkdownStyle};
|
||||||
|
use scripting_tool::{ScriptingTool, ScriptingToolInput};
|
||||||
use settings::Settings as _;
|
use settings::Settings as _;
|
||||||
use std::ops::Range;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{prelude::*, Disclosure, KeyBinding};
|
use ui::{prelude::*, Disclosure, KeyBinding};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
|
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
|
||||||
use crate::thread_store::ThreadStore;
|
use crate::thread_store::ThreadStore;
|
||||||
use crate::tool_use::{ToolUse, ToolUseStatus};
|
use crate::tool_use::{ToolUse, ToolUseStatus};
|
||||||
use crate::ui::ContextPill;
|
use crate::ui::ContextPill;
|
||||||
use gpui::{HighlightStyle, StyledText};
|
|
||||||
use rich_text::{self, Highlight};
|
|
||||||
|
|
||||||
pub struct ActiveThread {
|
pub struct ActiveThread {
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
thread: Entity<Thread>,
|
thread: Entity<Thread>,
|
||||||
@@ -34,9 +29,9 @@ pub struct ActiveThread {
|
|||||||
messages: Vec<MessageId>,
|
messages: Vec<MessageId>,
|
||||||
list_state: ListState,
|
list_state: ListState,
|
||||||
rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
|
rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
|
||||||
|
rendered_scripting_tool_uses: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
|
||||||
editing_message: Option<(MessageId, EditMessageState)>,
|
editing_message: Option<(MessageId, EditMessageState)>,
|
||||||
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
|
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
|
||||||
expanded_scripts: HashSet<ScriptId>,
|
|
||||||
last_error: Option<ThreadError>,
|
last_error: Option<ThreadError>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
@@ -47,7 +42,6 @@ struct EditMessageState {
|
|||||||
|
|
||||||
impl ActiveThread {
|
impl ActiveThread {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
thread: Entity<Thread>,
|
thread: Entity<Thread>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
@@ -60,15 +54,14 @@ impl ActiveThread {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
workspace,
|
|
||||||
language_registry,
|
language_registry,
|
||||||
thread_store,
|
thread_store,
|
||||||
thread: thread.clone(),
|
thread: thread.clone(),
|
||||||
save_thread_task: None,
|
save_thread_task: None,
|
||||||
messages: Vec::new(),
|
messages: Vec::new(),
|
||||||
rendered_messages_by_id: HashMap::default(),
|
rendered_messages_by_id: HashMap::default(),
|
||||||
|
rendered_scripting_tool_uses: HashMap::default(),
|
||||||
expanded_tool_uses: HashMap::default(),
|
expanded_tool_uses: HashMap::default(),
|
||||||
expanded_scripts: HashSet::default(),
|
|
||||||
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
|
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
|
||||||
let this = cx.entity().downgrade();
|
let this = cx.entity().downgrade();
|
||||||
move |ix, window: &mut Window, cx: &mut App| {
|
move |ix, window: &mut Window, cx: &mut App| {
|
||||||
@@ -83,6 +76,16 @@ impl ActiveThread {
|
|||||||
|
|
||||||
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
|
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
|
||||||
this.push_message(&message.id, message.text.clone(), window, cx);
|
this.push_message(&message.id, message.text.clone(), window, cx);
|
||||||
|
|
||||||
|
for tool_use in thread.read(cx).scripting_tool_uses_for_message(message.id) {
|
||||||
|
this.render_scripting_tool_use_markdown(
|
||||||
|
tool_use.id.clone(),
|
||||||
|
tool_use.name.as_ref(),
|
||||||
|
tool_use.input.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this
|
this
|
||||||
@@ -249,6 +252,32 @@ impl ActiveThread {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Renders the input of a scripting tool use to Markdown.
|
||||||
|
///
|
||||||
|
/// Does nothing if the tool use does not correspond to the scripting tool.
|
||||||
|
fn render_scripting_tool_use_markdown(
|
||||||
|
&mut self,
|
||||||
|
tool_use_id: LanguageModelToolUseId,
|
||||||
|
tool_name: &str,
|
||||||
|
tool_input: serde_json::Value,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if tool_name != ScriptingTool::NAME {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lua_script = serde_json::from_value::<ScriptingToolInput>(tool_input)
|
||||||
|
.map(|input| input.lua_script)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let lua_script =
|
||||||
|
self.render_markdown(format!("```lua\n{lua_script}\n```").into(), window, cx);
|
||||||
|
|
||||||
|
self.rendered_scripting_tool_uses
|
||||||
|
.insert(tool_use_id, lua_script);
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_thread_event(
|
fn handle_thread_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
_thread: &Entity<Thread>,
|
_thread: &Entity<Thread>,
|
||||||
@@ -306,7 +335,19 @@ impl ActiveThread {
|
|||||||
thread.use_pending_tools(cx);
|
thread.use_pending_tools(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ThreadEvent::ToolFinished { .. } => {
|
ThreadEvent::ToolFinished {
|
||||||
|
pending_tool_use, ..
|
||||||
|
} => {
|
||||||
|
if let Some(tool_use) = pending_tool_use {
|
||||||
|
self.render_scripting_tool_use_markdown(
|
||||||
|
tool_use.id.clone(),
|
||||||
|
tool_use.name.as_ref(),
|
||||||
|
tool_use.input.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if self.thread.read(cx).all_tools_finished() {
|
if self.thread.read(cx).all_tools_finished() {
|
||||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||||
if let Some(model) = model_registry.active_model() {
|
if let Some(model) = model_registry.active_model() {
|
||||||
@@ -316,14 +357,6 @@ impl ActiveThread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ThreadEvent::ScriptFinished => {
|
|
||||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
|
||||||
if let Some(model) = model_registry.active_model() {
|
|
||||||
self.thread.update(cx, |thread, cx| {
|
|
||||||
thread.send_to_model(model, RequestKind::Chat, false, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +447,7 @@ impl ActiveThread {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.thread.update(cx, |thread, cx| {
|
self.thread.update(cx, |thread, cx| {
|
||||||
thread.send_to_model(model, RequestKind::Chat, false, cx)
|
thread.send_to_model(model, RequestKind::Chat, cx)
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -467,11 +500,12 @@ impl ActiveThread {
|
|||||||
|
|
||||||
let context = thread.context_for_message(message_id);
|
let context = thread.context_for_message(message_id);
|
||||||
let tool_uses = thread.tool_uses_for_message(message_id);
|
let tool_uses = thread.tool_uses_for_message(message_id);
|
||||||
|
let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id);
|
||||||
|
|
||||||
// Don't render user messages that are just there for returning tool results.
|
// Don't render user messages that are just there for returning tool results.
|
||||||
if message.role == Role::User
|
if message.role == Role::User
|
||||||
&& (thread.message_has_tool_results(message_id)
|
&& (thread.message_has_tool_results(message_id)
|
||||||
|| thread.message_has_script_output(message_id))
|
|| thread.message_has_scripting_tool_results(message_id))
|
||||||
{
|
{
|
||||||
return Empty.into_any();
|
return Empty.into_any();
|
||||||
}
|
}
|
||||||
@@ -621,18 +655,23 @@ impl ActiveThread {
|
|||||||
Role::Assistant => div()
|
Role::Assistant => div()
|
||||||
.id(("message-container", ix))
|
.id(("message-container", ix))
|
||||||
.child(message_content)
|
.child(message_content)
|
||||||
.children(self.render_script(message_id, cx))
|
|
||||||
.map(|parent| {
|
.map(|parent| {
|
||||||
if tool_uses.is_empty() {
|
if tool_uses.is_empty() && scripting_tool_uses.is_empty() {
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.child(
|
parent.child(
|
||||||
v_flex().children(
|
v_flex()
|
||||||
tool_uses
|
.children(
|
||||||
.into_iter()
|
tool_uses
|
||||||
.map(|tool_use| self.render_tool_use(tool_use, cx)),
|
.into_iter()
|
||||||
),
|
.map(|tool_use| self.render_tool_use(tool_use, cx)),
|
||||||
|
)
|
||||||
|
.children(
|
||||||
|
scripting_tool_uses
|
||||||
|
.into_iter()
|
||||||
|
.map(|tool_use| self.render_scripting_tool_use(tool_use, cx)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
|
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
|
||||||
@@ -666,8 +705,13 @@ impl ActiveThread {
|
|||||||
.pl_1()
|
.pl_1()
|
||||||
.pr_2()
|
.pr_2()
|
||||||
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
|
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
|
||||||
.when(is_open, |element| element.border_b_1().rounded_t(px(6.)))
|
.map(|element| {
|
||||||
.when(!is_open, |element| element.rounded_md())
|
if is_open {
|
||||||
|
element.border_b_1().rounded_t(px(6.))
|
||||||
|
} else {
|
||||||
|
element.rounded_md()
|
||||||
|
}
|
||||||
|
})
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -742,189 +786,117 @@ impl ActiveThread {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_script(&self, message_id: MessageId, cx: &mut Context<Self>) -> Option<AnyElement> {
|
fn render_scripting_tool_use(
|
||||||
let script = self.thread.read(cx).script_for_message(message_id, cx)?;
|
&self,
|
||||||
|
tool_use: ToolUse,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let is_open = self
|
||||||
|
.expanded_tool_uses
|
||||||
|
.get(&tool_use.id)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let is_open = self.expanded_scripts.contains(&script.id);
|
div().px_2p5().child(
|
||||||
let colors = cx.theme().colors();
|
|
||||||
|
|
||||||
let element = div().px_2p5().child(
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.rounded_lg()
|
.rounded_lg()
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(colors.border)
|
.border_color(cx.theme().colors().border)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.pl_1()
|
.pl_1()
|
||||||
.pr_2()
|
.pr_2()
|
||||||
.bg(colors.editor_foreground.opacity(0.02))
|
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
|
||||||
.when(is_open, |element| element.border_b_1().rounded_t(px(6.)))
|
.map(|element| {
|
||||||
.when(!is_open, |element| element.rounded_md())
|
if is_open {
|
||||||
.border_color(colors.border)
|
element.border_b_1().rounded_t(px(6.))
|
||||||
|
} else {
|
||||||
|
element.rounded_md()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Disclosure::new("script-disclosure", is_open).on_click(
|
.child(Disclosure::new("tool-use-disclosure", is_open).on_click(
|
||||||
cx.listener({
|
cx.listener({
|
||||||
let script_id = script.id;
|
let tool_use_id = tool_use.id.clone();
|
||||||
move |this, _event, _window, _cx| {
|
move |this, _event, _window, _cx| {
|
||||||
if this.expanded_scripts.contains(&script_id) {
|
let is_open = this
|
||||||
this.expanded_scripts.remove(&script_id);
|
.expanded_tool_uses
|
||||||
} else {
|
.entry(tool_use_id.clone())
|
||||||
this.expanded_scripts.insert(script_id);
|
.or_insert(false);
|
||||||
}
|
|
||||||
|
*is_open = !*is_open;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
// TODO: Generate script description
|
.child(Label::new(tool_use.name)),
|
||||||
.child(Label::new("Script")),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
Label::new(match tool_use.status {
|
||||||
.gap_1()
|
ToolUseStatus::Pending => "Pending",
|
||||||
.child(
|
ToolUseStatus::Running => "Running",
|
||||||
Label::new(match script.state {
|
ToolUseStatus::Finished(_) => "Finished",
|
||||||
ScriptState::Generating => "Generating",
|
ToolUseStatus::Error(_) => "Error",
|
||||||
ScriptState::Running { .. } => "Running",
|
})
|
||||||
ScriptState::Succeeded { .. } => "Finished",
|
.size(LabelSize::XSmall)
|
||||||
ScriptState::Failed { .. } => "Error",
|
.buffer_font(cx),
|
||||||
})
|
|
||||||
.size(LabelSize::XSmall)
|
|
||||||
.buffer_font(cx),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
IconButton::new("view-source", IconName::Eye)
|
|
||||||
.icon_color(Color::Muted)
|
|
||||||
.disabled(matches!(script.state, ScriptState::Generating))
|
|
||||||
.on_click(cx.listener({
|
|
||||||
let source = script.source.clone();
|
|
||||||
move |this, _event, window, cx| {
|
|
||||||
this.open_script_source(source.clone(), window, cx);
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.when(is_open, |parent| {
|
.map(|parent| {
|
||||||
let stdout = script.stdout_snapshot();
|
if !is_open {
|
||||||
let error = script.error();
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
let lua_language =
|
let lua_script_markdown =
|
||||||
async { self.language_registry.language_for_name("Lua").await.ok() }
|
self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
|
||||||
.now_or_never()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
let source_display = if let Some(lua_language) = &lua_language {
|
|
||||||
let mut highlights = Vec::new();
|
|
||||||
let mut buf = String::new();
|
|
||||||
|
|
||||||
rich_text::render_code(
|
|
||||||
&mut buf,
|
|
||||||
&mut highlights,
|
|
||||||
&script.source,
|
|
||||||
lua_language,
|
|
||||||
);
|
|
||||||
|
|
||||||
let theme = cx.theme();
|
|
||||||
let gpui_highlights: Vec<(Range<usize>, HighlightStyle)> = highlights
|
|
||||||
.iter()
|
|
||||||
.map(|(range, highlight)| {
|
|
||||||
let style = match highlight {
|
|
||||||
Highlight::Code => Default::default(),
|
|
||||||
Highlight::Id(id) => {
|
|
||||||
id.style(theme.syntax()).unwrap_or_default()
|
|
||||||
}
|
|
||||||
Highlight::InlineCode(_) => Default::default(),
|
|
||||||
Highlight::Highlight(highlight) => *highlight,
|
|
||||||
_ => HighlightStyle::default(),
|
|
||||||
};
|
|
||||||
(range.clone(), style)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
StyledText::new(buf)
|
|
||||||
.with_highlights(gpui_highlights)
|
|
||||||
.into_any_element()
|
|
||||||
} else {
|
|
||||||
Label::new(script.source.clone())
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.buffer_font(cx)
|
|
||||||
.into_any_element()
|
|
||||||
};
|
|
||||||
|
|
||||||
parent.child(
|
parent.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.p_2()
|
|
||||||
.bg(colors.editor_background)
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
v_flex()
|
||||||
.border_1()
|
.gap_0p5()
|
||||||
.border_color(colors.border)
|
.py_1()
|
||||||
.p_2()
|
.px_2p5()
|
||||||
.bg(colors.editor_foreground.opacity(0.025))
|
.border_b_1()
|
||||||
.rounded_md()
|
.border_color(cx.theme().colors().border)
|
||||||
.child(source_display),
|
.child(Label::new("Input:"))
|
||||||
|
.map(|parent| {
|
||||||
|
if let Some(markdown) = lua_script_markdown {
|
||||||
|
parent.child(markdown)
|
||||||
|
} else {
|
||||||
|
parent.child(Label::new(
|
||||||
|
"Failed to render script input to Markdown",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.child(if stdout.is_empty() && error.is_none() {
|
.map(|parent| match tool_use.status {
|
||||||
Label::new("No output yet")
|
ToolUseStatus::Finished(output) => parent.child(
|
||||||
.size(LabelSize::Small)
|
v_flex()
|
||||||
.color(Color::Muted)
|
.gap_0p5()
|
||||||
} else {
|
.py_1()
|
||||||
Label::new(stdout).size(LabelSize::Small).buffer_font(cx)
|
.px_2p5()
|
||||||
})
|
.child(Label::new("Result:"))
|
||||||
.children(script.error().map(|err| {
|
.child(Label::new(output)),
|
||||||
Label::new(err.to_string())
|
),
|
||||||
.size(LabelSize::Small)
|
ToolUseStatus::Error(err) => parent.child(
|
||||||
.color(Color::Error)
|
v_flex()
|
||||||
})),
|
.gap_0p5()
|
||||||
|
.py_1()
|
||||||
|
.px_2p5()
|
||||||
|
.child(Label::new("Error:"))
|
||||||
|
.child(Label::new(err)),
|
||||||
|
),
|
||||||
|
ToolUseStatus::Pending | ToolUseStatus::Running => parent,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
Some(element.into_any())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_script_source(
|
|
||||||
&mut self,
|
|
||||||
source: SharedString,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<'_, ActiveThread>,
|
|
||||||
) {
|
|
||||||
let language_registry = self.language_registry.clone();
|
|
||||||
let workspace = self.workspace.clone();
|
|
||||||
let source = source.clone();
|
|
||||||
|
|
||||||
cx.spawn_in(window, |_, mut cx| async move {
|
|
||||||
let lua = language_registry.language_for_name("Lua").await.log_err();
|
|
||||||
|
|
||||||
workspace.update_in(&mut cx, |workspace, window, cx| {
|
|
||||||
let project = workspace.project().clone();
|
|
||||||
|
|
||||||
let buffer = project.update(cx, |project, cx| {
|
|
||||||
project.create_local_buffer(&source.trim(), lua, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let buffer = cx.new(|cx| {
|
|
||||||
MultiBuffer::singleton(buffer, cx)
|
|
||||||
// TODO: Generate script description
|
|
||||||
.with_title("Assistant script".into())
|
|
||||||
});
|
|
||||||
|
|
||||||
let editor = cx.new(|cx| {
|
|
||||||
let mut editor =
|
|
||||||
Editor::for_multibuffer(buffer, Some(project), true, window, cx);
|
|
||||||
editor.set_read_only(true);
|
|
||||||
editor
|
|
||||||
});
|
|
||||||
|
|
||||||
workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ mod terminal_inline_assistant;
|
|||||||
mod thread;
|
mod thread;
|
||||||
mod thread_history;
|
mod thread_history;
|
||||||
mod thread_store;
|
mod thread_store;
|
||||||
|
mod tool_selector;
|
||||||
mod tool_use;
|
mod tool_use;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
|
|||||||
@@ -168,7 +168,6 @@ impl AssistantPanel {
|
|||||||
|
|
||||||
let thread = cx.new(|cx| {
|
let thread = cx.new(|cx| {
|
||||||
ActiveThread::new(
|
ActiveThread::new(
|
||||||
workspace.clone(),
|
|
||||||
thread.clone(),
|
thread.clone(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
language_registry.clone(),
|
language_registry.clone(),
|
||||||
@@ -242,7 +241,6 @@ impl AssistantPanel {
|
|||||||
self.active_view = ActiveView::Thread;
|
self.active_view = ActiveView::Thread;
|
||||||
self.thread = cx.new(|cx| {
|
self.thread = cx.new(|cx| {
|
||||||
ActiveThread::new(
|
ActiveThread::new(
|
||||||
self.workspace.clone(),
|
|
||||||
thread.clone(),
|
thread.clone(),
|
||||||
self.thread_store.clone(),
|
self.thread_store.clone(),
|
||||||
self.language_registry.clone(),
|
self.language_registry.clone(),
|
||||||
@@ -376,7 +374,6 @@ impl AssistantPanel {
|
|||||||
this.active_view = ActiveView::Thread;
|
this.active_view = ActiveView::Thread;
|
||||||
this.thread = cx.new(|cx| {
|
this.thread = cx.new(|cx| {
|
||||||
ActiveThread::new(
|
ActiveThread::new(
|
||||||
this.workspace.clone(),
|
|
||||||
thread.clone(),
|
thread.clone(),
|
||||||
this.thread_store.clone(),
|
this.thread_store.clone(),
|
||||||
this.language_registry.clone(),
|
this.language_registry.clone(),
|
||||||
|
|||||||
@@ -167,8 +167,8 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
|
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||||
"Enter the URL that you would like to fetch".into()
|
Some("Enter the URL that you would like to fetch".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn selected_index(&self) -> usize {
|
fn selected_index(&self) -> usize {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ use crate::{
|
|||||||
|
|
||||||
pub struct ContextStrip {
|
pub struct ContextStrip {
|
||||||
context_store: Entity<ContextStore>,
|
context_store: Entity<ContextStore>,
|
||||||
pub context_picker: Entity<ContextPicker>,
|
context_picker: Entity<ContextPicker>,
|
||||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
suggest_context_kind: SuggestContextKind,
|
suggest_context_kind: SuggestContextKind,
|
||||||
@@ -36,7 +36,6 @@ pub struct ContextStrip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ContextStrip {
|
impl ContextStrip {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
context_store: Entity<ContextStore>,
|
context_store: Entity<ContextStore>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
|||||||
@@ -480,7 +480,6 @@ impl InlineAssistant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn suggest_assist(
|
pub fn suggest_assist(
|
||||||
&mut self,
|
&mut self,
|
||||||
editor: &Entity<Editor>,
|
editor: &Entity<Editor>,
|
||||||
@@ -1451,7 +1450,6 @@ struct InlineAssistScrollLock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EditorInlineAssists {
|
impl EditorInlineAssists {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn new(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) -> Self {
|
fn new(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) -> Self {
|
||||||
let (highlight_updates_tx, mut highlight_updates_rx) = async_watch::channel(());
|
let (highlight_updates_tx, mut highlight_updates_rx) = async_watch::channel(());
|
||||||
Self {
|
Self {
|
||||||
@@ -1563,7 +1561,6 @@ pub struct InlineAssist {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InlineAssist {
|
impl InlineAssist {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn new(
|
fn new(
|
||||||
assist_id: InlineAssistId,
|
assist_id: InlineAssistId,
|
||||||
group_id: InlineAssistGroupId,
|
group_id: InlineAssistGroupId,
|
||||||
|
|||||||
@@ -816,7 +816,6 @@ impl InlineAssistId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PromptEditor<BufferCodegen> {
|
impl PromptEditor<BufferCodegen> {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new_buffer(
|
pub fn new_buffer(
|
||||||
id: InlineAssistId,
|
id: InlineAssistId,
|
||||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||||
@@ -976,7 +975,6 @@ impl TerminalInlineAssistId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PromptEditor<TerminalCodegen> {
|
impl PromptEditor<TerminalCodegen> {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new_terminal(
|
pub fn new_terminal(
|
||||||
id: TerminalInlineAssistId,
|
id: TerminalInlineAssistId,
|
||||||
prompt_history: VecDeque<String>,
|
prompt_history: VecDeque<String>,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use editor::actions::MoveUp;
|
use editor::actions::MoveUp;
|
||||||
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
|
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
|
||||||
|
use file_icons::FileIcons;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
|
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
|
||||||
@@ -15,7 +16,7 @@ use std::time::Duration;
|
|||||||
use text::Bias;
|
use text::Bias;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Switch,
|
prelude::*, ButtonLike, Disclosure, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
};
|
};
|
||||||
use vim_mode_setting::VimModeSetting;
|
use vim_mode_setting::VimModeSetting;
|
||||||
@@ -27,6 +28,7 @@ use crate::context_store::{refresh_context_store_text, ContextStore};
|
|||||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||||
use crate::thread::{RequestKind, Thread};
|
use crate::thread::{RequestKind, Thread};
|
||||||
use crate::thread_store::ThreadStore;
|
use crate::thread_store::ThreadStore;
|
||||||
|
use crate::tool_selector::ToolSelector;
|
||||||
use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker};
|
use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker};
|
||||||
|
|
||||||
pub struct MessageEditor {
|
pub struct MessageEditor {
|
||||||
@@ -38,7 +40,8 @@ pub struct MessageEditor {
|
|||||||
inline_context_picker: Entity<ContextPicker>,
|
inline_context_picker: Entity<ContextPicker>,
|
||||||
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||||
model_selector: Entity<AssistantModelSelector>,
|
model_selector: Entity<AssistantModelSelector>,
|
||||||
use_tools: bool,
|
tool_selector: Entity<ToolSelector>,
|
||||||
|
edits_expanded: bool,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +54,7 @@ impl MessageEditor {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let tools = thread.read(cx).tools().clone();
|
||||||
let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
|
let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
|
||||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||||
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
|
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
|
||||||
@@ -116,13 +120,13 @@ impl MessageEditor {
|
|||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
use_tools: false,
|
tool_selector: cx.new(|cx| ToolSelector::new(tools, cx)),
|
||||||
|
edits_expanded: false,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {
|
fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.use_tools = !self.use_tools;
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,14 +193,13 @@ impl MessageEditor {
|
|||||||
|
|
||||||
let thread = self.thread.clone();
|
let thread = self.thread.clone();
|
||||||
let context_store = self.context_store.clone();
|
let context_store = self.context_store.clone();
|
||||||
let use_tools = self.use_tools;
|
|
||||||
cx.spawn(move |_, mut cx| async move {
|
cx.spawn(move |_, mut cx| async move {
|
||||||
refresh_task.await;
|
refresh_task.await;
|
||||||
thread
|
thread
|
||||||
.update(&mut cx, |thread, cx| {
|
.update(&mut cx, |thread, cx| {
|
||||||
let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
|
let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
|
||||||
thread.insert_user_message(user_message, context, cx);
|
thread.insert_user_message(user_message, context, cx);
|
||||||
thread.send_to_model(model, request_kind, use_tools, cx);
|
thread.send_to_model(model, request_kind, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
})
|
})
|
||||||
@@ -303,6 +306,9 @@ impl Render for MessageEditor {
|
|||||||
px(64.)
|
px(64.)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let changed_buffers = self.thread.read(cx).scripting_changed_buffers(cx);
|
||||||
|
let changed_buffers_count = changed_buffers.len();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.when(is_streaming_completion, |parent| {
|
.when(is_streaming_completion, |parent| {
|
||||||
@@ -363,6 +369,109 @@ impl Render for MessageEditor {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.when(changed_buffers_count > 0, |parent| {
|
||||||
|
parent.child(
|
||||||
|
v_flex()
|
||||||
|
.mx_2()
|
||||||
|
.bg(cx.theme().colors().element_background)
|
||||||
|
.border_1()
|
||||||
|
.border_b_0()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.rounded_t_md()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.p_2()
|
||||||
|
.child(
|
||||||
|
Disclosure::new("edits-disclosure", self.edits_expanded)
|
||||||
|
.on_click(cx.listener(|this, _ev, _window, cx| {
|
||||||
|
this.edits_expanded = !this.edits_expanded;
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Label::new("Edits")
|
||||||
|
.size(LabelSize::XSmall)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted))
|
||||||
|
.child(
|
||||||
|
Label::new(format!(
|
||||||
|
"{} {}",
|
||||||
|
changed_buffers_count,
|
||||||
|
if changed_buffers_count == 1 {
|
||||||
|
"file"
|
||||||
|
} else {
|
||||||
|
"files"
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.size(LabelSize::XSmall)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when(self.edits_expanded, |parent| {
|
||||||
|
parent.child(
|
||||||
|
v_flex().bg(cx.theme().colors().editor_background).children(
|
||||||
|
changed_buffers.enumerate().flat_map(|(index, buffer)| {
|
||||||
|
let file = buffer.read(cx).file()?;
|
||||||
|
let path = file.path();
|
||||||
|
|
||||||
|
let parent_label = path.parent().and_then(|parent| {
|
||||||
|
let parent_str = parent.to_string_lossy();
|
||||||
|
|
||||||
|
if parent_str.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
Label::new(format!(
|
||||||
|
"{}{}",
|
||||||
|
parent_str,
|
||||||
|
std::path::MAIN_SEPARATOR_STR
|
||||||
|
))
|
||||||
|
.color(Color::Muted)
|
||||||
|
.size(LabelSize::Small),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let name_label = path.file_name().map(|name| {
|
||||||
|
Label::new(name.to_string_lossy().to_string())
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
});
|
||||||
|
|
||||||
|
let file_icon = FileIcons::get_icon(&path, cx)
|
||||||
|
.map(Icon::from_path)
|
||||||
|
.unwrap_or_else(|| Icon::new(IconName::File));
|
||||||
|
|
||||||
|
let element = div()
|
||||||
|
.p_2()
|
||||||
|
.when(index + 1 < changed_buffers_count, |parent| {
|
||||||
|
parent
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.border_b_1()
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(file_icon)
|
||||||
|
.child(
|
||||||
|
// TODO: handle overflow
|
||||||
|
h_flex()
|
||||||
|
.children(parent_label)
|
||||||
|
.children(name_label),
|
||||||
|
)
|
||||||
|
// TODO: show lines changed
|
||||||
|
.child(Label::new("+").color(Color::Created))
|
||||||
|
.child(Label::new("-").color(Color::Deleted)),
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(element)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.key_context("MessageEditor")
|
.key_context("MessageEditor")
|
||||||
@@ -428,25 +537,7 @@ impl Render for MessageEditor {
|
|||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(
|
.child(h_flex().gap_2().child(self.tool_selector.clone()))
|
||||||
Switch::new("use-tools", self.use_tools.into())
|
|
||||||
.label("Tools")
|
|
||||||
.on_click(cx.listener(
|
|
||||||
|this, selection, _window, _cx| {
|
|
||||||
this.use_tools = match selection {
|
|
||||||
ToggleState::Selected => true,
|
|
||||||
ToggleState::Unselected
|
|
||||||
| ToggleState::Indeterminate => false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.key_binding(KeyBinding::for_action_in(
|
|
||||||
&ChatMode,
|
|
||||||
&focus_handle,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
h_flex().gap_1().child(self.model_selector.clone()).child(
|
h_flex().gap_1().child(self.model_selector.clone()).child(
|
||||||
ButtonLike::new("submit-message")
|
ButtonLike::new("submit-message")
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assistant_scripting::{
|
|
||||||
Script, ScriptEvent, ScriptId, ScriptSession, ScriptTagParser, SCRIPTING_PROMPT,
|
|
||||||
};
|
|
||||||
use assistant_tool::ToolWorkingSet;
|
use assistant_tool::ToolWorkingSet;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use collections::{BTreeMap, HashMap, HashSet};
|
use collections::{BTreeMap, HashMap, HashSet};
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Subscription, Task};
|
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
|
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
|
||||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||||
@@ -16,6 +13,7 @@ use language_model::{
|
|||||||
Role, StopReason,
|
Role, StopReason,
|
||||||
};
|
};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
use scripting_tool::{ScriptingSession, ScriptingTool};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use util::{post_inc, TryFutureExt as _};
|
use util::{post_inc, TryFutureExt as _};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -78,10 +76,8 @@ pub struct Thread {
|
|||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
tools: Arc<ToolWorkingSet>,
|
tools: Arc<ToolWorkingSet>,
|
||||||
tool_use: ToolUseState,
|
tool_use: ToolUseState,
|
||||||
scripts_by_assistant_message: HashMap<MessageId, ScriptId>,
|
scripting_session: Entity<ScriptingSession>,
|
||||||
script_output_messages: HashSet<MessageId>,
|
scripting_tool_use: ToolUseState,
|
||||||
script_session: Entity<ScriptSession>,
|
|
||||||
_script_session_subscription: Subscription,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Thread {
|
impl Thread {
|
||||||
@@ -90,8 +86,7 @@ impl Thread {
|
|||||||
tools: Arc<ToolWorkingSet>,
|
tools: Arc<ToolWorkingSet>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let script_session = cx.new(|cx| ScriptSession::new(project.clone(), cx));
|
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
|
||||||
let script_session_subscription = cx.subscribe(&script_session, Self::handle_script_event);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: ThreadId::new(),
|
id: ThreadId::new(),
|
||||||
@@ -107,10 +102,8 @@ impl Thread {
|
|||||||
project,
|
project,
|
||||||
tools,
|
tools,
|
||||||
tool_use: ToolUseState::new(),
|
tool_use: ToolUseState::new(),
|
||||||
scripts_by_assistant_message: HashMap::default(),
|
scripting_session,
|
||||||
script_output_messages: HashSet::default(),
|
scripting_tool_use: ToolUseState::new(),
|
||||||
script_session,
|
|
||||||
_script_session_subscription: script_session_subscription,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,9 +121,11 @@ impl Thread {
|
|||||||
.map(|message| message.id.0 + 1)
|
.map(|message| message.id.0 + 1)
|
||||||
.unwrap_or(0),
|
.unwrap_or(0),
|
||||||
);
|
);
|
||||||
let tool_use = ToolUseState::from_saved_messages(&saved.messages);
|
let tool_use =
|
||||||
let script_session = cx.new(|cx| ScriptSession::new(project.clone(), cx));
|
ToolUseState::from_saved_messages(&saved.messages, |name| name != ScriptingTool::NAME);
|
||||||
let script_session_subscription = cx.subscribe(&script_session, Self::handle_script_event);
|
let scripting_tool_use =
|
||||||
|
ToolUseState::from_saved_messages(&saved.messages, |name| name == ScriptingTool::NAME);
|
||||||
|
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
@@ -154,10 +149,8 @@ impl Thread {
|
|||||||
project,
|
project,
|
||||||
tools,
|
tools,
|
||||||
tool_use,
|
tool_use,
|
||||||
scripts_by_assistant_message: HashMap::default(),
|
scripting_session,
|
||||||
script_output_messages: HashSet::default(),
|
scripting_tool_use,
|
||||||
script_session,
|
|
||||||
_script_session_subscription: script_session_subscription,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,33 +211,51 @@ impl Thread {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
|
|
||||||
self.tool_use.pending_tool_uses()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether all of the tool uses have finished running.
|
/// Returns whether all of the tool uses have finished running.
|
||||||
pub fn all_tools_finished(&self) -> bool {
|
pub fn all_tools_finished(&self) -> bool {
|
||||||
|
let mut all_pending_tool_uses = self
|
||||||
|
.tool_use
|
||||||
|
.pending_tool_uses()
|
||||||
|
.into_iter()
|
||||||
|
.chain(self.scripting_tool_use.pending_tool_uses());
|
||||||
|
|
||||||
// If the only pending tool uses left are the ones with errors, then that means that we've finished running all
|
// If the only pending tool uses left are the ones with errors, then that means that we've finished running all
|
||||||
// of the pending tools.
|
// of the pending tools.
|
||||||
self.pending_tool_uses()
|
all_pending_tool_uses.all(|tool_use| tool_use.status.is_error())
|
||||||
.into_iter()
|
|
||||||
.all(|tool_use| tool_use.status.is_error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
|
pub fn tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
|
||||||
self.tool_use.tool_uses_for_message(id)
|
self.tool_use.tool_uses_for_message(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scripting_tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
|
||||||
|
self.scripting_tool_use.tool_uses_for_message(id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn tool_results_for_message(&self, id: MessageId) -> Vec<&LanguageModelToolResult> {
|
pub fn tool_results_for_message(&self, id: MessageId) -> Vec<&LanguageModelToolResult> {
|
||||||
self.tool_use.tool_results_for_message(id)
|
self.tool_use.tool_results_for_message(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scripting_tool_results_for_message(
|
||||||
|
&self,
|
||||||
|
id: MessageId,
|
||||||
|
) -> Vec<&LanguageModelToolResult> {
|
||||||
|
self.scripting_tool_use.tool_results_for_message(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scripting_changed_buffers<'a>(
|
||||||
|
&self,
|
||||||
|
cx: &'a App,
|
||||||
|
) -> impl ExactSizeIterator<Item = &'a Entity<language::Buffer>> {
|
||||||
|
self.scripting_session.read(cx).changed_buffers()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
|
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
|
||||||
self.tool_use.message_has_tool_results(message_id)
|
self.tool_use.message_has_tool_results(message_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn message_has_script_output(&self, message_id: MessageId) -> bool {
|
pub fn message_has_scripting_tool_results(&self, message_id: MessageId) -> bool {
|
||||||
self.script_output_messages.contains(&message_id)
|
self.scripting_tool_use.message_has_tool_results(message_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_user_message(
|
pub fn insert_user_message(
|
||||||
@@ -327,60 +338,34 @@ impl Thread {
|
|||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn script_for_message<'a>(
|
|
||||||
&'a self,
|
|
||||||
message_id: MessageId,
|
|
||||||
cx: &'a App,
|
|
||||||
) -> Option<&'a Script> {
|
|
||||||
self.scripts_by_assistant_message
|
|
||||||
.get(&message_id)
|
|
||||||
.map(|script_id| self.script_session.read(cx).get(*script_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_script_event(
|
|
||||||
&mut self,
|
|
||||||
_script_session: Entity<ScriptSession>,
|
|
||||||
event: &ScriptEvent,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
match event {
|
|
||||||
ScriptEvent::Spawned(_) => {}
|
|
||||||
ScriptEvent::Exited(script_id) => {
|
|
||||||
if let Some(output_message) = self
|
|
||||||
.script_session
|
|
||||||
.read(cx)
|
|
||||||
.get(*script_id)
|
|
||||||
.output_message_for_llm()
|
|
||||||
{
|
|
||||||
let message_id = self.insert_user_message(output_message, vec![], cx);
|
|
||||||
self.script_output_messages.insert(message_id);
|
|
||||||
cx.emit(ThreadEvent::ScriptFinished)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_to_model(
|
pub fn send_to_model(
|
||||||
&mut self,
|
&mut self,
|
||||||
model: Arc<dyn LanguageModel>,
|
model: Arc<dyn LanguageModel>,
|
||||||
request_kind: RequestKind,
|
request_kind: RequestKind,
|
||||||
use_tools: bool,
|
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let mut request = self.to_completion_request(request_kind, cx);
|
let mut request = self.to_completion_request(request_kind, cx);
|
||||||
|
request.tools = {
|
||||||
|
let mut tools = Vec::new();
|
||||||
|
|
||||||
if use_tools {
|
if self.tools.is_scripting_tool_enabled() {
|
||||||
request.tools = self
|
tools.push(LanguageModelRequestTool {
|
||||||
.tools()
|
name: ScriptingTool::NAME.into(),
|
||||||
.tools(cx)
|
description: ScriptingTool::DESCRIPTION.into(),
|
||||||
.into_iter()
|
input_schema: ScriptingTool::input_schema(),
|
||||||
.map(|tool| LanguageModelRequestTool {
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tools.extend(self.tools().enabled_tools(cx).into_iter().map(|tool| {
|
||||||
|
LanguageModelRequestTool {
|
||||||
name: tool.name(),
|
name: tool.name(),
|
||||||
description: tool.description(),
|
description: tool.description(),
|
||||||
input_schema: tool.input_schema(),
|
input_schema: tool.input_schema(),
|
||||||
})
|
}
|
||||||
.collect();
|
}));
|
||||||
}
|
|
||||||
|
tools
|
||||||
|
};
|
||||||
|
|
||||||
self.stream_completion(request, model, cx);
|
self.stream_completion(request, model, cx);
|
||||||
}
|
}
|
||||||
@@ -388,7 +373,7 @@ impl Thread {
|
|||||||
pub fn to_completion_request(
|
pub fn to_completion_request(
|
||||||
&self,
|
&self,
|
||||||
request_kind: RequestKind,
|
request_kind: RequestKind,
|
||||||
cx: &App,
|
_cx: &App,
|
||||||
) -> LanguageModelRequest {
|
) -> LanguageModelRequest {
|
||||||
let mut request = LanguageModelRequest {
|
let mut request = LanguageModelRequest {
|
||||||
messages: vec![],
|
messages: vec![],
|
||||||
@@ -397,12 +382,6 @@ impl Thread {
|
|||||||
temperature: None,
|
temperature: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
request.messages.push(LanguageModelRequestMessage {
|
|
||||||
role: Role::System,
|
|
||||||
content: vec![SCRIPTING_PROMPT.to_string().into()],
|
|
||||||
cache: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut referenced_context_ids = HashSet::default();
|
let mut referenced_context_ids = HashSet::default();
|
||||||
|
|
||||||
for message in &self.messages {
|
for message in &self.messages {
|
||||||
@@ -420,6 +399,8 @@ impl Thread {
|
|||||||
RequestKind::Chat => {
|
RequestKind::Chat => {
|
||||||
self.tool_use
|
self.tool_use
|
||||||
.attach_tool_results(message.id, &mut request_message);
|
.attach_tool_results(message.id, &mut request_message);
|
||||||
|
self.scripting_tool_use
|
||||||
|
.attach_tool_results(message.id, &mut request_message);
|
||||||
}
|
}
|
||||||
RequestKind::Summarize => {
|
RequestKind::Summarize => {
|
||||||
// We don't care about tool use during summarization.
|
// We don't care about tool use during summarization.
|
||||||
@@ -436,15 +417,8 @@ impl Thread {
|
|||||||
RequestKind::Chat => {
|
RequestKind::Chat => {
|
||||||
self.tool_use
|
self.tool_use
|
||||||
.attach_tool_uses(message.id, &mut request_message);
|
.attach_tool_uses(message.id, &mut request_message);
|
||||||
|
self.scripting_tool_use
|
||||||
if matches!(message.role, Role::Assistant) {
|
.attach_tool_uses(message.id, &mut request_message);
|
||||||
if let Some(script_id) = self.scripts_by_assistant_message.get(&message.id)
|
|
||||||
{
|
|
||||||
let script = self.script_session.read(cx).get(*script_id);
|
|
||||||
|
|
||||||
request_message.content.push(script.source_tag().into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
RequestKind::Summarize => {
|
RequestKind::Summarize => {
|
||||||
// We don't care about tool use during summarization.
|
// We don't care about tool use during summarization.
|
||||||
@@ -486,8 +460,6 @@ impl Thread {
|
|||||||
let stream_completion = async {
|
let stream_completion = async {
|
||||||
let mut events = stream.await?;
|
let mut events = stream.await?;
|
||||||
let mut stop_reason = StopReason::EndTurn;
|
let mut stop_reason = StopReason::EndTurn;
|
||||||
let mut script_tag_parser = ScriptTagParser::new();
|
|
||||||
let mut script_id = None;
|
|
||||||
|
|
||||||
while let Some(event) = events.next().await {
|
while let Some(event) = events.next().await {
|
||||||
let event = event?;
|
let event = event?;
|
||||||
@@ -502,44 +474,20 @@ impl Thread {
|
|||||||
}
|
}
|
||||||
LanguageModelCompletionEvent::Text(chunk) => {
|
LanguageModelCompletionEvent::Text(chunk) => {
|
||||||
if let Some(last_message) = thread.messages.last_mut() {
|
if let Some(last_message) = thread.messages.last_mut() {
|
||||||
let chunk = script_tag_parser.parse_chunk(&chunk);
|
if last_message.role == Role::Assistant {
|
||||||
|
last_message.text.push_str(&chunk);
|
||||||
let message_id = if last_message.role == Role::Assistant {
|
|
||||||
last_message.text.push_str(&chunk.content);
|
|
||||||
cx.emit(ThreadEvent::StreamedAssistantText(
|
cx.emit(ThreadEvent::StreamedAssistantText(
|
||||||
last_message.id,
|
last_message.id,
|
||||||
chunk.content,
|
chunk,
|
||||||
));
|
));
|
||||||
last_message.id
|
|
||||||
} else {
|
} else {
|
||||||
// If we won't have an Assistant message yet, assume this chunk marks the beginning
|
// If we won't have an Assistant message yet, assume this chunk marks the beginning
|
||||||
// of a new Assistant response.
|
// of a new Assistant response.
|
||||||
//
|
//
|
||||||
// Importantly: We do *not* want to emit a `StreamedAssistantText` event here, as it
|
// Importantly: We do *not* want to emit a `StreamedAssistantText` event here, as it
|
||||||
// will result in duplicating the text of the chunk in the rendered Markdown.
|
// will result in duplicating the text of the chunk in the rendered Markdown.
|
||||||
thread.insert_message(Role::Assistant, chunk.content, cx)
|
thread.insert_message(Role::Assistant, chunk, cx);
|
||||||
};
|
};
|
||||||
|
|
||||||
if script_id.is_none() && script_tag_parser.found_script() {
|
|
||||||
let id = thread
|
|
||||||
.script_session
|
|
||||||
.update(cx, |session, _cx| session.new_script());
|
|
||||||
thread.scripts_by_assistant_message.insert(message_id, id);
|
|
||||||
|
|
||||||
script_id = Some(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (Some(script_source), Some(script_id)) =
|
|
||||||
(chunk.script_source, script_id)
|
|
||||||
{
|
|
||||||
// TODO: move buffer to script and run as it streams
|
|
||||||
thread
|
|
||||||
.script_session
|
|
||||||
.update(cx, |this, cx| {
|
|
||||||
this.run_script(script_id, script_source, cx)
|
|
||||||
})
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LanguageModelCompletionEvent::ToolUse(tool_use) => {
|
LanguageModelCompletionEvent::ToolUse(tool_use) => {
|
||||||
@@ -548,9 +496,15 @@ impl Thread {
|
|||||||
.iter()
|
.iter()
|
||||||
.rfind(|message| message.role == Role::Assistant)
|
.rfind(|message| message.role == Role::Assistant)
|
||||||
{
|
{
|
||||||
thread
|
if tool_use.name.as_ref() == ScriptingTool::NAME {
|
||||||
.tool_use
|
thread
|
||||||
.request_tool_use(last_assistant_message.id, tool_use);
|
.scripting_tool_use
|
||||||
|
.request_tool_use(last_assistant_message.id, tool_use);
|
||||||
|
} else {
|
||||||
|
thread
|
||||||
|
.tool_use
|
||||||
|
.request_tool_use(last_assistant_message.id, tool_use);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -670,6 +624,7 @@ impl Thread {
|
|||||||
|
|
||||||
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) {
|
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) {
|
||||||
let pending_tool_uses = self
|
let pending_tool_uses = self
|
||||||
|
.tool_use
|
||||||
.pending_tool_uses()
|
.pending_tool_uses()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|tool_use| tool_use.status.is_idle())
|
.filter(|tool_use| tool_use.status.is_idle())
|
||||||
@@ -683,6 +638,45 @@ impl Thread {
|
|||||||
self.insert_tool_output(tool_use.id.clone(), task, cx);
|
self.insert_tool_output(tool_use.id.clone(), task, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pending_scripting_tool_uses = self
|
||||||
|
.scripting_tool_use
|
||||||
|
.pending_tool_uses()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|tool_use| tool_use.status.is_idle())
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for scripting_tool_use in pending_scripting_tool_uses {
|
||||||
|
let task = match ScriptingTool::deserialize_input(scripting_tool_use.input) {
|
||||||
|
Err(err) => Task::ready(Err(err.into())),
|
||||||
|
Ok(input) => {
|
||||||
|
let (script_id, script_task) =
|
||||||
|
self.scripting_session.update(cx, move |session, cx| {
|
||||||
|
session.run_script(input.lua_script, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
let session = self.scripting_session.clone();
|
||||||
|
cx.spawn(|_, cx| async move {
|
||||||
|
script_task.await;
|
||||||
|
|
||||||
|
let message = session.read_with(&cx, |session, _cx| {
|
||||||
|
// Using a id to get the script output seems impractical.
|
||||||
|
// Why not just include it in the Task result?
|
||||||
|
// This is because we'll later report the script state as it runs,
|
||||||
|
session
|
||||||
|
.get(script_id)
|
||||||
|
.output_message_for_llm()
|
||||||
|
.expect("Script shouldn't still be running")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.insert_scripting_tool_output(scripting_tool_use.id.clone(), task, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_tool_output(
|
pub fn insert_tool_output(
|
||||||
@@ -697,11 +691,14 @@ impl Thread {
|
|||||||
let output = output.await;
|
let output = output.await;
|
||||||
thread
|
thread
|
||||||
.update(&mut cx, |thread, cx| {
|
.update(&mut cx, |thread, cx| {
|
||||||
thread
|
let pending_tool_use = thread
|
||||||
.tool_use
|
.tool_use
|
||||||
.insert_tool_output(tool_use_id.clone(), output);
|
.insert_tool_output(tool_use_id.clone(), output);
|
||||||
|
|
||||||
cx.emit(ThreadEvent::ToolFinished { tool_use_id });
|
cx.emit(ThreadEvent::ToolFinished {
|
||||||
|
tool_use_id,
|
||||||
|
pending_tool_use,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -711,6 +708,35 @@ impl Thread {
|
|||||||
.run_pending_tool(tool_use_id, insert_output_task);
|
.run_pending_tool(tool_use_id, insert_output_task);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_scripting_tool_output(
|
||||||
|
&mut self,
|
||||||
|
tool_use_id: LanguageModelToolUseId,
|
||||||
|
output: Task<Result<String>>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let insert_output_task = cx.spawn(|thread, mut cx| {
|
||||||
|
let tool_use_id = tool_use_id.clone();
|
||||||
|
async move {
|
||||||
|
let output = output.await;
|
||||||
|
thread
|
||||||
|
.update(&mut cx, |thread, cx| {
|
||||||
|
let pending_tool_use = thread
|
||||||
|
.scripting_tool_use
|
||||||
|
.insert_tool_output(tool_use_id.clone(), output);
|
||||||
|
|
||||||
|
cx.emit(ThreadEvent::ToolFinished {
|
||||||
|
tool_use_id,
|
||||||
|
pending_tool_use,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.scripting_tool_use
|
||||||
|
.run_pending_tool(tool_use_id, insert_output_task);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn send_tool_results_to_model(
|
pub fn send_tool_results_to_model(
|
||||||
&mut self,
|
&mut self,
|
||||||
model: Arc<dyn LanguageModel>,
|
model: Arc<dyn LanguageModel>,
|
||||||
@@ -725,7 +751,7 @@ impl Thread {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
self.send_to_model(model, RequestKind::Chat, true, cx);
|
self.send_to_model(model, RequestKind::Chat, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cancels the last pending completion, if there are any pending.
|
/// Cancels the last pending completion, if there are any pending.
|
||||||
@@ -760,8 +786,9 @@ pub enum ThreadEvent {
|
|||||||
ToolFinished {
|
ToolFinished {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
tool_use_id: LanguageModelToolUseId,
|
tool_use_id: LanguageModelToolUseId,
|
||||||
|
/// The pending tool use that corresponds to this tool.
|
||||||
|
pending_tool_use: Option<PendingToolUse>,
|
||||||
},
|
},
|
||||||
ScriptFinished,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<ThreadEvent> for Thread {}
|
impl EventEmitter<ThreadEvent> for Thread {}
|
||||||
|
|||||||
@@ -116,28 +116,35 @@ impl ThreadStore {
|
|||||||
updated_at: thread.updated_at(),
|
updated_at: thread.updated_at(),
|
||||||
messages: thread
|
messages: thread
|
||||||
.messages()
|
.messages()
|
||||||
.map(|message| SavedMessage {
|
.map(|message| {
|
||||||
id: message.id,
|
let all_tool_uses = thread
|
||||||
role: message.role,
|
|
||||||
text: message.text.clone(),
|
|
||||||
tool_uses: thread
|
|
||||||
.tool_uses_for_message(message.id)
|
.tool_uses_for_message(message.id)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.chain(thread.scripting_tool_uses_for_message(message.id))
|
||||||
.map(|tool_use| SavedToolUse {
|
.map(|tool_use| SavedToolUse {
|
||||||
id: tool_use.id,
|
id: tool_use.id,
|
||||||
name: tool_use.name,
|
name: tool_use.name,
|
||||||
input: tool_use.input,
|
input: tool_use.input,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect();
|
||||||
tool_results: thread
|
let all_tool_results = thread
|
||||||
.tool_results_for_message(message.id)
|
.tool_results_for_message(message.id)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.chain(thread.scripting_tool_results_for_message(message.id))
|
||||||
.map(|tool_result| SavedToolResult {
|
.map(|tool_result| SavedToolResult {
|
||||||
tool_use_id: tool_result.tool_use_id.clone(),
|
tool_use_id: tool_result.tool_use_id.clone(),
|
||||||
is_error: tool_result.is_error,
|
is_error: tool_result.is_error,
|
||||||
content: tool_result.content.clone(),
|
content: tool_result.content.clone(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect();
|
||||||
|
|
||||||
|
SavedMessage {
|
||||||
|
id: message.id,
|
||||||
|
role: message.role,
|
||||||
|
text: message.text.clone(),
|
||||||
|
tool_uses: all_tool_uses,
|
||||||
|
tool_results: all_tool_results,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
|
|||||||
95
crates/assistant2/src/tool_selector.rs
Normal file
95
crates/assistant2/src/tool_selector.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||||
|
use gpui::Entity;
|
||||||
|
use scripting_tool::ScriptingTool;
|
||||||
|
use ui::{prelude::*, ContextMenu, IconButtonShape, PopoverMenu, Tooltip};
|
||||||
|
|
||||||
|
pub struct ToolSelector {
|
||||||
|
tools: Arc<ToolWorkingSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToolSelector {
|
||||||
|
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut Context<Self>) -> Self {
|
||||||
|
Self { tools }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_context_menu(
|
||||||
|
&self,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Entity<ContextMenu> {
|
||||||
|
ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||||
|
let tools_by_source = self.tools.tools_by_source(cx);
|
||||||
|
|
||||||
|
for (source, tools) in tools_by_source {
|
||||||
|
let mut tools = tools
|
||||||
|
.into_iter()
|
||||||
|
.map(|tool| {
|
||||||
|
let source = tool.source();
|
||||||
|
let name = tool.name().into();
|
||||||
|
let is_enabled = self.tools.is_enabled(&source, &name);
|
||||||
|
|
||||||
|
(source, name, is_enabled)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if ToolSource::Native == source {
|
||||||
|
tools.push((
|
||||||
|
ToolSource::Native,
|
||||||
|
ScriptingTool::NAME.into(),
|
||||||
|
self.tools.is_scripting_tool_enabled(),
|
||||||
|
));
|
||||||
|
tools.sort_by(|(_, name_a, _), (_, name_b, _)| name_a.cmp(name_b));
|
||||||
|
}
|
||||||
|
|
||||||
|
menu = match source {
|
||||||
|
ToolSource::Native => menu.header("Zed"),
|
||||||
|
ToolSource::ContextServer { id } => menu.separator().header(id),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (source, name, is_enabled) in tools {
|
||||||
|
menu =
|
||||||
|
menu.toggleable_entry(name.clone(), is_enabled, IconPosition::End, None, {
|
||||||
|
let tools = self.tools.clone();
|
||||||
|
move |_window, _cx| {
|
||||||
|
if name.as_ref() == ScriptingTool::NAME {
|
||||||
|
if is_enabled {
|
||||||
|
tools.disable_scripting_tool();
|
||||||
|
} else {
|
||||||
|
tools.enable_scripting_tool();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if is_enabled {
|
||||||
|
tools.disable(source.clone(), &[name.clone()]);
|
||||||
|
} else {
|
||||||
|
tools.enable(source.clone(), &[name.clone()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
menu
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ToolSelector {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
|
||||||
|
let this = cx.entity().clone();
|
||||||
|
PopoverMenu::new("tool-selector")
|
||||||
|
.menu(move |window, cx| {
|
||||||
|
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||||
|
})
|
||||||
|
.trigger_with_tooltip(
|
||||||
|
IconButton::new("tool-selector-button", IconName::SettingsAlt)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.icon_color(Color::Muted),
|
||||||
|
Tooltip::text("Customize Tools"),
|
||||||
|
)
|
||||||
|
.anchor(gpui::Corner::BottomLeft)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,25 +46,39 @@ impl ToolUseState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_saved_messages(messages: &[SavedMessage]) -> Self {
|
/// Constructs a [`ToolUseState`] from the given list of [`SavedMessage`]s.
|
||||||
|
///
|
||||||
|
/// Accepts a function to filter the tools that should be used to populate the state.
|
||||||
|
pub fn from_saved_messages(
|
||||||
|
messages: &[SavedMessage],
|
||||||
|
mut filter_by_tool_name: impl FnMut(&str) -> bool,
|
||||||
|
) -> Self {
|
||||||
let mut this = Self::new();
|
let mut this = Self::new();
|
||||||
|
let mut tool_names_by_id = HashMap::default();
|
||||||
|
|
||||||
for message in messages {
|
for message in messages {
|
||||||
match message.role {
|
match message.role {
|
||||||
Role::Assistant => {
|
Role::Assistant => {
|
||||||
if !message.tool_uses.is_empty() {
|
if !message.tool_uses.is_empty() {
|
||||||
this.tool_uses_by_assistant_message.insert(
|
let tool_uses = message
|
||||||
message.id,
|
.tool_uses
|
||||||
message
|
.iter()
|
||||||
.tool_uses
|
.filter(|tool_use| (filter_by_tool_name)(tool_use.name.as_ref()))
|
||||||
|
.map(|tool_use| LanguageModelToolUse {
|
||||||
|
id: tool_use.id.clone(),
|
||||||
|
name: tool_use.name.clone().into(),
|
||||||
|
input: tool_use.input.clone(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
tool_names_by_id.extend(
|
||||||
|
tool_uses
|
||||||
.iter()
|
.iter()
|
||||||
.map(|tool_use| LanguageModelToolUse {
|
.map(|tool_use| (tool_use.id.clone(), tool_use.name.clone())),
|
||||||
id: tool_use.id.clone(),
|
|
||||||
name: tool_use.name.clone().into(),
|
|
||||||
input: tool_use.input.clone(),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.tool_uses_by_assistant_message
|
||||||
|
.insert(message.id, tool_uses);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Role::User => {
|
Role::User => {
|
||||||
@@ -76,6 +90,14 @@ impl ToolUseState {
|
|||||||
|
|
||||||
for tool_result in &message.tool_results {
|
for tool_result in &message.tool_results {
|
||||||
let tool_use_id = tool_result.tool_use_id.clone();
|
let tool_use_id = tool_result.tool_use_id.clone();
|
||||||
|
let Some(tool_use) = tool_names_by_id.get(&tool_use_id) else {
|
||||||
|
log::warn!("no tool name found for tool use: {tool_use_id:?}");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(filter_by_tool_name)(tool_use.as_ref()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
tool_uses_by_user_message.push(tool_use_id.clone());
|
tool_uses_by_user_message.push(tool_use_id.clone());
|
||||||
this.tool_results.insert(
|
this.tool_results.insert(
|
||||||
@@ -202,7 +224,7 @@ impl ToolUseState {
|
|||||||
&mut self,
|
&mut self,
|
||||||
tool_use_id: LanguageModelToolUseId,
|
tool_use_id: LanguageModelToolUseId,
|
||||||
output: Result<String>,
|
output: Result<String>,
|
||||||
) {
|
) -> Option<PendingToolUse> {
|
||||||
match output {
|
match output {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
self.tool_results.insert(
|
self.tool_results.insert(
|
||||||
@@ -213,7 +235,7 @@ impl ToolUseState {
|
|||||||
is_error: false,
|
is_error: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.pending_tool_uses_by_id.remove(&tool_use_id);
|
self.pending_tool_uses_by_id.remove(&tool_use_id)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.tool_results.insert(
|
self.tool_results.insert(
|
||||||
@@ -228,6 +250,8 @@ impl ToolUseState {
|
|||||||
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
|
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
|
||||||
tool_use.status = PendingToolUseStatus::Error(err.to_string().into());
|
tool_use.status = PendingToolUseStatus::Error(err.to_string().into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.pending_tool_uses_by_id.get(&tool_use_id).cloned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,6 +291,7 @@ impl ToolUseState {
|
|||||||
pub struct PendingToolUse {
|
pub struct PendingToolUse {
|
||||||
pub id: LanguageModelToolUseId,
|
pub id: LanguageModelToolUseId,
|
||||||
/// The ID of the Assistant message in which the tool use was requested.
|
/// The ID of the Assistant message in which the tool use was requested.
|
||||||
|
#[allow(unused)]
|
||||||
pub assistant_message_id: MessageId,
|
pub assistant_message_id: MessageId,
|
||||||
pub name: Arc<str>,
|
pub name: Arc<str>,
|
||||||
pub input: serde_json::Value,
|
pub input: serde_json::Value,
|
||||||
|
|||||||
@@ -647,7 +647,6 @@ impl AssistantContext {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
id: ContextId,
|
id: ContextId,
|
||||||
replica_id: ReplicaId,
|
replica_id: ReplicaId,
|
||||||
@@ -768,7 +767,6 @@ impl AssistantContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn deserialize(
|
pub fn deserialize(
|
||||||
saved_context: SavedContext,
|
saved_context: SavedContext,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
|||||||
@@ -535,7 +535,6 @@ impl ContextEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn run_command(
|
pub fn run_command(
|
||||||
&mut self,
|
&mut self,
|
||||||
command_range: Range<language::Anchor>,
|
command_range: Range<language::Anchor>,
|
||||||
@@ -2057,7 +2056,6 @@ impl ContextEditor {
|
|||||||
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
|
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn render_patch_block(
|
fn render_patch_block(
|
||||||
&mut self,
|
&mut self,
|
||||||
range: Range<text::Anchor>,
|
range: Range<text::Anchor>,
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ impl SlashCommandCompletionProvider {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn complete_command_argument(
|
fn complete_command_argument(
|
||||||
&self,
|
&self,
|
||||||
command_name: &str,
|
command_name: &str,
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
mod session;
|
|
||||||
mod tag;
|
|
||||||
|
|
||||||
pub use session::*;
|
|
||||||
pub use tag::*;
|
|
||||||
|
|
||||||
pub const SCRIPTING_PROMPT: &str = include_str!("./system_prompt.txt");
|
|
||||||
@@ -1,953 +0,0 @@
|
|||||||
use anyhow::anyhow;
|
|
||||||
use collections::{HashMap, HashSet};
|
|
||||||
use futures::{
|
|
||||||
channel::{mpsc, oneshot},
|
|
||||||
pin_mut, SinkExt, StreamExt,
|
|
||||||
};
|
|
||||||
use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
|
|
||||||
use mlua::{ExternalResult, Lua, MultiValue, Table, UserData, UserDataMethods};
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use project::{search::SearchQuery, Fs, Project};
|
|
||||||
use regex::Regex;
|
|
||||||
use std::{
|
|
||||||
cell::RefCell,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
use util::{paths::PathMatcher, ResultExt};
|
|
||||||
|
|
||||||
use crate::{SCRIPT_END_TAG, SCRIPT_START_TAG};
|
|
||||||
|
|
||||||
struct ForegroundFn(Box<dyn FnOnce(WeakEntity<ScriptSession>, AsyncApp) + Send>);
|
|
||||||
|
|
||||||
pub struct ScriptSession {
|
|
||||||
project: Entity<Project>,
|
|
||||||
// TODO Remove this
|
|
||||||
fs_changes: Arc<Mutex<HashMap<PathBuf, Vec<u8>>>>,
|
|
||||||
foreground_fns_tx: mpsc::Sender<ForegroundFn>,
|
|
||||||
_invoke_foreground_fns: Task<()>,
|
|
||||||
scripts: Vec<Script>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScriptSession {
|
|
||||||
pub fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
|
|
||||||
let (foreground_fns_tx, mut foreground_fns_rx) = mpsc::channel(128);
|
|
||||||
ScriptSession {
|
|
||||||
project,
|
|
||||||
fs_changes: Arc::new(Mutex::new(HashMap::default())),
|
|
||||||
foreground_fns_tx,
|
|
||||||
_invoke_foreground_fns: cx.spawn(|this, cx| async move {
|
|
||||||
while let Some(foreground_fn) = foreground_fns_rx.next().await {
|
|
||||||
foreground_fn.0(this.clone(), cx.clone());
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
scripts: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_script(&mut self) -> ScriptId {
|
|
||||||
let id = ScriptId(self.scripts.len() as u32);
|
|
||||||
let script = Script {
|
|
||||||
id,
|
|
||||||
state: ScriptState::Generating,
|
|
||||||
source: SharedString::new_static(""),
|
|
||||||
};
|
|
||||||
self.scripts.push(script);
|
|
||||||
id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_script(
|
|
||||||
&mut self,
|
|
||||||
script_id: ScriptId,
|
|
||||||
script_src: String,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Task<anyhow::Result<()>> {
|
|
||||||
let script = self.get_mut(script_id);
|
|
||||||
|
|
||||||
let stdout = Arc::new(Mutex::new(String::new()));
|
|
||||||
script.source = script_src.clone().into();
|
|
||||||
script.state = ScriptState::Running {
|
|
||||||
stdout: stdout.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let task = self.run_lua(script_src, stdout, cx);
|
|
||||||
|
|
||||||
cx.emit(ScriptEvent::Spawned(script_id));
|
|
||||||
|
|
||||||
cx.spawn(|session, mut cx| async move {
|
|
||||||
let result = task.await;
|
|
||||||
|
|
||||||
session.update(&mut cx, |session, cx| {
|
|
||||||
let script = session.get_mut(script_id);
|
|
||||||
let stdout = script.stdout_snapshot();
|
|
||||||
|
|
||||||
script.state = match result {
|
|
||||||
Ok(()) => ScriptState::Succeeded { stdout },
|
|
||||||
Err(error) => ScriptState::Failed { stdout, error },
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.emit(ScriptEvent::Exited(script_id))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_lua(
|
|
||||||
&mut self,
|
|
||||||
script: String,
|
|
||||||
stdout: Arc<Mutex<String>>,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Task<anyhow::Result<()>> {
|
|
||||||
const SANDBOX_PREAMBLE: &str = include_str!("sandbox_preamble.lua");
|
|
||||||
|
|
||||||
// TODO Remove fs_changes
|
|
||||||
let fs_changes = self.fs_changes.clone();
|
|
||||||
// TODO Honor all worktrees instead of the first one
|
|
||||||
let root_dir = self
|
|
||||||
.project
|
|
||||||
.read(cx)
|
|
||||||
.visible_worktrees(cx)
|
|
||||||
.next()
|
|
||||||
.map(|worktree| worktree.read(cx).abs_path());
|
|
||||||
|
|
||||||
let fs = self.project.read(cx).fs().clone();
|
|
||||||
let foreground_fns_tx = self.foreground_fns_tx.clone();
|
|
||||||
|
|
||||||
let task = cx.background_spawn({
|
|
||||||
let stdout = stdout.clone();
|
|
||||||
|
|
||||||
async move {
|
|
||||||
let lua = Lua::new();
|
|
||||||
lua.set_memory_limit(2 * 1024 * 1024 * 1024)?; // 2 GB
|
|
||||||
let globals = lua.globals();
|
|
||||||
|
|
||||||
// Use the project root dir as the script's current working dir.
|
|
||||||
if let Some(root_dir) = &root_dir {
|
|
||||||
if let Some(root_dir) = root_dir.to_str() {
|
|
||||||
globals.set("cwd", root_dir)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
globals.set(
|
|
||||||
"sb_print",
|
|
||||||
lua.create_function({
|
|
||||||
let stdout = stdout.clone();
|
|
||||||
move |_, args: MultiValue| Self::print(args, &stdout)
|
|
||||||
})?,
|
|
||||||
)?;
|
|
||||||
globals.set(
|
|
||||||
"search",
|
|
||||||
lua.create_async_function({
|
|
||||||
let foreground_fns_tx = foreground_fns_tx.clone();
|
|
||||||
move |lua, regex| {
|
|
||||||
let mut foreground_fns_tx = foreground_fns_tx.clone();
|
|
||||||
let fs = fs.clone();
|
|
||||||
async move {
|
|
||||||
Self::search(&lua, &mut foreground_fns_tx, fs, regex)
|
|
||||||
.await
|
|
||||||
.into_lua_err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})?,
|
|
||||||
)?;
|
|
||||||
globals.set(
|
|
||||||
"outline",
|
|
||||||
lua.create_async_function({
|
|
||||||
let root_dir = root_dir.clone();
|
|
||||||
move |_lua, path| {
|
|
||||||
let mut foreground_fns_tx = foreground_fns_tx.clone();
|
|
||||||
let root_dir = root_dir.clone();
|
|
||||||
async move {
|
|
||||||
Self::outline(root_dir, &mut foreground_fns_tx, path)
|
|
||||||
.await
|
|
||||||
.into_lua_err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})?,
|
|
||||||
)?;
|
|
||||||
globals.set(
|
|
||||||
"sb_io_open",
|
|
||||||
lua.create_function({
|
|
||||||
let fs_changes = fs_changes.clone();
|
|
||||||
let root_dir = root_dir.clone();
|
|
||||||
move |lua, (path_str, mode)| {
|
|
||||||
Self::io_open(&lua, &fs_changes, root_dir.as_ref(), path_str, mode)
|
|
||||||
}
|
|
||||||
})?,
|
|
||||||
)?;
|
|
||||||
globals.set("user_script", script)?;
|
|
||||||
|
|
||||||
lua.load(SANDBOX_PREAMBLE).exec_async().await?;
|
|
||||||
|
|
||||||
// Drop Lua instance to decrement reference count.
|
|
||||||
drop(lua);
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
task
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, script_id: ScriptId) -> &Script {
|
|
||||||
&self.scripts[script_id.0 as usize]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_mut(&mut self, script_id: ScriptId) -> &mut Script {
|
|
||||||
&mut self.scripts[script_id.0 as usize]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sandboxed print() function in Lua.
|
|
||||||
fn print(args: MultiValue, stdout: &Mutex<String>) -> mlua::Result<()> {
|
|
||||||
for (index, arg) in args.into_iter().enumerate() {
|
|
||||||
// Lua's `print()` prints tab characters between each argument.
|
|
||||||
if index > 0 {
|
|
||||||
stdout.lock().push('\t');
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the argument's to_string() fails, have the whole function call fail.
|
|
||||||
stdout.lock().push_str(&arg.to_string()?);
|
|
||||||
}
|
|
||||||
stdout.lock().push('\n');
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sandboxed io.open() function in Lua.
|
|
||||||
fn io_open(
|
|
||||||
lua: &Lua,
|
|
||||||
fs_changes: &Arc<Mutex<HashMap<PathBuf, Vec<u8>>>>,
|
|
||||||
root_dir: Option<&Arc<Path>>,
|
|
||||||
path_str: String,
|
|
||||||
mode: Option<String>,
|
|
||||||
) -> mlua::Result<(Option<Table>, String)> {
|
|
||||||
let root_dir = root_dir
|
|
||||||
.ok_or_else(|| mlua::Error::runtime("cannot open file without a root directory"))?;
|
|
||||||
|
|
||||||
let mode = mode.unwrap_or_else(|| "r".to_string());
|
|
||||||
|
|
||||||
// Parse the mode string to determine read/write permissions
|
|
||||||
let read_perm = mode.contains('r');
|
|
||||||
let write_perm = mode.contains('w') || mode.contains('a') || mode.contains('+');
|
|
||||||
let append = mode.contains('a');
|
|
||||||
let truncate = mode.contains('w');
|
|
||||||
|
|
||||||
// This will be the Lua value returned from the `open` function.
|
|
||||||
let file = lua.create_table()?;
|
|
||||||
|
|
||||||
// Store file metadata in the file
|
|
||||||
file.set("__path", path_str.clone())?;
|
|
||||||
file.set("__mode", mode.clone())?;
|
|
||||||
file.set("__read_perm", read_perm)?;
|
|
||||||
file.set("__write_perm", write_perm)?;
|
|
||||||
|
|
||||||
let path = match Self::parse_abs_path_in_root_dir(&root_dir, &path_str) {
|
|
||||||
Ok(path) => path,
|
|
||||||
Err(err) => return Ok((None, format!("{err}"))),
|
|
||||||
};
|
|
||||||
|
|
||||||
// close method
|
|
||||||
let close_fn = {
|
|
||||||
let fs_changes = fs_changes.clone();
|
|
||||||
lua.create_function(move |_lua, file_userdata: mlua::Table| {
|
|
||||||
let write_perm = file_userdata.get::<bool>("__write_perm")?;
|
|
||||||
let path = file_userdata.get::<String>("__path")?;
|
|
||||||
|
|
||||||
if write_perm {
|
|
||||||
// When closing a writable file, record the content
|
|
||||||
let content = file_userdata.get::<mlua::AnyUserData>("__content")?;
|
|
||||||
let content_ref = content.borrow::<FileContent>()?;
|
|
||||||
let content_vec = content_ref.0.borrow();
|
|
||||||
|
|
||||||
// Don't actually write to disk; instead, just update fs_changes.
|
|
||||||
let path_buf = PathBuf::from(&path);
|
|
||||||
fs_changes
|
|
||||||
.lock()
|
|
||||||
.insert(path_buf.clone(), content_vec.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(true)
|
|
||||||
})?
|
|
||||||
};
|
|
||||||
file.set("close", close_fn)?;
|
|
||||||
|
|
||||||
// If it's a directory, give it a custom read() and return early.
|
|
||||||
if path.is_dir() {
|
|
||||||
// TODO handle the case where we changed it in the in-memory fs
|
|
||||||
|
|
||||||
// Create a special directory handle
|
|
||||||
file.set("__is_directory", true)?;
|
|
||||||
|
|
||||||
// Store directory entries
|
|
||||||
let entries = match std::fs::read_dir(&path) {
|
|
||||||
Ok(entries) => {
|
|
||||||
let mut entry_names = Vec::new();
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
entry_names.push(entry.file_name().to_string_lossy().into_owned());
|
|
||||||
}
|
|
||||||
entry_names
|
|
||||||
}
|
|
||||||
Err(e) => return Ok((None, format!("Error reading directory: {}", e))),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save the list of entries
|
|
||||||
file.set("__dir_entries", entries)?;
|
|
||||||
file.set("__dir_position", 0usize)?;
|
|
||||||
|
|
||||||
// Create a directory-specific read function
|
|
||||||
let read_fn = lua.create_function(|_lua, file_userdata: mlua::Table| {
|
|
||||||
let position = file_userdata.get::<usize>("__dir_position")?;
|
|
||||||
let entries = file_userdata.get::<Vec<String>>("__dir_entries")?;
|
|
||||||
|
|
||||||
if position >= entries.len() {
|
|
||||||
return Ok(None); // No more entries
|
|
||||||
}
|
|
||||||
|
|
||||||
let entry = entries[position].clone();
|
|
||||||
file_userdata.set("__dir_position", position + 1)?;
|
|
||||||
|
|
||||||
Ok(Some(entry))
|
|
||||||
})?;
|
|
||||||
file.set("read", read_fn)?;
|
|
||||||
|
|
||||||
// If we got this far, the directory was opened successfully
|
|
||||||
return Ok((Some(file), String::new()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let fs_changes_map = fs_changes.lock();
|
|
||||||
|
|
||||||
let is_in_changes = fs_changes_map.contains_key(&path);
|
|
||||||
let file_exists = is_in_changes || path.exists();
|
|
||||||
let mut file_content = Vec::new();
|
|
||||||
|
|
||||||
if file_exists && !truncate {
|
|
||||||
if is_in_changes {
|
|
||||||
file_content = fs_changes_map.get(&path).unwrap().clone();
|
|
||||||
} else {
|
|
||||||
// Try to read existing content if file exists and we're not truncating
|
|
||||||
match std::fs::read(&path) {
|
|
||||||
Ok(content) => file_content = content,
|
|
||||||
Err(e) => return Ok((None, format!("Error reading file: {}", e))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(fs_changes_map); // Unlock the fs_changes mutex.
|
|
||||||
|
|
||||||
// If in append mode, position should be at the end
|
|
||||||
let position = if append && file_exists {
|
|
||||||
file_content.len()
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
file.set("__position", position)?;
|
|
||||||
file.set(
|
|
||||||
"__content",
|
|
||||||
lua.create_userdata(FileContent(RefCell::new(file_content)))?,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Create file methods
|
|
||||||
|
|
||||||
// read method
|
|
||||||
let read_fn = {
|
|
||||||
lua.create_function(
|
|
||||||
|_lua, (file_userdata, format): (mlua::Table, Option<mlua::Value>)| {
|
|
||||||
let read_perm = file_userdata.get::<bool>("__read_perm")?;
|
|
||||||
if !read_perm {
|
|
||||||
return Err(mlua::Error::runtime("File not open for reading"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = file_userdata.get::<mlua::AnyUserData>("__content")?;
|
|
||||||
let mut position = file_userdata.get::<usize>("__position")?;
|
|
||||||
let content_ref = content.borrow::<FileContent>()?;
|
|
||||||
let content_vec = content_ref.0.borrow();
|
|
||||||
|
|
||||||
if position >= content_vec.len() {
|
|
||||||
return Ok(None); // EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
match format {
|
|
||||||
Some(mlua::Value::String(s)) => {
|
|
||||||
let lossy_string = s.to_string_lossy();
|
|
||||||
let format_str: &str = lossy_string.as_ref();
|
|
||||||
|
|
||||||
// Only consider the first 2 bytes, since it's common to pass e.g. "*all" instead of "*a"
|
|
||||||
match &format_str[0..2] {
|
|
||||||
"*a" => {
|
|
||||||
// Read entire file from current position
|
|
||||||
let result = String::from_utf8_lossy(&content_vec[position..])
|
|
||||||
.to_string();
|
|
||||||
position = content_vec.len();
|
|
||||||
file_userdata.set("__position", position)?;
|
|
||||||
Ok(Some(result))
|
|
||||||
}
|
|
||||||
"*l" => {
|
|
||||||
// Read next line
|
|
||||||
let mut line = Vec::new();
|
|
||||||
let mut found_newline = false;
|
|
||||||
|
|
||||||
while position < content_vec.len() {
|
|
||||||
let byte = content_vec[position];
|
|
||||||
position += 1;
|
|
||||||
|
|
||||||
if byte == b'\n' {
|
|
||||||
found_newline = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip \r in \r\n sequence but add it if it's alone
|
|
||||||
if byte == b'\r' {
|
|
||||||
if position < content_vec.len()
|
|
||||||
&& content_vec[position] == b'\n'
|
|
||||||
{
|
|
||||||
position += 1;
|
|
||||||
found_newline = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
line.push(byte);
|
|
||||||
}
|
|
||||||
|
|
||||||
file_userdata.set("__position", position)?;
|
|
||||||
|
|
||||||
if !found_newline
|
|
||||||
&& line.is_empty()
|
|
||||||
&& position >= content_vec.len()
|
|
||||||
{
|
|
||||||
return Ok(None); // EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = String::from_utf8_lossy(&line).to_string();
|
|
||||||
Ok(Some(result))
|
|
||||||
}
|
|
||||||
"*n" => {
|
|
||||||
// Try to parse as a number (number of bytes to read)
|
|
||||||
match format_str.parse::<usize>() {
|
|
||||||
Ok(n) => {
|
|
||||||
let end =
|
|
||||||
std::cmp::min(position + n, content_vec.len());
|
|
||||||
let bytes = &content_vec[position..end];
|
|
||||||
let result = String::from_utf8_lossy(bytes).to_string();
|
|
||||||
position = end;
|
|
||||||
file_userdata.set("__position", position)?;
|
|
||||||
Ok(Some(result))
|
|
||||||
}
|
|
||||||
Err(_) => Err(mlua::Error::runtime(format!(
|
|
||||||
"Invalid format: {}",
|
|
||||||
format_str
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"*L" => {
|
|
||||||
// Read next line keeping the end of line
|
|
||||||
let mut line = Vec::new();
|
|
||||||
|
|
||||||
while position < content_vec.len() {
|
|
||||||
let byte = content_vec[position];
|
|
||||||
position += 1;
|
|
||||||
|
|
||||||
line.push(byte);
|
|
||||||
|
|
||||||
if byte == b'\n' {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we encounter a \r, add it and check if the next is \n
|
|
||||||
if byte == b'\r'
|
|
||||||
&& position < content_vec.len()
|
|
||||||
&& content_vec[position] == b'\n'
|
|
||||||
{
|
|
||||||
line.push(content_vec[position]);
|
|
||||||
position += 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file_userdata.set("__position", position)?;
|
|
||||||
|
|
||||||
if line.is_empty() && position >= content_vec.len() {
|
|
||||||
return Ok(None); // EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = String::from_utf8_lossy(&line).to_string();
|
|
||||||
Ok(Some(result))
|
|
||||||
}
|
|
||||||
_ => Err(mlua::Error::runtime(format!(
|
|
||||||
"Unsupported format: {}",
|
|
||||||
format_str
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(mlua::Value::Number(n)) => {
|
|
||||||
// Read n bytes
|
|
||||||
let n = n as usize;
|
|
||||||
let end = std::cmp::min(position + n, content_vec.len());
|
|
||||||
let bytes = &content_vec[position..end];
|
|
||||||
let result = String::from_utf8_lossy(bytes).to_string();
|
|
||||||
position = end;
|
|
||||||
file_userdata.set("__position", position)?;
|
|
||||||
Ok(Some(result))
|
|
||||||
}
|
|
||||||
Some(_) => Err(mlua::Error::runtime("Invalid format")),
|
|
||||||
None => {
|
|
||||||
// Default is to read a line
|
|
||||||
let mut line = Vec::new();
|
|
||||||
let mut found_newline = false;
|
|
||||||
|
|
||||||
while position < content_vec.len() {
|
|
||||||
let byte = content_vec[position];
|
|
||||||
position += 1;
|
|
||||||
|
|
||||||
if byte == b'\n' {
|
|
||||||
found_newline = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle \r\n
|
|
||||||
if byte == b'\r' {
|
|
||||||
if position < content_vec.len()
|
|
||||||
&& content_vec[position] == b'\n'
|
|
||||||
{
|
|
||||||
position += 1;
|
|
||||||
found_newline = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
line.push(byte);
|
|
||||||
}
|
|
||||||
|
|
||||||
file_userdata.set("__position", position)?;
|
|
||||||
|
|
||||||
if !found_newline && line.is_empty() && position >= content_vec.len() {
|
|
||||||
return Ok(None); // EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = String::from_utf8_lossy(&line).to_string();
|
|
||||||
Ok(Some(result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)?
|
|
||||||
};
|
|
||||||
file.set("read", read_fn)?;
|
|
||||||
|
|
||||||
// write method
|
|
||||||
let write_fn = {
|
|
||||||
let fs_changes = fs_changes.clone();
|
|
||||||
|
|
||||||
lua.create_function(move |_lua, (file_userdata, text): (mlua::Table, String)| {
|
|
||||||
let write_perm = file_userdata.get::<bool>("__write_perm")?;
|
|
||||||
if !write_perm {
|
|
||||||
return Err(mlua::Error::runtime("File not open for writing"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = file_userdata.get::<mlua::AnyUserData>("__content")?;
|
|
||||||
let position = file_userdata.get::<usize>("__position")?;
|
|
||||||
let content_ref = content.borrow::<FileContent>()?;
|
|
||||||
let mut content_vec = content_ref.0.borrow_mut();
|
|
||||||
|
|
||||||
let bytes = text.as_bytes();
|
|
||||||
|
|
||||||
// Ensure the vector has enough capacity
|
|
||||||
if position + bytes.len() > content_vec.len() {
|
|
||||||
content_vec.resize(position + bytes.len(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the bytes
|
|
||||||
for (i, &byte) in bytes.iter().enumerate() {
|
|
||||||
content_vec[position + i] = byte;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update position
|
|
||||||
let new_position = position + bytes.len();
|
|
||||||
file_userdata.set("__position", new_position)?;
|
|
||||||
|
|
||||||
// Update fs_changes
|
|
||||||
let path = file_userdata.get::<String>("__path")?;
|
|
||||||
let path_buf = PathBuf::from(path);
|
|
||||||
fs_changes.lock().insert(path_buf, content_vec.clone());
|
|
||||||
|
|
||||||
Ok(true)
|
|
||||||
})?
|
|
||||||
};
|
|
||||||
file.set("write", write_fn)?;
|
|
||||||
|
|
||||||
// If we got this far, the file was opened successfully
|
|
||||||
Ok((Some(file), String::new()))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn search(
|
|
||||||
lua: &Lua,
|
|
||||||
foreground_tx: &mut mpsc::Sender<ForegroundFn>,
|
|
||||||
fs: Arc<dyn Fs>,
|
|
||||||
regex: String,
|
|
||||||
) -> anyhow::Result<Table> {
|
|
||||||
// TODO: Allow specification of these options.
|
|
||||||
let search_query = SearchQuery::regex(
|
|
||||||
®ex,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
PathMatcher::default(),
|
|
||||||
PathMatcher::default(),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
let search_query = match search_query {
|
|
||||||
Ok(query) => query,
|
|
||||||
Err(e) => return Err(anyhow!("Invalid search query: {}", e)),
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Should use `search_query.regex`. The tool description should also be updated,
|
|
||||||
// as it specifies standard regex.
|
|
||||||
let search_regex = match Regex::new(®ex) {
|
|
||||||
Ok(re) => re,
|
|
||||||
Err(e) => return Err(anyhow!("Invalid regex: {}", e)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut abs_paths_rx = Self::find_search_candidates(search_query, foreground_tx).await?;
|
|
||||||
|
|
||||||
let mut search_results: Vec<Table> = Vec::new();
|
|
||||||
while let Some(path) = abs_paths_rx.next().await {
|
|
||||||
// Skip files larger than 1MB
|
|
||||||
if let Ok(Some(metadata)) = fs.metadata(&path).await {
|
|
||||||
if metadata.len > 1_000_000 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to read the file as text
|
|
||||||
if let Ok(content) = fs.load(&path).await {
|
|
||||||
let mut matches = Vec::new();
|
|
||||||
|
|
||||||
// Find all regex matches in the content
|
|
||||||
for capture in search_regex.find_iter(&content) {
|
|
||||||
matches.push(capture.as_str().to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we found matches, create a result entry
|
|
||||||
if !matches.is_empty() {
|
|
||||||
let result_entry = lua.create_table()?;
|
|
||||||
result_entry.set("path", path.to_string_lossy().to_string())?;
|
|
||||||
|
|
||||||
let matches_table = lua.create_table()?;
|
|
||||||
for (ix, m) in matches.iter().enumerate() {
|
|
||||||
matches_table.set(ix + 1, m.clone())?;
|
|
||||||
}
|
|
||||||
result_entry.set("matches", matches_table)?;
|
|
||||||
|
|
||||||
search_results.push(result_entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a table to hold our results
|
|
||||||
let results_table = lua.create_table()?;
|
|
||||||
for (ix, entry) in search_results.into_iter().enumerate() {
|
|
||||||
results_table.set(ix + 1, entry)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(results_table)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_search_candidates(
|
|
||||||
search_query: SearchQuery,
|
|
||||||
foreground_tx: &mut mpsc::Sender<ForegroundFn>,
|
|
||||||
) -> anyhow::Result<mpsc::UnboundedReceiver<PathBuf>> {
|
|
||||||
Self::run_foreground_fn(
|
|
||||||
"finding search file candidates",
|
|
||||||
foreground_tx,
|
|
||||||
Box::new(move |session, mut cx| {
|
|
||||||
session.update(&mut cx, |session, cx| {
|
|
||||||
session.project.update(cx, |project, cx| {
|
|
||||||
project.worktree_store().update(cx, |worktree_store, cx| {
|
|
||||||
// TODO: Better limit? For now this is the same as
|
|
||||||
// MAX_SEARCH_RESULT_FILES.
|
|
||||||
let limit = 5000;
|
|
||||||
// TODO: Providing non-empty open_entries can make this a bit more
|
|
||||||
// efficient as it can skip checking that these paths are textual.
|
|
||||||
let open_entries = HashSet::default();
|
|
||||||
let candidates = worktree_store.find_search_candidates(
|
|
||||||
search_query,
|
|
||||||
limit,
|
|
||||||
open_entries,
|
|
||||||
project.fs().clone(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
let (abs_paths_tx, abs_paths_rx) = mpsc::unbounded();
|
|
||||||
cx.spawn(|worktree_store, cx| async move {
|
|
||||||
pin_mut!(candidates);
|
|
||||||
|
|
||||||
while let Some(project_path) = candidates.next().await {
|
|
||||||
worktree_store.read_with(&cx, |worktree_store, cx| {
|
|
||||||
if let Some(worktree) = worktree_store
|
|
||||||
.worktree_for_id(project_path.worktree_id, cx)
|
|
||||||
{
|
|
||||||
if let Some(abs_path) = worktree
|
|
||||||
.read(cx)
|
|
||||||
.absolutize(&project_path.path)
|
|
||||||
.log_err()
|
|
||||||
{
|
|
||||||
abs_paths_tx.unbounded_send(abs_path)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
anyhow::Ok(())
|
|
||||||
})??;
|
|
||||||
}
|
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
abs_paths_rx
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn outline(
|
|
||||||
root_dir: Option<Arc<Path>>,
|
|
||||||
foreground_tx: &mut mpsc::Sender<ForegroundFn>,
|
|
||||||
path_str: String,
|
|
||||||
) -> anyhow::Result<String> {
|
|
||||||
let root_dir = root_dir
|
|
||||||
.ok_or_else(|| mlua::Error::runtime("cannot get outline without a root directory"))?;
|
|
||||||
let path = Self::parse_abs_path_in_root_dir(&root_dir, &path_str)?;
|
|
||||||
let outline = Self::run_foreground_fn(
|
|
||||||
"getting code outline",
|
|
||||||
foreground_tx,
|
|
||||||
Box::new(move |session, cx| {
|
|
||||||
cx.spawn(move |mut cx| async move {
|
|
||||||
// TODO: This will not use file content from `fs_changes`. It will also reflect
|
|
||||||
// user changes that have not been saved.
|
|
||||||
let buffer = session
|
|
||||||
.update(&mut cx, |session, cx| {
|
|
||||||
session
|
|
||||||
.project
|
|
||||||
.update(cx, |project, cx| project.open_local_buffer(&path, cx))
|
|
||||||
})?
|
|
||||||
.await?;
|
|
||||||
buffer.update(&mut cx, |buffer, _cx| {
|
|
||||||
if let Some(outline) = buffer.snapshot().outline(None) {
|
|
||||||
Ok(outline)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("No outline for file {path_str}"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(outline
|
|
||||||
.items
|
|
||||||
.into_iter()
|
|
||||||
.map(|item| {
|
|
||||||
if item.text.contains('\n') {
|
|
||||||
log::error!("Outline item unexpectedly contains newline");
|
|
||||||
}
|
|
||||||
format!("{}{}", " ".repeat(item.depth), item.text)
|
|
||||||
})
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_foreground_fn<R: Send + 'static>(
|
|
||||||
description: &str,
|
|
||||||
foreground_tx: &mut mpsc::Sender<ForegroundFn>,
|
|
||||||
function: Box<dyn FnOnce(WeakEntity<Self>, AsyncApp) -> R + Send>,
|
|
||||||
) -> anyhow::Result<R> {
|
|
||||||
let (response_tx, response_rx) = oneshot::channel();
|
|
||||||
let send_result = foreground_tx
|
|
||||||
.send(ForegroundFn(Box::new(move |this, cx| {
|
|
||||||
response_tx.send(function(this, cx)).ok();
|
|
||||||
})))
|
|
||||||
.await;
|
|
||||||
match send_result {
|
|
||||||
Ok(()) => (),
|
|
||||||
Err(err) => {
|
|
||||||
return Err(anyhow::Error::new(err).context(format!(
|
|
||||||
"Internal error while enqueuing work for {description}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match response_rx.await {
|
|
||||||
Ok(result) => Ok(result),
|
|
||||||
Err(oneshot::Canceled) => Err(anyhow!(
|
|
||||||
"Internal error: response oneshot was canceled while {description}."
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_abs_path_in_root_dir(root_dir: &Path, path_str: &str) -> anyhow::Result<PathBuf> {
|
|
||||||
let path = Path::new(&path_str);
|
|
||||||
if path.is_absolute() {
|
|
||||||
// Check if path starts with root_dir prefix without resolving symlinks
|
|
||||||
if path.starts_with(&root_dir) {
|
|
||||||
Ok(path.to_path_buf())
|
|
||||||
} else {
|
|
||||||
Err(anyhow!(
|
|
||||||
"Error: Absolute path {} is outside the current working directory",
|
|
||||||
path_str
|
|
||||||
))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO: Does use of `../` break sandbox - is path canonicalization needed?
|
|
||||||
Ok(root_dir.join(path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FileContent(RefCell<Vec<u8>>);
|
|
||||||
|
|
||||||
impl UserData for FileContent {
|
|
||||||
fn add_methods<M: UserDataMethods<Self>>(_methods: &mut M) {
|
|
||||||
// FileContent doesn't have any methods so far.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ScriptEvent {
|
|
||||||
Spawned(ScriptId),
|
|
||||||
Exited(ScriptId),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<ScriptEvent> for ScriptSession {}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ScriptId(u32);
|
|
||||||
|
|
||||||
pub struct Script {
|
|
||||||
pub id: ScriptId,
|
|
||||||
pub state: ScriptState,
|
|
||||||
pub source: SharedString,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ScriptState {
|
|
||||||
Generating,
|
|
||||||
Running {
|
|
||||||
stdout: Arc<Mutex<String>>,
|
|
||||||
},
|
|
||||||
Succeeded {
|
|
||||||
stdout: String,
|
|
||||||
},
|
|
||||||
Failed {
|
|
||||||
stdout: String,
|
|
||||||
error: anyhow::Error,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Script {
|
|
||||||
pub fn source_tag(&self) -> String {
|
|
||||||
format!("{}{}{}", SCRIPT_START_TAG, self.source, SCRIPT_END_TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If exited, returns a message with the output for the LLM
|
|
||||||
pub fn output_message_for_llm(&self) -> Option<String> {
|
|
||||||
match &self.state {
|
|
||||||
ScriptState::Generating { .. } => None,
|
|
||||||
ScriptState::Running { .. } => None,
|
|
||||||
ScriptState::Succeeded { stdout } => {
|
|
||||||
format!("Here's the script output:\n{}", stdout).into()
|
|
||||||
}
|
|
||||||
ScriptState::Failed { stdout, error } => format!(
|
|
||||||
"The script failed with:\n{}\n\nHere's the output it managed to print:\n{}",
|
|
||||||
error, stdout
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a snapshot of the script's stdout
|
|
||||||
pub fn stdout_snapshot(&self) -> String {
|
|
||||||
match &self.state {
|
|
||||||
ScriptState::Generating { .. } => String::new(),
|
|
||||||
ScriptState::Running { stdout } => stdout.lock().clone(),
|
|
||||||
ScriptState::Succeeded { stdout } => stdout.clone(),
|
|
||||||
ScriptState::Failed { stdout, .. } => stdout.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the error if the script failed, otherwise None
|
|
||||||
pub fn error(&self) -> Option<&anyhow::Error> {
|
|
||||||
match &self.state {
|
|
||||||
ScriptState::Generating { .. } => None,
|
|
||||||
ScriptState::Running { .. } => None,
|
|
||||||
ScriptState::Succeeded { .. } => None,
|
|
||||||
ScriptState::Failed { error, .. } => Some(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use gpui::TestAppContext;
|
|
||||||
use project::FakeFs;
|
|
||||||
use serde_json::json;
|
|
||||||
use settings::SettingsStore;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_print(cx: &mut TestAppContext) {
|
|
||||||
let script = r#"
|
|
||||||
print("Hello", "world!")
|
|
||||||
print("Goodbye", "moon!")
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let output = test_script(script, cx).await.unwrap();
|
|
||||||
assert_eq!(output, "Hello\tworld!\nGoodbye\tmoon!\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_search(cx: &mut TestAppContext) {
|
|
||||||
let script = r#"
|
|
||||||
local results = search("world")
|
|
||||||
for i, result in ipairs(results) do
|
|
||||||
print("File: " .. result.path)
|
|
||||||
print("Matches:")
|
|
||||||
for j, match in ipairs(result.matches) do
|
|
||||||
print(" " .. match)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let output = test_script(script, cx).await.unwrap();
|
|
||||||
assert_eq!(output, "File: /file1.txt\nMatches:\n world\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn test_script(source: &str, cx: &mut TestAppContext) -> anyhow::Result<String> {
|
|
||||||
init_test(cx);
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
fs.insert_tree(
|
|
||||||
"/",
|
|
||||||
json!({
|
|
||||||
"file1.txt": "Hello world!",
|
|
||||||
"file2.txt": "Goodbye moon!"
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let project = Project::test(fs, [Path::new("/")], cx).await;
|
|
||||||
let session = cx.new(|cx| ScriptSession::new(project, cx));
|
|
||||||
|
|
||||||
let (script_id, task) = session.update(cx, |session, cx| {
|
|
||||||
let script_id = session.new_script();
|
|
||||||
let task = session.run_script(script_id, source.to_string(), cx);
|
|
||||||
|
|
||||||
(script_id, task)
|
|
||||||
});
|
|
||||||
|
|
||||||
task.await?;
|
|
||||||
|
|
||||||
Ok(session.read_with(cx, |session, _cx| session.get(script_id).stdout_snapshot()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_test(cx: &mut TestAppContext) {
|
|
||||||
let settings_store = cx.update(SettingsStore::test);
|
|
||||||
cx.set_global(settings_store);
|
|
||||||
cx.update(Project::init_settings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
pub const SCRIPT_START_TAG: &str = "<eval type=\"lua\">";
|
|
||||||
pub const SCRIPT_END_TAG: &str = "</eval>";
|
|
||||||
|
|
||||||
const START_TAG: &[u8] = SCRIPT_START_TAG.as_bytes();
|
|
||||||
const END_TAG: &[u8] = SCRIPT_END_TAG.as_bytes();
|
|
||||||
|
|
||||||
/// Parses a script tag in an assistant message as it is being streamed.
|
|
||||||
pub struct ScriptTagParser {
|
|
||||||
state: State,
|
|
||||||
buffer: Vec<u8>,
|
|
||||||
tag_match_ix: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum State {
|
|
||||||
Unstarted,
|
|
||||||
Streaming,
|
|
||||||
Ended,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
pub struct ChunkOutput {
|
|
||||||
/// The chunk with script tags removed.
|
|
||||||
pub content: String,
|
|
||||||
/// The full script tag content. `None` until closed.
|
|
||||||
pub script_source: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScriptTagParser {
|
|
||||||
/// Create a new script tag parser.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
state: State::Unstarted,
|
|
||||||
buffer: Vec::new(),
|
|
||||||
tag_match_ix: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the parser has found a script tag.
|
|
||||||
pub fn found_script(&self) -> bool {
|
|
||||||
match self.state {
|
|
||||||
State::Unstarted => false,
|
|
||||||
State::Streaming | State::Ended => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process a new chunk of input, splitting it into surrounding content and script source.
|
|
||||||
pub fn parse_chunk(&mut self, input: &str) -> ChunkOutput {
|
|
||||||
let mut content = Vec::with_capacity(input.len());
|
|
||||||
|
|
||||||
for byte in input.bytes() {
|
|
||||||
match self.state {
|
|
||||||
State::Unstarted => {
|
|
||||||
if collect_until_tag(byte, START_TAG, &mut self.tag_match_ix, &mut content) {
|
|
||||||
self.state = State::Streaming;
|
|
||||||
self.buffer = Vec::with_capacity(1024);
|
|
||||||
self.tag_match_ix = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
State::Streaming => {
|
|
||||||
if collect_until_tag(byte, END_TAG, &mut self.tag_match_ix, &mut self.buffer) {
|
|
||||||
self.state = State::Ended;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
State::Ended => content.push(byte),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = unsafe { String::from_utf8_unchecked(content) };
|
|
||||||
|
|
||||||
let script_source = if matches!(self.state, State::Ended) && !self.buffer.is_empty() {
|
|
||||||
let source = unsafe { String::from_utf8_unchecked(std::mem::take(&mut self.buffer)) };
|
|
||||||
|
|
||||||
Some(source)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
ChunkOutput {
|
|
||||||
content,
|
|
||||||
script_source,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_until_tag(byte: u8, tag: &[u8], tag_match_ix: &mut usize, buffer: &mut Vec<u8>) -> bool {
|
|
||||||
// this can't be a method because it'd require a mutable borrow on both self and self.buffer
|
|
||||||
|
|
||||||
if match_tag_byte(byte, tag, tag_match_ix) {
|
|
||||||
*tag_match_ix >= tag.len()
|
|
||||||
} else {
|
|
||||||
if *tag_match_ix > 0 {
|
|
||||||
// push the partially matched tag to the buffer
|
|
||||||
buffer.extend_from_slice(&tag[..*tag_match_ix]);
|
|
||||||
*tag_match_ix = 0;
|
|
||||||
|
|
||||||
// the tag might start to match again
|
|
||||||
if match_tag_byte(byte, tag, tag_match_ix) {
|
|
||||||
return *tag_match_ix >= tag.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.push(byte);
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn match_tag_byte(byte: u8, tag: &[u8], tag_match_ix: &mut usize) -> bool {
|
|
||||||
if byte == tag[*tag_match_ix] {
|
|
||||||
*tag_match_ix += 1;
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_complete_tag() {
|
|
||||||
let mut parser = ScriptTagParser::new();
|
|
||||||
let input = "<eval type=\"lua\">print(\"Hello, World!\")</eval>";
|
|
||||||
let result = parser.parse_chunk(input);
|
|
||||||
assert_eq!(result.content, "");
|
|
||||||
assert_eq!(
|
|
||||||
result.script_source,
|
|
||||||
Some("print(\"Hello, World!\")".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_no_tag() {
|
|
||||||
let mut parser = ScriptTagParser::new();
|
|
||||||
let input = "No tags here, just plain text";
|
|
||||||
let result = parser.parse_chunk(input);
|
|
||||||
assert_eq!(result.content, "No tags here, just plain text");
|
|
||||||
assert_eq!(result.script_source, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_partial_end_tag() {
|
|
||||||
let mut parser = ScriptTagParser::new();
|
|
||||||
|
|
||||||
// Start the tag
|
|
||||||
let result = parser.parse_chunk("<eval type=\"lua\">let x = '</e");
|
|
||||||
assert_eq!(result.content, "");
|
|
||||||
assert_eq!(result.script_source, None);
|
|
||||||
|
|
||||||
// Finish with the rest
|
|
||||||
let result = parser.parse_chunk("val' + 'not the end';</eval>");
|
|
||||||
assert_eq!(result.content, "");
|
|
||||||
assert_eq!(
|
|
||||||
result.script_source,
|
|
||||||
Some("let x = '</eval' + 'not the end';".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_text_before_and_after_tag() {
|
|
||||||
let mut parser = ScriptTagParser::new();
|
|
||||||
let input = "Before tag <eval type=\"lua\">print(\"Hello\")</eval> After tag";
|
|
||||||
let result = parser.parse_chunk(input);
|
|
||||||
assert_eq!(result.content, "Before tag After tag");
|
|
||||||
assert_eq!(result.script_source, Some("print(\"Hello\")".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multiple_chunks_with_surrounding_text() {
|
|
||||||
let mut parser = ScriptTagParser::new();
|
|
||||||
|
|
||||||
// First chunk with text before
|
|
||||||
let result = parser.parse_chunk("Before script <eval type=\"lua\">local x = 10");
|
|
||||||
assert_eq!(result.content, "Before script ");
|
|
||||||
assert_eq!(result.script_source, None);
|
|
||||||
|
|
||||||
// Second chunk with script content
|
|
||||||
let result = parser.parse_chunk("\nlocal y = 20");
|
|
||||||
assert_eq!(result.content, "");
|
|
||||||
assert_eq!(result.script_source, None);
|
|
||||||
|
|
||||||
// Last chunk with text after
|
|
||||||
let result = parser.parse_chunk("\nprint(x + y)</eval> After script");
|
|
||||||
assert_eq!(result.content, " After script");
|
|
||||||
assert_eq!(
|
|
||||||
result.script_source,
|
|
||||||
Some("local x = 10\nlocal y = 20\nprint(x + y)".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = parser.parse_chunk(" there's more text");
|
|
||||||
assert_eq!(result.content, " there's more text");
|
|
||||||
assert_eq!(result.script_source, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_partial_start_tag_matching() {
|
|
||||||
let mut parser = ScriptTagParser::new();
|
|
||||||
|
|
||||||
// partial match of start tag...
|
|
||||||
let result = parser.parse_chunk("<ev");
|
|
||||||
assert_eq!(result.content, "");
|
|
||||||
|
|
||||||
// ...that's abandandoned when the < of a real tag is encountered
|
|
||||||
let result = parser.parse_chunk("<eval type=\"lua\">script content</eval>");
|
|
||||||
// ...so it gets pushed to content
|
|
||||||
assert_eq!(result.content, "<ev");
|
|
||||||
// ...and the real tag is parsed correctly
|
|
||||||
assert_eq!(result.script_source, Some("script content".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_random_chunked_parsing() {
|
|
||||||
use rand::rngs::StdRng;
|
|
||||||
use rand::{Rng, SeedableRng};
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
let test_inputs = [
|
|
||||||
"Before <eval type=\"lua\">print(\"Hello\")</eval> After",
|
|
||||||
"No tags here at all",
|
|
||||||
"<eval type=\"lua\">local x = 10\nlocal y = 20\nprint(x + y)</eval>",
|
|
||||||
"Text <eval type=\"lua\">if true then\nprint(\"nested </e\")\nend</eval> more",
|
|
||||||
];
|
|
||||||
|
|
||||||
let seed = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
eprintln!("Using random seed: {}", seed);
|
|
||||||
let mut rng = StdRng::seed_from_u64(seed);
|
|
||||||
|
|
||||||
for test_input in &test_inputs {
|
|
||||||
let mut reference_parser = ScriptTagParser::new();
|
|
||||||
let expected = reference_parser.parse_chunk(test_input);
|
|
||||||
|
|
||||||
let mut chunked_parser = ScriptTagParser::new();
|
|
||||||
let mut remaining = test_input.as_bytes();
|
|
||||||
let mut actual_content = String::new();
|
|
||||||
let mut actual_script = None;
|
|
||||||
|
|
||||||
while !remaining.is_empty() {
|
|
||||||
let chunk_size = rng.gen_range(1..=remaining.len().min(5));
|
|
||||||
let (chunk, rest) = remaining.split_at(chunk_size);
|
|
||||||
remaining = rest;
|
|
||||||
|
|
||||||
let chunk_str = std::str::from_utf8(chunk).unwrap();
|
|
||||||
let result = chunked_parser.parse_chunk(chunk_str);
|
|
||||||
|
|
||||||
actual_content.push_str(&result.content);
|
|
||||||
if result.script_source.is_some() {
|
|
||||||
actual_script = result.script_source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(actual_content, expected.content);
|
|
||||||
assert_eq!(actual_script, expected.script_source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -88,7 +88,6 @@ pub trait SlashCommand: 'static + Send + Sync {
|
|||||||
fn accepts_arguments(&self) -> bool {
|
fn accepts_arguments(&self) -> bool {
|
||||||
self.requires_argument()
|
self.requires_argument()
|
||||||
}
|
}
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ mod tool_working_set;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use gpui::{App, Entity, Task};
|
use gpui::{App, Entity, SharedString, Task};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
|
||||||
pub use crate::tool_registry::*;
|
pub use crate::tool_registry::*;
|
||||||
@@ -14,6 +14,14 @@ pub fn init(cx: &mut App) {
|
|||||||
ToolRegistry::default_global(cx);
|
ToolRegistry::default_global(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||||
|
pub enum ToolSource {
|
||||||
|
/// A native tool built-in to Zed.
|
||||||
|
Native,
|
||||||
|
/// A tool provided by a context server.
|
||||||
|
ContextServer { id: SharedString },
|
||||||
|
}
|
||||||
|
|
||||||
/// A tool that can be used by a language model.
|
/// A tool that can be used by a language model.
|
||||||
pub trait Tool: 'static + Send + Sync {
|
pub trait Tool: 'static + Send + Sync {
|
||||||
/// Returns the name of the tool.
|
/// Returns the name of the tool.
|
||||||
@@ -22,6 +30,11 @@ pub trait Tool: 'static + Send + Sync {
|
|||||||
/// Returns the description of the tool.
|
/// Returns the description of the tool.
|
||||||
fn description(&self) -> String;
|
fn description(&self) -> String;
|
||||||
|
|
||||||
|
/// Returns the source of the tool.
|
||||||
|
fn source(&self) -> ToolSource {
|
||||||
|
ToolSource::Native
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the JSON schema that describes the tool's input.
|
/// Returns the JSON schema that describes the tool's input.
|
||||||
fn input_schema(&self) -> serde_json::Value {
|
fn input_schema(&self) -> serde_json::Value {
|
||||||
serde_json::Value::Object(serde_json::Map::default())
|
serde_json::Value::Object(serde_json::Map::default())
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use collections::HashMap;
|
use collections::{HashMap, HashSet, IndexMap};
|
||||||
use gpui::App;
|
use gpui::App;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
use crate::{Tool, ToolRegistry};
|
use crate::{Tool, ToolRegistry, ToolSource};
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
|
||||||
pub struct ToolId(usize);
|
pub struct ToolId(usize);
|
||||||
@@ -19,6 +19,8 @@ pub struct ToolWorkingSet {
|
|||||||
struct WorkingSetState {
|
struct WorkingSetState {
|
||||||
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
|
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
|
||||||
context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
|
context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
|
||||||
|
disabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
|
||||||
|
is_scripting_tool_disabled: bool,
|
||||||
next_tool_id: ToolId,
|
next_tool_id: ToolId,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,24 +47,97 @@ impl ToolWorkingSet {
|
|||||||
tools
|
tools
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&self, command: Arc<dyn Tool>) -> ToolId {
|
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||||
|
let all_tools = self.tools(cx);
|
||||||
|
|
||||||
|
all_tools
|
||||||
|
.into_iter()
|
||||||
|
.filter(|tool| self.is_enabled(&tool.source(), &tool.name().into()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
|
||||||
|
let mut tools_by_source = IndexMap::default();
|
||||||
|
|
||||||
|
for tool in self.tools(cx) {
|
||||||
|
tools_by_source
|
||||||
|
.entry(tool.source())
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
for tools in tools_by_source.values_mut() {
|
||||||
|
tools.sort_by_key(|tool| tool.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
tools_by_source.sort_unstable_keys();
|
||||||
|
|
||||||
|
tools_by_source
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&self, tool: Arc<dyn Tool>) -> ToolId {
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
let command_id = state.next_tool_id;
|
let tool_id = state.next_tool_id;
|
||||||
state.next_tool_id.0 += 1;
|
state.next_tool_id.0 += 1;
|
||||||
state
|
state
|
||||||
.context_server_tools_by_id
|
.context_server_tools_by_id
|
||||||
.insert(command_id, command.clone());
|
.insert(tool_id, tool.clone());
|
||||||
state.tools_changed();
|
state.tools_changed();
|
||||||
command_id
|
tool_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&self, command_ids_to_remove: &[ToolId]) {
|
pub fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||||
|
!self.is_disabled(source, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||||
|
let state = self.state.lock();
|
||||||
|
state
|
||||||
|
.disabled_tools_by_source
|
||||||
|
.get(source)
|
||||||
|
.map_or(false, |disabled_tools| disabled_tools.contains(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable(&self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
|
||||||
|
let mut state = self.state.lock();
|
||||||
|
state
|
||||||
|
.disabled_tools_by_source
|
||||||
|
.entry(source)
|
||||||
|
.or_default()
|
||||||
|
.retain(|name| !tools_to_enable.contains(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable(&self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
|
||||||
|
let mut state = self.state.lock();
|
||||||
|
state
|
||||||
|
.disabled_tools_by_source
|
||||||
|
.entry(source)
|
||||||
|
.or_default()
|
||||||
|
.extend(tools_to_disable.into_iter().cloned());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&self, tool_ids_to_remove: &[ToolId]) {
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
state
|
state
|
||||||
.context_server_tools_by_id
|
.context_server_tools_by_id
|
||||||
.retain(|id, _| !command_ids_to_remove.contains(id));
|
.retain(|id, _| !tool_ids_to_remove.contains(id));
|
||||||
state.tools_changed();
|
state.tools_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_scripting_tool_enabled(&self) -> bool {
|
||||||
|
let state = self.state.lock();
|
||||||
|
!state.is_scripting_tool_disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_scripting_tool(&self) {
|
||||||
|
let mut state = self.state.lock();
|
||||||
|
state.is_scripting_tool_disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable_scripting_tool(&self) {
|
||||||
|
let mut state = self.state.lock();
|
||||||
|
state.is_scripting_tool_disabled = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkingSetState {
|
impl WorkingSetState {
|
||||||
@@ -71,7 +146,7 @@ impl WorkingSetState {
|
|||||||
self.context_server_tools_by_name.extend(
|
self.context_server_tools_by_name.extend(
|
||||||
self.context_server_tools_by_id
|
self.context_server_tools_by_id
|
||||||
.values()
|
.values()
|
||||||
.map(|command| (command.name(), command.clone())),
|
.map(|tool| (tool.name(), tool.clone())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -828,7 +828,6 @@ impl BufferDiff {
|
|||||||
Some(start..end)
|
Some(start..end)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub async fn update_diff(
|
pub async fn update_diff(
|
||||||
this: Entity<BufferDiff>,
|
this: Entity<BufferDiff>,
|
||||||
buffer: text::BufferSnapshot,
|
buffer: text::BufferSnapshot,
|
||||||
@@ -838,8 +837,8 @@ impl BufferDiff {
|
|||||||
language: Option<Arc<Language>>,
|
language: Option<Arc<Language>>,
|
||||||
language_registry: Option<Arc<LanguageRegistry>>,
|
language_registry: Option<Arc<LanguageRegistry>>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> anyhow::Result<Option<Range<Anchor>>> {
|
) -> anyhow::Result<BufferDiffSnapshot> {
|
||||||
let snapshot = if base_text_changed || language_changed {
|
let inner = if base_text_changed || language_changed {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
Self::build(
|
Self::build(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
@@ -861,18 +860,45 @@ impl BufferDiff {
|
|||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
};
|
};
|
||||||
|
Ok(BufferDiffSnapshot {
|
||||||
this.update(cx, |this, _| this.set_state(snapshot, &buffer))
|
inner,
|
||||||
|
secondary_diff: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_diff_from(
|
pub fn set_snapshot(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: &text::BufferSnapshot,
|
buffer: &text::BufferSnapshot,
|
||||||
other: &Entity<Self>,
|
new_snapshot: BufferDiffSnapshot,
|
||||||
|
language_changed: bool,
|
||||||
|
secondary_changed_range: Option<Range<Anchor>>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Option<Range<Anchor>> {
|
) -> Option<Range<Anchor>> {
|
||||||
let other = other.read(cx).inner.clone();
|
let changed_range = self.set_state(new_snapshot.inner, buffer);
|
||||||
self.set_state(other, buffer)
|
if language_changed {
|
||||||
|
cx.emit(BufferDiffEvent::LanguageChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
let changed_range = match (secondary_changed_range, changed_range) {
|
||||||
|
(None, None) => None,
|
||||||
|
(Some(unstaged_range), None) => self.range_to_hunk_range(unstaged_range, &buffer, cx),
|
||||||
|
(None, Some(uncommitted_range)) => Some(uncommitted_range),
|
||||||
|
(Some(unstaged_range), Some(uncommitted_range)) => {
|
||||||
|
let mut start = uncommitted_range.start;
|
||||||
|
let mut end = uncommitted_range.end;
|
||||||
|
if let Some(unstaged_range) = self.range_to_hunk_range(unstaged_range, &buffer, cx)
|
||||||
|
{
|
||||||
|
start = unstaged_range.start.min(&uncommitted_range.start, &buffer);
|
||||||
|
end = unstaged_range.end.max(&uncommitted_range.end, &buffer);
|
||||||
|
}
|
||||||
|
Some(start..end)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.emit(BufferDiffEvent::DiffChanged {
|
||||||
|
changed_range: changed_range.clone(),
|
||||||
|
});
|
||||||
|
changed_range
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_state(
|
fn set_state(
|
||||||
|
|||||||
@@ -229,7 +229,6 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new channel message.
|
/// Creates a new channel message.
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub async fn create_channel_message(
|
pub async fn create_channel_message(
|
||||||
&self,
|
&self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub async fn get_or_create_user_by_github_account_tx(
|
pub async fn get_or_create_user_by_github_account_tx(
|
||||||
&self,
|
&self,
|
||||||
github_login: &str,
|
github_login: &str,
|
||||||
|
|||||||
@@ -289,7 +289,6 @@ impl LlmDatabase {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub async fn record_usage(
|
pub async fn record_usage(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
@@ -554,7 +553,6 @@ impl LlmDatabase {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
async fn update_usage_for_measure(
|
async fn update_usage_for_measure(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ pub struct LlmTokenClaims {
|
|||||||
const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
|
const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
|
||||||
|
|
||||||
impl LlmTokenClaims {
|
impl LlmTokenClaims {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn create(
|
pub fn create(
|
||||||
user: &user::Model,
|
user: &user::Model,
|
||||||
is_staff: bool,
|
is_staff: bool,
|
||||||
|
|||||||
@@ -697,7 +697,6 @@ impl Server {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn handle_connection(
|
pub fn handle_connection(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
@@ -1081,7 +1080,6 @@ pub fn routes(server: Arc<Server>) -> Router<(), Body> {
|
|||||||
.layer(Extension(server))
|
.layer(Extension(server))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub async fn handle_websocket_request(
|
pub async fn handle_websocket_request(
|
||||||
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
|
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
|
||||||
app_version_header: Option<TypedHeader<AppVersionHeader>>,
|
app_version_header: Option<TypedHeader<AppVersionHeader>>,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ use crate::{
|
|||||||
tests::{rust_lang, TestServer},
|
tests::{rust_lang, TestServer},
|
||||||
};
|
};
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use collections::HashMap;
|
|
||||||
use editor::{
|
use editor::{
|
||||||
actions::{
|
actions::{
|
||||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename,
|
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename,
|
||||||
@@ -1983,7 +1982,6 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
|||||||
blame_entry("3a3a3a", 2..3),
|
blame_entry("3a3a3a", 2..3),
|
||||||
blame_entry("4c4c4c", 3..4),
|
blame_entry("4c4c4c", 3..4),
|
||||||
],
|
],
|
||||||
permalinks: HashMap::default(), // This field is deprecrated
|
|
||||||
messages: [
|
messages: [
|
||||||
("1b1b1b", "message for idx-0"),
|
("1b1b1b", "message for idx-0"),
|
||||||
("0d0d0d", "message for idx-1"),
|
("0d0d0d", "message for idx-1"),
|
||||||
|
|||||||
@@ -6770,7 +6770,7 @@ async fn test_remote_git_branches(
|
|||||||
|
|
||||||
assert_eq!(branches_b, branches_set);
|
assert_eq!(branches_b, branches_set);
|
||||||
|
|
||||||
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch.to_string()))
|
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch))
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -6790,23 +6790,15 @@ async fn test_remote_git_branches(
|
|||||||
assert_eq!(host_branch.name, branches[2]);
|
assert_eq!(host_branch.name, branches[2]);
|
||||||
|
|
||||||
// Also try creating a new branch
|
// Also try creating a new branch
|
||||||
cx_b.update(|cx| {
|
cx_b.update(|cx| repo_b.read(cx).create_branch("totally-new-branch"))
|
||||||
repo_b
|
.await
|
||||||
.read(cx)
|
.unwrap()
|
||||||
.create_branch("totally-new-branch".to_string())
|
.unwrap();
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
cx_b.update(|cx| {
|
cx_b.update(|cx| repo_b.read(cx).change_branch("totally-new-branch"))
|
||||||
repo_b
|
.await
|
||||||
.read(cx)
|
.unwrap()
|
||||||
.change_branch("totally-new-branch".to_string())
|
.unwrap();
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
|||||||
@@ -463,7 +463,6 @@ impl<T: RandomizedTest> TestPlan<T> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
async fn apply_server_operation(
|
async fn apply_server_operation(
|
||||||
plan: Arc<Mutex<Self>>,
|
plan: Arc<Mutex<Self>>,
|
||||||
deterministic: BackgroundExecutor,
|
deterministic: BackgroundExecutor,
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ async fn test_ssh_collaboration_git_branches(
|
|||||||
|
|
||||||
assert_eq!(&branches_b, &branches_set);
|
assert_eq!(&branches_b, &branches_set);
|
||||||
|
|
||||||
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch.to_string()))
|
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch))
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -316,23 +316,15 @@ async fn test_ssh_collaboration_git_branches(
|
|||||||
assert_eq!(server_branch.name, branches[2]);
|
assert_eq!(server_branch.name, branches[2]);
|
||||||
|
|
||||||
// Also try creating a new branch
|
// Also try creating a new branch
|
||||||
cx_b.update(|cx| {
|
cx_b.update(|cx| repo_b.read(cx).create_branch("totally-new-branch"))
|
||||||
repo_b
|
.await
|
||||||
.read(cx)
|
.unwrap()
|
||||||
.create_branch("totally-new-branch".to_string())
|
.unwrap();
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
cx_b.update(|cx| {
|
cx_b.update(|cx| repo_b.read(cx).change_branch("totally-new-branch"))
|
||||||
repo_b
|
.await
|
||||||
.read(cx)
|
.unwrap()
|
||||||
.change_branch("totally-new-branch".to_string())
|
.unwrap();
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
|||||||
@@ -869,7 +869,6 @@ impl CollabPanel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn render_participant_project(
|
fn render_participant_project(
|
||||||
&self,
|
&self,
|
||||||
project_id: u64,
|
project_id: u64,
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ default = []
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
collections.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
languages.workspace = true
|
languages.workspace = true
|
||||||
|
notifications.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
notifications.workspace = true
|
|
||||||
collections.workspace = true
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use gpui::{ListState, ScrollHandle, UniformListScrollHandle};
|
|||||||
use languages::LanguageRegistry;
|
use languages::LanguageRegistry;
|
||||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
use ui::{prelude::*, Divider, ListItem, ListSubHeader};
|
use ui::{prelude::*, Divider, ListItem, ListSubHeader};
|
||||||
|
|
||||||
use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId};
|
use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId};
|
||||||
@@ -62,6 +63,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
|||||||
|
|
||||||
enum PreviewEntry {
|
enum PreviewEntry {
|
||||||
AllComponents,
|
AllComponents,
|
||||||
|
AllIcons,
|
||||||
Separator,
|
Separator,
|
||||||
Component(ComponentMetadata),
|
Component(ComponentMetadata),
|
||||||
SectionHeader(SharedString),
|
SectionHeader(SharedString),
|
||||||
@@ -83,6 +85,7 @@ impl From<SharedString> for PreviewEntry {
|
|||||||
enum PreviewPage {
|
enum PreviewPage {
|
||||||
#[default]
|
#[default]
|
||||||
AllComponents,
|
AllComponents,
|
||||||
|
AllIcons,
|
||||||
Component(ComponentId),
|
Component(ComponentId),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +199,7 @@ impl ComponentPreview {
|
|||||||
|
|
||||||
// Always show all components first
|
// Always show all components first
|
||||||
entries.push(PreviewEntry::AllComponents);
|
entries.push(PreviewEntry::AllComponents);
|
||||||
|
entries.push(PreviewEntry::AllIcons);
|
||||||
entries.push(PreviewEntry::Separator);
|
entries.push(PreviewEntry::Separator);
|
||||||
|
|
||||||
for scope in known_scopes.iter() {
|
for scope in known_scopes.iter() {
|
||||||
@@ -245,6 +249,20 @@ impl ComponentPreview {
|
|||||||
entry: &PreviewEntry,
|
entry: &PreviewEntry,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
|
let custom_entry = |ix: usize, label: &str, page: PreviewPage| {
|
||||||
|
let selected = self.active_page == page.clone();
|
||||||
|
|
||||||
|
ListItem::new(ix)
|
||||||
|
.child(Label::new(label.to_string()).color(Color::Default))
|
||||||
|
.selectable(true)
|
||||||
|
.toggle_state(selected)
|
||||||
|
.inset(true)
|
||||||
|
.on_click(cx.listener(move |this, _, _, cx| {
|
||||||
|
this.set_active_page(page.clone(), cx);
|
||||||
|
}))
|
||||||
|
.into_any_element()
|
||||||
|
};
|
||||||
|
|
||||||
match entry {
|
match entry {
|
||||||
PreviewEntry::Component(component_metadata) => {
|
PreviewEntry::Component(component_metadata) => {
|
||||||
let id = component_metadata.id();
|
let id = component_metadata.id();
|
||||||
@@ -264,18 +282,9 @@ impl ComponentPreview {
|
|||||||
.inset(true)
|
.inset(true)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
PreviewEntry::AllComponents => {
|
PreviewEntry::AllComponents => {
|
||||||
let selected = self.active_page == PreviewPage::AllComponents;
|
custom_entry(ix, "All Components", PreviewPage::AllComponents)
|
||||||
|
|
||||||
ListItem::new(ix)
|
|
||||||
.child(Label::new("All Components").color(Color::Default))
|
|
||||||
.selectable(true)
|
|
||||||
.toggle_state(selected)
|
|
||||||
.inset(true)
|
|
||||||
.on_click(cx.listener(move |this, _, _, cx| {
|
|
||||||
this.set_active_page(PreviewPage::AllComponents, cx);
|
|
||||||
}))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
PreviewEntry::AllIcons => custom_entry(ix, "All Icons", PreviewPage::AllIcons),
|
||||||
PreviewEntry::Separator => ListItem::new(ix)
|
PreviewEntry::Separator => ListItem::new(ix)
|
||||||
.child(h_flex().pt_3().child(Divider::horizontal_dashed()))
|
.child(h_flex().pt_3().child(Divider::horizontal_dashed()))
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
@@ -303,6 +312,7 @@ impl ComponentPreview {
|
|||||||
.render_scope_header(ix, shared_string.clone(), window, cx)
|
.render_scope_header(ix, shared_string.clone(), window, cx)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
|
PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
|
||||||
|
PreviewEntry::AllIcons => div().w_full().h_0().into_any_element(),
|
||||||
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
|
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -417,25 +427,48 @@ impl ComponentPreview {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_status_toast(&self, window: &mut Window, cx: &mut Context<Self>) {
|
fn test_status_toast(&self, cx: &mut Context<Self>) {
|
||||||
if let Some(workspace) = self.workspace.upgrade() {
|
if let Some(workspace) = self.workspace.upgrade() {
|
||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
let status_toast = StatusToast::new(
|
let status_toast =
|
||||||
"`zed/new-notification-system` created!",
|
StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
|this, _, cx| {
|
|
||||||
this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
|
this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
|
||||||
.action(
|
.action("Open Pull Request", |_, cx| {
|
||||||
"Open Pull Request",
|
cx.open_url("https://github.com/")
|
||||||
cx.listener(|_, _, _, cx| cx.open_url("https://github.com/")),
|
})
|
||||||
)
|
});
|
||||||
},
|
workspace.toggle_status_toast(status_toast, cx)
|
||||||
);
|
|
||||||
workspace.toggle_status_toast(window, cx, status_toast)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_icon_preview(&self, icon: IconName, cx: &App) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.flex_none()
|
||||||
|
.size_10()
|
||||||
|
.rounded_md()
|
||||||
|
.bg(cx.theme().colors().surface_background)
|
||||||
|
.child(Icon::new(icon))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_all_icons(&self, cx: &App) -> impl IntoElement {
|
||||||
|
let all_icons = IconName::iter().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.p_8()
|
||||||
|
.flex()
|
||||||
|
.items_start()
|
||||||
|
.justify_start()
|
||||||
|
.flex_wrap()
|
||||||
|
.gap_1()
|
||||||
|
.children(
|
||||||
|
all_icons
|
||||||
|
.into_iter()
|
||||||
|
.map(|icon| self.render_icon_preview(icon, cx)),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for ComponentPreview {
|
impl Render for ComponentPreview {
|
||||||
@@ -478,8 +511,8 @@ impl Render for ComponentPreview {
|
|||||||
div().w_full().pb_4().child(
|
div().w_full().pb_4().child(
|
||||||
Button::new("toast-test", "Launch Toast")
|
Button::new("toast-test", "Launch Toast")
|
||||||
.on_click(cx.listener({
|
.on_click(cx.listener({
|
||||||
move |this, _, window, cx| {
|
move |this, _, _window, cx| {
|
||||||
this.test_status_toast(window, cx);
|
this.test_status_toast(cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -489,6 +522,7 @@ impl Render for ComponentPreview {
|
|||||||
)
|
)
|
||||||
.child(match active_page {
|
.child(match active_page {
|
||||||
PreviewPage::AllComponents => self.render_all_components().into_any_element(),
|
PreviewPage::AllComponents => self.render_all_components().into_any_element(),
|
||||||
|
PreviewPage::AllIcons => self.render_all_icons(cx).into_any_element(),
|
||||||
PreviewPage::Component(id) => self
|
PreviewPage::Component(id) => self
|
||||||
.render_component_page(&id, window, cx)
|
.render_component_page(&id, window, cx)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use assistant_tool::Tool;
|
use assistant_tool::{Tool, ToolSource};
|
||||||
use gpui::{App, Entity, Task};
|
use gpui::{App, Entity, Task};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
|
||||||
@@ -37,6 +37,12 @@ impl Tool for ContextServerTool {
|
|||||||
self.tool.description.clone().unwrap_or_default()
|
self.tool.description.clone().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn source(&self) -> ToolSource {
|
||||||
|
ToolSource::ContextServer {
|
||||||
|
id: self.server_id.clone().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn input_schema(&self) -> serde_json::Value {
|
fn input_schema(&self) -> serde_json::Value {
|
||||||
match &self.tool.input_schema {
|
match &self.tool.input_schema {
|
||||||
serde_json::Value::Null => {
|
serde_json::Value::Null => {
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ pub struct DisplayMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayMap {
|
impl DisplayMap {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
buffer: Entity<MultiBuffer>,
|
buffer: Entity<MultiBuffer>,
|
||||||
font: Font,
|
font: Font,
|
||||||
|
|||||||
@@ -726,7 +726,6 @@ impl BlockMap {
|
|||||||
self.show_excerpt_controls
|
self.show_excerpt_controls
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn header_and_footer_blocks<'a, R, T>(
|
fn header_and_footer_blocks<'a, R, T>(
|
||||||
show_excerpt_controls: bool,
|
show_excerpt_controls: bool,
|
||||||
excerpt_footer_height: u32,
|
excerpt_footer_height: u32,
|
||||||
|
|||||||
@@ -5931,7 +5931,6 @@ impl Editor {
|
|||||||
const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.);
|
const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.);
|
||||||
const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.);
|
const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.);
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn render_edit_prediction_popover(
|
fn render_edit_prediction_popover(
|
||||||
&mut self,
|
&mut self,
|
||||||
text_bounds: &Bounds<Pixels>,
|
text_bounds: &Bounds<Pixels>,
|
||||||
@@ -6043,7 +6042,6 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn render_edit_prediction_modifier_jump_popover(
|
fn render_edit_prediction_modifier_jump_popover(
|
||||||
&mut self,
|
&mut self,
|
||||||
text_bounds: &Bounds<Pixels>,
|
text_bounds: &Bounds<Pixels>,
|
||||||
@@ -6139,7 +6137,6 @@ impl Editor {
|
|||||||
Some((element, origin))
|
Some((element, origin))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn render_edit_prediction_scroll_popover(
|
fn render_edit_prediction_scroll_popover(
|
||||||
&mut self,
|
&mut self,
|
||||||
to_y: impl Fn(Size<Pixels>) -> Pixels,
|
to_y: impl Fn(Size<Pixels>) -> Pixels,
|
||||||
@@ -6170,7 +6167,6 @@ impl Editor {
|
|||||||
Some((element, origin))
|
Some((element, origin))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn render_edit_prediction_eager_jump_popover(
|
fn render_edit_prediction_eager_jump_popover(
|
||||||
&mut self,
|
&mut self,
|
||||||
text_bounds: &Bounds<Pixels>,
|
text_bounds: &Bounds<Pixels>,
|
||||||
@@ -6240,7 +6236,6 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn render_edit_prediction_end_of_line_popover(
|
fn render_edit_prediction_end_of_line_popover(
|
||||||
self: &mut Editor,
|
self: &mut Editor,
|
||||||
label: &'static str,
|
label: &'static str,
|
||||||
@@ -6299,7 +6294,6 @@ impl Editor {
|
|||||||
Some((element, origin))
|
Some((element, origin))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn render_edit_prediction_diff_popover(
|
fn render_edit_prediction_diff_popover(
|
||||||
self: &Editor,
|
self: &Editor,
|
||||||
text_bounds: &Bounds<Pixels>,
|
text_bounds: &Bounds<Pixels>,
|
||||||
@@ -6607,7 +6601,6 @@ impl Editor {
|
|||||||
editor_bg_color.blend(accent_color.opacity(0.6))
|
editor_bg_color.blend(accent_color.opacity(0.6))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn render_edit_prediction_cursor_popover(
|
fn render_edit_prediction_cursor_popover(
|
||||||
&self,
|
&self,
|
||||||
min_width: Pixels,
|
min_width: Pixels,
|
||||||
@@ -13862,7 +13855,37 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
||||||
self.go_to_next_hunk(&GoToHunk, window, cx);
|
let snapshot = self.snapshot(window, cx);
|
||||||
|
let position = self.selections.newest::<Point>(cx).head();
|
||||||
|
let mut row = snapshot
|
||||||
|
.buffer_snapshot
|
||||||
|
.diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point())
|
||||||
|
.find(|hunk| hunk.row_range.start.0 > position.row)
|
||||||
|
.map(|hunk| hunk.row_range.start);
|
||||||
|
|
||||||
|
let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded();
|
||||||
|
// Outside of the project diff editor, wrap around to the beginning.
|
||||||
|
if !all_diff_hunks_expanded {
|
||||||
|
row = row.or_else(|| {
|
||||||
|
snapshot
|
||||||
|
.buffer_snapshot
|
||||||
|
.diff_hunks_in_range(Point::zero()..position)
|
||||||
|
.find(|hunk| hunk.row_range.end.0 < position.row)
|
||||||
|
.map(|hunk| hunk.row_range.start)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(row) = row {
|
||||||
|
let destination = Point::new(row.0, 0);
|
||||||
|
let autoscroll = Autoscroll::center();
|
||||||
|
|
||||||
|
self.unfold_ranges(&[destination..destination], false, false, cx);
|
||||||
|
self.change_selections(Some(autoscroll), window, cx, |s| {
|
||||||
|
s.select_ranges([destination..destination]);
|
||||||
|
});
|
||||||
|
} else if all_diff_hunks_expanded {
|
||||||
|
window.dispatch_action(::git::ExpandCommitEditor.boxed_clone(), cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_stage_or_unstage(
|
fn do_stage_or_unstage(
|
||||||
|
|||||||
@@ -958,7 +958,6 @@ impl EditorElement {
|
|||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_selections(
|
fn layout_selections(
|
||||||
&self,
|
&self,
|
||||||
start_anchor: Anchor,
|
start_anchor: Anchor,
|
||||||
@@ -1130,7 +1129,6 @@ impl EditorElement {
|
|||||||
cursors
|
cursors
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_visible_cursors(
|
fn layout_visible_cursors(
|
||||||
&self,
|
&self,
|
||||||
snapshot: &EditorSnapshot,
|
snapshot: &EditorSnapshot,
|
||||||
@@ -1484,7 +1482,6 @@ impl EditorElement {
|
|||||||
axis_pair(horizontal_scrollbar, vertical_scrollbar)
|
axis_pair(horizontal_scrollbar, vertical_scrollbar)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn prepaint_crease_toggles(
|
fn prepaint_crease_toggles(
|
||||||
&self,
|
&self,
|
||||||
crease_toggles: &mut [Option<AnyElement>],
|
crease_toggles: &mut [Option<AnyElement>],
|
||||||
@@ -1519,7 +1516,6 @@ impl EditorElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn prepaint_crease_trailers(
|
fn prepaint_crease_trailers(
|
||||||
&self,
|
&self,
|
||||||
trailers: Vec<Option<AnyElement>>,
|
trailers: Vec<Option<AnyElement>>,
|
||||||
@@ -1596,7 +1592,6 @@ impl EditorElement {
|
|||||||
display_hunks
|
display_hunks
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_inline_diagnostics(
|
fn layout_inline_diagnostics(
|
||||||
&self,
|
&self,
|
||||||
line_layouts: &[LineWithInvisibles],
|
line_layouts: &[LineWithInvisibles],
|
||||||
@@ -1747,7 +1742,6 @@ impl EditorElement {
|
|||||||
elements
|
elements
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_inline_blame(
|
fn layout_inline_blame(
|
||||||
&self,
|
&self,
|
||||||
display_row: DisplayRow,
|
display_row: DisplayRow,
|
||||||
@@ -1827,7 +1821,6 @@ impl EditorElement {
|
|||||||
Some(element)
|
Some(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_blame_entries(
|
fn layout_blame_entries(
|
||||||
&self,
|
&self,
|
||||||
buffer_rows: &[RowInfo],
|
buffer_rows: &[RowInfo],
|
||||||
@@ -1896,7 +1889,6 @@ impl EditorElement {
|
|||||||
Some(shaped_lines)
|
Some(shaped_lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_indent_guides(
|
fn layout_indent_guides(
|
||||||
&self,
|
&self,
|
||||||
content_origin: gpui::Point<Pixels>,
|
content_origin: gpui::Point<Pixels>,
|
||||||
@@ -2014,7 +2006,6 @@ impl EditorElement {
|
|||||||
(offset_y, length)
|
(offset_y, length)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_run_indicators(
|
fn layout_run_indicators(
|
||||||
&self,
|
&self,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
@@ -2108,7 +2099,6 @@ impl EditorElement {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_code_actions_indicator(
|
fn layout_code_actions_indicator(
|
||||||
&self,
|
&self,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
@@ -2207,7 +2197,6 @@ impl EditorElement {
|
|||||||
relative_rows
|
relative_rows
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_line_numbers(
|
fn layout_line_numbers(
|
||||||
&self,
|
&self,
|
||||||
gutter_hitbox: Option<&Hitbox>,
|
gutter_hitbox: Option<&Hitbox>,
|
||||||
@@ -2423,7 +2412,6 @@ impl EditorElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn prepaint_lines(
|
fn prepaint_lines(
|
||||||
&self,
|
&self,
|
||||||
start_row: DisplayRow,
|
start_row: DisplayRow,
|
||||||
@@ -2450,7 +2438,6 @@ impl EditorElement {
|
|||||||
line_elements
|
line_elements
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn render_block(
|
fn render_block(
|
||||||
&self,
|
&self,
|
||||||
block: &Block,
|
block: &Block,
|
||||||
@@ -2950,7 +2937,6 @@ impl EditorElement {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn render_blocks(
|
fn render_blocks(
|
||||||
&self,
|
&self,
|
||||||
rows: Range<DisplayRow>,
|
rows: Range<DisplayRow>,
|
||||||
@@ -3135,7 +3121,6 @@ impl EditorElement {
|
|||||||
|
|
||||||
/// Returns true if any of the blocks changed size since the previous frame. This will trigger
|
/// Returns true if any of the blocks changed size since the previous frame. This will trigger
|
||||||
/// a restart of rendering for the editor based on the new sizes.
|
/// a restart of rendering for the editor based on the new sizes.
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_blocks(
|
fn layout_blocks(
|
||||||
&self,
|
&self,
|
||||||
blocks: &mut Vec<BlockLayout>,
|
blocks: &mut Vec<BlockLayout>,
|
||||||
@@ -3179,7 +3164,6 @@ impl EditorElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_sticky_buffer_header(
|
fn layout_sticky_buffer_header(
|
||||||
&self,
|
&self,
|
||||||
StickyHeaderExcerpt {
|
StickyHeaderExcerpt {
|
||||||
@@ -3254,7 +3238,6 @@ impl EditorElement {
|
|||||||
header
|
header
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_cursor_popovers(
|
fn layout_cursor_popovers(
|
||||||
&self,
|
&self,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
@@ -3443,7 +3426,6 @@ impl EditorElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_gutter_menu(
|
fn layout_gutter_menu(
|
||||||
&self,
|
&self,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
@@ -3496,7 +3478,6 @@ impl EditorElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_popovers_above_or_below_line(
|
fn layout_popovers_above_or_below_line(
|
||||||
&self,
|
&self,
|
||||||
target_position: gpui::Point<Pixels>,
|
target_position: gpui::Point<Pixels>,
|
||||||
@@ -3610,7 +3591,6 @@ impl EditorElement {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_context_menu_aside(
|
fn layout_context_menu_aside(
|
||||||
&self,
|
&self,
|
||||||
y_flipped: bool,
|
y_flipped: bool,
|
||||||
@@ -3806,7 +3786,6 @@ impl EditorElement {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_hover_popovers(
|
fn layout_hover_popovers(
|
||||||
&self,
|
&self,
|
||||||
snapshot: &EditorSnapshot,
|
snapshot: &EditorSnapshot,
|
||||||
@@ -3923,7 +3902,6 @@ impl EditorElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_diff_hunk_controls(
|
fn layout_diff_hunk_controls(
|
||||||
&self,
|
&self,
|
||||||
row_range: Range<DisplayRow>,
|
row_range: Range<DisplayRow>,
|
||||||
@@ -4008,7 +3986,6 @@ impl EditorElement {
|
|||||||
controls
|
controls
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn layout_signature_help(
|
fn layout_signature_help(
|
||||||
&self,
|
&self,
|
||||||
hitbox: &Hitbox,
|
hitbox: &Hitbox,
|
||||||
@@ -4676,6 +4653,7 @@ impl EditorElement {
|
|||||||
};
|
};
|
||||||
window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
|
window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
|
||||||
|
|
||||||
|
self.paint_lines_background(layout, window, cx);
|
||||||
let invisible_display_ranges = self.paint_highlights(layout, window);
|
let invisible_display_ranges = self.paint_highlights(layout, window);
|
||||||
self.paint_lines(&invisible_display_ranges, layout, window, cx);
|
self.paint_lines(&invisible_display_ranges, layout, window, cx);
|
||||||
self.paint_redactions(layout, window);
|
self.paint_redactions(layout, window);
|
||||||
@@ -4766,6 +4744,18 @@ impl EditorElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paint_lines_background(
|
||||||
|
&mut self,
|
||||||
|
layout: &mut EditorLayout,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
|
||||||
|
let row = DisplayRow(layout.visible_display_row_range.start.0 + ix as u32);
|
||||||
|
line_with_invisibles.draw_background(layout, row, layout.content_origin, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn paint_redactions(&mut self, layout: &EditorLayout, window: &mut Window) {
|
fn paint_redactions(&mut self, layout: &EditorLayout, window: &mut Window) {
|
||||||
if layout.redacted_ranges.is_empty() {
|
if layout.redacted_ranges.is_empty() {
|
||||||
return;
|
return;
|
||||||
@@ -5304,7 +5294,6 @@ impl EditorElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn paint_highlighted_range(
|
fn paint_highlighted_range(
|
||||||
&self,
|
&self,
|
||||||
range: Range<DisplayPoint>,
|
range: Range<DisplayPoint>,
|
||||||
@@ -5730,7 +5719,6 @@ impl AcceptEditPredictionBinding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn prepaint_gutter_button(
|
fn prepaint_gutter_button(
|
||||||
button: IconButton,
|
button: IconButton,
|
||||||
row: DisplayRow,
|
row: DisplayRow,
|
||||||
@@ -5981,7 +5969,6 @@ impl fmt::Debug for LineFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LineWithInvisibles {
|
impl LineWithInvisibles {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn from_chunks<'a>(
|
fn from_chunks<'a>(
|
||||||
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
|
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
|
||||||
editor_style: &EditorStyle,
|
editor_style: &EditorStyle,
|
||||||
@@ -6186,7 +6173,6 @@ impl LineWithInvisibles {
|
|||||||
layouts
|
layouts
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn prepaint(
|
fn prepaint(
|
||||||
&mut self,
|
&mut self,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
@@ -6221,7 +6207,6 @@ impl LineWithInvisibles {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
layout: &EditorLayout,
|
layout: &EditorLayout,
|
||||||
@@ -6265,7 +6250,35 @@ impl LineWithInvisibles {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
fn draw_background(
|
||||||
|
&self,
|
||||||
|
layout: &EditorLayout,
|
||||||
|
row: DisplayRow,
|
||||||
|
content_origin: gpui::Point<Pixels>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let line_height = layout.position_map.line_height;
|
||||||
|
let line_y = line_height
|
||||||
|
* (row.as_f32() - layout.position_map.scroll_pixel_position.y / line_height);
|
||||||
|
|
||||||
|
let mut fragment_origin =
|
||||||
|
content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y);
|
||||||
|
|
||||||
|
for fragment in &self.fragments {
|
||||||
|
match fragment {
|
||||||
|
LineFragment::Text(line) => {
|
||||||
|
line.paint_background(fragment_origin, line_height, window, cx)
|
||||||
|
.log_err();
|
||||||
|
fragment_origin.x += line.width;
|
||||||
|
}
|
||||||
|
LineFragment::Element { size, .. } => {
|
||||||
|
fragment_origin.x += size.width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_invisibles(
|
fn draw_invisibles(
|
||||||
&self,
|
&self,
|
||||||
selection_ranges: &[Range<DisplayPoint>],
|
selection_ranges: &[Range<DisplayPoint>],
|
||||||
@@ -7659,7 +7672,6 @@ struct ScrollbarRangeData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ScrollbarRangeData {
|
impl ScrollbarRangeData {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
scrollbar_bounds: Bounds<Pixels>,
|
scrollbar_bounds: Bounds<Pixels>,
|
||||||
letter_size: Size<Pixels>,
|
letter_size: Size<Pixels>,
|
||||||
|
|||||||
@@ -370,7 +370,6 @@ impl GitBlame {
|
|||||||
async move {
|
async move {
|
||||||
let Some(Blame {
|
let Some(Blame {
|
||||||
entries,
|
entries,
|
||||||
permalinks,
|
|
||||||
messages,
|
messages,
|
||||||
remote_url,
|
remote_url,
|
||||||
}) = blame.await?
|
}) = blame.await?
|
||||||
@@ -379,13 +378,8 @@ impl GitBlame {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
|
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
|
||||||
let commit_details = parse_commit_messages(
|
let commit_details =
|
||||||
messages,
|
parse_commit_messages(messages, remote_url, provider_registry).await;
|
||||||
remote_url,
|
|
||||||
&permalinks,
|
|
||||||
provider_registry,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
anyhow::Ok(Some((entries, commit_details)))
|
anyhow::Ok(Some((entries, commit_details)))
|
||||||
}
|
}
|
||||||
@@ -477,7 +471,6 @@ fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree
|
|||||||
async fn parse_commit_messages(
|
async fn parse_commit_messages(
|
||||||
messages: impl IntoIterator<Item = (Oid, String)>,
|
messages: impl IntoIterator<Item = (Oid, String)>,
|
||||||
remote_url: Option<String>,
|
remote_url: Option<String>,
|
||||||
deprecated_permalinks: &HashMap<Oid, Url>,
|
|
||||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||||
) -> HashMap<Oid, ParsedCommitMessage> {
|
) -> HashMap<Oid, ParsedCommitMessage> {
|
||||||
let mut commit_details = HashMap::default();
|
let mut commit_details = HashMap::default();
|
||||||
@@ -495,11 +488,7 @@ async fn parse_commit_messages(
|
|||||||
},
|
},
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
|
continue;
|
||||||
// now do the parsing. This is here for backwards compatibility, so that
|
|
||||||
// when an old peer sends a client no `parsed_remote_url` but `deprecated_permalinks`,
|
|
||||||
// we fall back to that.
|
|
||||||
deprecated_permalinks.get(&oid).cloned()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let remote = parsed_remote_url
|
let remote = parsed_remote_url
|
||||||
|
|||||||
@@ -223,7 +223,6 @@ impl ScrollManager {
|
|||||||
self.anchor.scroll_position(snapshot)
|
self.anchor.scroll_position(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn set_scroll_position(
|
fn set_scroll_position(
|
||||||
&mut self,
|
&mut self,
|
||||||
scroll_position: gpui::Point<f32>,
|
scroll_position: gpui::Point<f32>,
|
||||||
@@ -298,7 +297,6 @@ impl ScrollManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn set_anchor(
|
fn set_anchor(
|
||||||
&mut self,
|
&mut self,
|
||||||
anchor: ScrollAnchor,
|
anchor: ScrollAnchor,
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ collections.workspace = true
|
|||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
feature_flags.workspace = true
|
feature_flags.workspace = true
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
git.workspace = true
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
http_client.workspace = true
|
http_client.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use client::{Client, UserStore};
|
|||||||
use clock::RealSystemClock;
|
use clock::RealSystemClock;
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
use feature_flags::FeatureFlagAppExt as _;
|
use feature_flags::FeatureFlagAppExt as _;
|
||||||
use git::GitHostingProviderRegistry;
|
|
||||||
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Entity};
|
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Entity};
|
||||||
use http_client::{HttpClient, Method};
|
use http_client::{HttpClient, Method};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
@@ -274,8 +273,7 @@ async fn run_evaluation(
|
|||||||
let repos_dir = Path::new(EVAL_REPOS_DIR);
|
let repos_dir = Path::new(EVAL_REPOS_DIR);
|
||||||
let db_path = Path::new(EVAL_DB_PATH);
|
let db_path = Path::new(EVAL_DB_PATH);
|
||||||
let api_key = std::env::var("OPENAI_API_KEY").unwrap();
|
let api_key = std::env::var("OPENAI_API_KEY").unwrap();
|
||||||
let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
|
let fs = Arc::new(RealFs::new(None)) as Arc<dyn Fs>;
|
||||||
let fs = Arc::new(RealFs::new(git_hosting_provider_registry, None)) as Arc<dyn Fs>;
|
|
||||||
let clock = Arc::new(RealSystemClock);
|
let clock = Arc::new(RealSystemClock);
|
||||||
let client = cx
|
let client = cx
|
||||||
.update(|cx| {
|
.update(|cx| {
|
||||||
@@ -399,7 +397,6 @@ async fn run_evaluation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
async fn run_eval_project(
|
async fn run_eval_project(
|
||||||
evaluation_project: EvaluationProject,
|
evaluation_project: EvaluationProject,
|
||||||
user_store: &Entity<UserStore>,
|
user_store: &Entity<UserStore>,
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ repository = "https://github.com/zed-industries/zed"
|
|||||||
documentation = "https://docs.rs/zed_extension_api"
|
documentation = "https://docs.rs/zed_extension_api"
|
||||||
keywords = ["zed", "extension"]
|
keywords = ["zed", "extension"]
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
# Change back to `true` when we're ready to publish v0.3.0.
|
publish = true
|
||||||
publish = false
|
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ need to set your `crate-type` accordingly:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
zed_extension_api = "0.1.0"
|
zed_extension_api = "0.3.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
@@ -63,6 +63,7 @@ Here is the compatibility of the `zed_extension_api` with versions of Zed:
|
|||||||
|
|
||||||
| Zed version | `zed_extension_api` version |
|
| Zed version | `zed_extension_api` version |
|
||||||
| ----------- | --------------------------- |
|
| ----------- | --------------------------- |
|
||||||
|
| `0.178.x` | `0.0.1` - `0.3.0` |
|
||||||
| `0.162.x` | `0.0.1` - `0.2.0` |
|
| `0.162.x` | `0.0.1` - `0.2.0` |
|
||||||
| `0.149.x` | `0.0.1` - `0.1.0` |
|
| `0.149.x` | `0.0.1` - `0.1.0` |
|
||||||
| `0.131.x` | `0.0.1` - `0.0.6` |
|
| `0.131.x` | `0.0.1` - `0.0.6` |
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ static mut EXTENSION: Option<Box<dyn Extension>> = None;
|
|||||||
pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
|
pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
|
||||||
|
|
||||||
mod wit {
|
mod wit {
|
||||||
#![allow(clippy::too_many_arguments, clippy::missing_safety_doc)]
|
|
||||||
|
|
||||||
wit_bindgen::generate!({
|
wit_bindgen::generate!({
|
||||||
skip: ["init-extension"],
|
skip: ["init-extension"],
|
||||||
|
|||||||
@@ -218,7 +218,6 @@ impl ExtensionStore {
|
|||||||
cx.global::<GlobalExtensionStore>().0.clone()
|
cx.global::<GlobalExtensionStore>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
extensions_dir: PathBuf,
|
extensions_dir: PathBuf,
|
||||||
build_dir: Option<PathBuf>,
|
build_dir: Option<PathBuf>,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive
|
|||||||
|
|
||||||
let max_version = match release_channel {
|
let max_version = match release_channel {
|
||||||
ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION,
|
ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION,
|
||||||
ReleaseChannel::Stable | ReleaseChannel::Preview => since_v0_2_0::MAX_VERSION,
|
ReleaseChannel::Stable | ReleaseChannel::Preview => latest::MAX_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
since_v0_0_1::MIN_VERSION..=max_version
|
since_v0_0_1::MIN_VERSION..=max_version
|
||||||
@@ -108,8 +108,6 @@ impl Extension {
|
|||||||
let _ = release_channel;
|
let _ = release_channel;
|
||||||
|
|
||||||
if version >= latest::MIN_VERSION {
|
if version >= latest::MIN_VERSION {
|
||||||
authorize_access_to_unreleased_wasm_api_version(release_channel)?;
|
|
||||||
|
|
||||||
let extension =
|
let extension =
|
||||||
latest::Extension::instantiate_async(store, component, latest::linker())
|
latest::Extension::instantiate_async(store, component, latest::linker())
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use wasmtime::component::{Linker, Resource};
|
|||||||
use super::latest;
|
use super::latest;
|
||||||
|
|
||||||
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 2, 0);
|
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 2, 0);
|
||||||
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 2, 0);
|
|
||||||
|
|
||||||
wasmtime::component::bindgen!({
|
wasmtime::component::bindgen!({
|
||||||
async: true,
|
async: true,
|
||||||
|
|||||||
@@ -80,11 +80,6 @@ impl FeatureFlag for PredictEditsNonEagerModeFeatureFlag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GitUiFeatureFlag;
|
|
||||||
impl FeatureFlag for GitUiFeatureFlag {
|
|
||||||
const NAME: &'static str = "git-ui";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Remoting {}
|
pub struct Remoting {}
|
||||||
impl FeatureFlag for Remoting {
|
impl FeatureFlag for Remoting {
|
||||||
const NAME: &'static str = "remoting";
|
const NAME: &'static str = "remoting";
|
||||||
|
|||||||
@@ -653,7 +653,6 @@ impl FileSearchQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileFinderDelegate {
|
impl FileFinderDelegate {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn new(
|
fn new(
|
||||||
file_finder: WeakEntity<FileFinder>,
|
file_finder: WeakEntity<FileFinder>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
|||||||
@@ -436,8 +436,8 @@ impl PickerDelegate for NewPathDelegate {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
|
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||||
"Type a path...".into()
|
Some("Type a path...".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||||
|
|||||||
@@ -347,12 +347,14 @@ impl PickerDelegate for OpenPathDelegate {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
|
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||||
if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone()) {
|
let text = if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone())
|
||||||
|
{
|
||||||
error
|
error
|
||||||
} else {
|
} else {
|
||||||
"No such file or directory".into()
|
"No such file or directory".into()
|
||||||
}
|
};
|
||||||
|
Some(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use collections::HashMap;
|
|||||||
use git::status::StatusCode;
|
use git::status::StatusCode;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use git::status::TrackedStatus;
|
use git::status::TrackedStatus;
|
||||||
use git::GitHostingProviderRegistry;
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use git::{repository::RepoPath, status::FileStatus};
|
use git::{repository::RepoPath, status::FileStatus};
|
||||||
|
|
||||||
@@ -247,7 +246,6 @@ impl From<MTime> for proto::Timestamp {
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct RealFs {
|
pub struct RealFs {
|
||||||
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
|
||||||
git_binary_path: Option<PathBuf>,
|
git_binary_path: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,14 +298,8 @@ impl FileHandle for std::fs::File {
|
|||||||
pub struct RealWatcher {}
|
pub struct RealWatcher {}
|
||||||
|
|
||||||
impl RealFs {
|
impl RealFs {
|
||||||
pub fn new(
|
pub fn new(git_binary_path: Option<PathBuf>) -> Self {
|
||||||
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
Self { git_binary_path }
|
||||||
git_binary_path: Option<PathBuf>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
git_hosting_provider_registry,
|
|
||||||
git_binary_path,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,7 +762,6 @@ impl Fs for RealFs {
|
|||||||
Some(Arc::new(RealGitRepository::new(
|
Some(Arc::new(RealGitRepository::new(
|
||||||
repo,
|
repo,
|
||||||
self.git_binary_path.clone(),
|
self.git_binary_path.clone(),
|
||||||
self.git_hosting_provider_registry.clone(),
|
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,7 +170,6 @@ impl<'a> Matcher<'a> {
|
|||||||
score
|
score
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn recursive_score_match(
|
fn recursive_score_match(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: &[char],
|
path: &[char],
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
use crate::commit::get_messages;
|
use crate::commit::get_messages;
|
||||||
use crate::{parse_git_remote_url, BuildCommitPermalinkParams, GitHostingProviderRegistry, Oid};
|
use crate::Oid;
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::{ops::Range, path::Path};
|
use std::{ops::Range, path::Path};
|
||||||
use text::Rope;
|
use text::Rope;
|
||||||
use time::macros::format_description;
|
use time::macros::format_description;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use time::UtcOffset;
|
use time::UtcOffset;
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
pub use git2 as libgit;
|
pub use git2 as libgit;
|
||||||
|
|
||||||
@@ -19,7 +17,6 @@ pub use git2 as libgit;
|
|||||||
pub struct Blame {
|
pub struct Blame {
|
||||||
pub entries: Vec<BlameEntry>,
|
pub entries: Vec<BlameEntry>,
|
||||||
pub messages: HashMap<Oid, String>,
|
pub messages: HashMap<Oid, String>,
|
||||||
pub permalinks: HashMap<Oid, Url>,
|
|
||||||
pub remote_url: Option<String>,
|
pub remote_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,32 +27,15 @@ impl Blame {
|
|||||||
path: &Path,
|
path: &Path,
|
||||||
content: &Rope,
|
content: &Rope,
|
||||||
remote_url: Option<String>,
|
remote_url: Option<String>,
|
||||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let output = run_git_blame(git_binary, working_directory, path, content)?;
|
let output = run_git_blame(git_binary, working_directory, path, content)?;
|
||||||
let mut entries = parse_git_blame(&output)?;
|
let mut entries = parse_git_blame(&output)?;
|
||||||
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start));
|
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start));
|
||||||
|
|
||||||
let mut permalinks = HashMap::default();
|
|
||||||
let mut unique_shas = HashSet::default();
|
let mut unique_shas = HashSet::default();
|
||||||
let parsed_remote_url = remote_url
|
|
||||||
.as_deref()
|
|
||||||
.and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
|
|
||||||
|
|
||||||
for entry in entries.iter_mut() {
|
for entry in entries.iter_mut() {
|
||||||
unique_shas.insert(entry.sha);
|
unique_shas.insert(entry.sha);
|
||||||
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
|
|
||||||
// now do the parsing.
|
|
||||||
if let Some((provider, remote)) = parsed_remote_url.as_ref() {
|
|
||||||
permalinks.entry(entry.sha).or_insert_with(|| {
|
|
||||||
provider.build_commit_permalink(
|
|
||||||
remote,
|
|
||||||
BuildCommitPermalinkParams {
|
|
||||||
sha: entry.sha.to_string().as_str(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let shas = unique_shas.into_iter().collect::<Vec<_>>();
|
let shas = unique_shas.into_iter().collect::<Vec<_>>();
|
||||||
@@ -64,7 +44,6 @@ impl Blame {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
entries,
|
entries,
|
||||||
permalinks,
|
|
||||||
messages,
|
messages,
|
||||||
remote_url,
|
remote_url,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::status::FileStatus;
|
use crate::status::FileStatus;
|
||||||
use crate::GitHostingProviderRegistry;
|
use crate::SHORT_SHA_LENGTH;
|
||||||
use crate::{blame::Blame, status::GitStatus};
|
use crate::{blame::Blame, status::GitStatus};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use askpass::{AskPassResult, AskPassSession};
|
use askpass::{AskPassResult, AskPassSession};
|
||||||
@@ -57,6 +57,14 @@ pub struct Upstream {
|
|||||||
pub tracking: UpstreamTracking,
|
pub tracking: UpstreamTracking,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Upstream {
|
||||||
|
pub fn remote_name(&self) -> Option<&str> {
|
||||||
|
self.ref_name
|
||||||
|
.strip_prefix("refs/remotes/")
|
||||||
|
.and_then(|stripped| stripped.split("/").next())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
pub enum UpstreamTracking {
|
pub enum UpstreamTracking {
|
||||||
/// Remote ref not present in local repository.
|
/// Remote ref not present in local repository.
|
||||||
@@ -84,7 +92,7 @@ impl UpstreamTracking {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RemoteCommandOutput {
|
pub struct RemoteCommandOutput {
|
||||||
pub stdout: String,
|
pub stdout: String,
|
||||||
pub stderr: String,
|
pub stderr: String,
|
||||||
@@ -120,6 +128,12 @@ pub struct CommitDetails {
|
|||||||
pub committer_name: SharedString,
|
pub committer_name: SharedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CommitDetails {
|
||||||
|
pub fn short_sha(&self) -> SharedString {
|
||||||
|
self.sha[..SHORT_SHA_LENGTH].to_string().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
pub struct Remote {
|
pub struct Remote {
|
||||||
pub name: SharedString,
|
pub name: SharedString,
|
||||||
@@ -149,7 +163,12 @@ pub trait GitRepository: Send + Sync {
|
|||||||
/// Also returns `None` for symlinks.
|
/// Also returns `None` for symlinks.
|
||||||
fn load_committed_text(&self, path: &RepoPath) -> Option<String>;
|
fn load_committed_text(&self, path: &RepoPath) -> Option<String>;
|
||||||
|
|
||||||
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()>;
|
fn set_index_text(
|
||||||
|
&self,
|
||||||
|
path: &RepoPath,
|
||||||
|
content: Option<String>,
|
||||||
|
env: &HashMap<String, String>,
|
||||||
|
) -> anyhow::Result<()>;
|
||||||
|
|
||||||
/// Returns the URL of the remote with the given name.
|
/// Returns the URL of the remote with the given name.
|
||||||
fn remote_url(&self, name: &str) -> Option<String>;
|
fn remote_url(&self, name: &str) -> Option<String>;
|
||||||
@@ -167,8 +186,13 @@ pub trait GitRepository: Send + Sync {
|
|||||||
fn create_branch(&self, _: &str) -> Result<()>;
|
fn create_branch(&self, _: &str) -> Result<()>;
|
||||||
fn branch_exits(&self, _: &str) -> Result<bool>;
|
fn branch_exits(&self, _: &str) -> Result<bool>;
|
||||||
|
|
||||||
fn reset(&self, commit: &str, mode: ResetMode) -> Result<()>;
|
fn reset(&self, commit: &str, mode: ResetMode, env: &HashMap<String, String>) -> Result<()>;
|
||||||
fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()>;
|
fn checkout_files(
|
||||||
|
&self,
|
||||||
|
commit: &str,
|
||||||
|
paths: &[RepoPath],
|
||||||
|
env: &HashMap<String, String>,
|
||||||
|
) -> Result<()>;
|
||||||
|
|
||||||
fn show(&self, commit: &str) -> Result<CommitDetails>;
|
fn show(&self, commit: &str) -> Result<CommitDetails>;
|
||||||
|
|
||||||
@@ -189,13 +213,18 @@ pub trait GitRepository: Send + Sync {
|
|||||||
/// Updates the index to match the worktree at the given paths.
|
/// Updates the index to match the worktree at the given paths.
|
||||||
///
|
///
|
||||||
/// If any of the paths have been deleted from the worktree, they will be removed from the index if found there.
|
/// If any of the paths have been deleted from the worktree, they will be removed from the index if found there.
|
||||||
fn stage_paths(&self, paths: &[RepoPath]) -> Result<()>;
|
fn stage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()>;
|
||||||
/// Updates the index to match HEAD at the given paths.
|
/// Updates the index to match HEAD at the given paths.
|
||||||
///
|
///
|
||||||
/// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
|
/// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
|
||||||
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
|
fn unstage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()>;
|
||||||
|
|
||||||
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>;
|
fn commit(
|
||||||
|
&self,
|
||||||
|
message: &str,
|
||||||
|
name_and_email: Option<(&str, &str)>,
|
||||||
|
env: &HashMap<String, String>,
|
||||||
|
) -> Result<()>;
|
||||||
|
|
||||||
fn push(
|
fn push(
|
||||||
&self,
|
&self,
|
||||||
@@ -203,6 +232,7 @@ pub trait GitRepository: Send + Sync {
|
|||||||
upstream_name: &str,
|
upstream_name: &str,
|
||||||
options: Option<PushOptions>,
|
options: Option<PushOptions>,
|
||||||
askpass: AskPassSession,
|
askpass: AskPassSession,
|
||||||
|
env: &HashMap<String, String>,
|
||||||
) -> Result<RemoteCommandOutput>;
|
) -> Result<RemoteCommandOutput>;
|
||||||
|
|
||||||
fn pull(
|
fn pull(
|
||||||
@@ -210,8 +240,13 @@ pub trait GitRepository: Send + Sync {
|
|||||||
branch_name: &str,
|
branch_name: &str,
|
||||||
upstream_name: &str,
|
upstream_name: &str,
|
||||||
askpass: AskPassSession,
|
askpass: AskPassSession,
|
||||||
|
env: &HashMap<String, String>,
|
||||||
|
) -> Result<RemoteCommandOutput>;
|
||||||
|
fn fetch(
|
||||||
|
&self,
|
||||||
|
askpass: AskPassSession,
|
||||||
|
env: &HashMap<String, String>,
|
||||||
) -> Result<RemoteCommandOutput>;
|
) -> Result<RemoteCommandOutput>;
|
||||||
fn fetch(&self, askpass: AskPassSession) -> Result<RemoteCommandOutput>;
|
|
||||||
|
|
||||||
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
|
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
|
||||||
|
|
||||||
@@ -242,19 +277,13 @@ impl std::fmt::Debug for dyn GitRepository {
|
|||||||
pub struct RealGitRepository {
|
pub struct RealGitRepository {
|
||||||
pub repository: Mutex<git2::Repository>,
|
pub repository: Mutex<git2::Repository>,
|
||||||
pub git_binary_path: PathBuf,
|
pub git_binary_path: PathBuf,
|
||||||
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RealGitRepository {
|
impl RealGitRepository {
|
||||||
pub fn new(
|
pub fn new(repository: git2::Repository, git_binary_path: Option<PathBuf>) -> Self {
|
||||||
repository: git2::Repository,
|
|
||||||
git_binary_path: Option<PathBuf>,
|
|
||||||
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
repository: Mutex::new(repository),
|
repository: Mutex::new(repository),
|
||||||
git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
|
git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
|
||||||
hosting_provider_registry,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +337,7 @@ impl GitRepository for RealGitRepository {
|
|||||||
Ok(details)
|
Ok(details)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(&self, commit: &str, mode: ResetMode) -> Result<()> {
|
fn reset(&self, commit: &str, mode: ResetMode, env: &HashMap<String, String>) -> Result<()> {
|
||||||
let working_directory = self.working_directory()?;
|
let working_directory = self.working_directory()?;
|
||||||
|
|
||||||
let mode_flag = match mode {
|
let mode_flag = match mode {
|
||||||
@@ -317,6 +346,7 @@ impl GitRepository for RealGitRepository {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let output = new_std_command(&self.git_binary_path)
|
let output = new_std_command(&self.git_binary_path)
|
||||||
|
.envs(env)
|
||||||
.current_dir(&working_directory)
|
.current_dir(&working_directory)
|
||||||
.args(["reset", mode_flag, commit])
|
.args(["reset", mode_flag, commit])
|
||||||
.output()?;
|
.output()?;
|
||||||
@@ -329,7 +359,12 @@ impl GitRepository for RealGitRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()> {
|
fn checkout_files(
|
||||||
|
&self,
|
||||||
|
commit: &str,
|
||||||
|
paths: &[RepoPath],
|
||||||
|
env: &HashMap<String, String>,
|
||||||
|
) -> Result<()> {
|
||||||
if paths.is_empty() {
|
if paths.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -337,6 +372,7 @@ impl GitRepository for RealGitRepository {
|
|||||||
|
|
||||||
let output = new_std_command(&self.git_binary_path)
|
let output = new_std_command(&self.git_binary_path)
|
||||||
.current_dir(&working_directory)
|
.current_dir(&working_directory)
|
||||||
|
.envs(env)
|
||||||
.args(["checkout", commit, "--"])
|
.args(["checkout", commit, "--"])
|
||||||
.args(paths.iter().map(|path| path.as_ref()))
|
.args(paths.iter().map(|path| path.as_ref()))
|
||||||
.output()?;
|
.output()?;
|
||||||
@@ -385,11 +421,17 @@ impl GitRepository for RealGitRepository {
|
|||||||
Some(content)
|
Some(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
|
fn set_index_text(
|
||||||
|
&self,
|
||||||
|
path: &RepoPath,
|
||||||
|
content: Option<String>,
|
||||||
|
env: &HashMap<String, String>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
let working_directory = self.working_directory()?;
|
let working_directory = self.working_directory()?;
|
||||||
if let Some(content) = content {
|
if let Some(content) = content {
|
||||||
let mut child = new_std_command(&self.git_binary_path)
|
let mut child = new_std_command(&self.git_binary_path)
|
||||||
.current_dir(&working_directory)
|
.current_dir(&working_directory)
|
||||||
|
.envs(env)
|
||||||
.args(["hash-object", "-w", "--stdin"])
|
.args(["hash-object", "-w", "--stdin"])
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
@@ -402,6 +444,7 @@ impl GitRepository for RealGitRepository {
|
|||||||
|
|
||||||
let output = new_std_command(&self.git_binary_path)
|
let output = new_std_command(&self.git_binary_path)
|
||||||
.current_dir(&working_directory)
|
.current_dir(&working_directory)
|
||||||
|
.envs(env)
|
||||||
.args(["update-index", "--add", "--cacheinfo", "100644", &sha])
|
.args(["update-index", "--add", "--cacheinfo", "100644", &sha])
|
||||||
.arg(path.as_ref())
|
.arg(path.as_ref())
|
||||||
.output()?;
|
.output()?;
|
||||||
@@ -415,6 +458,7 @@ impl GitRepository for RealGitRepository {
|
|||||||
} else {
|
} else {
|
||||||
let output = new_std_command(&self.git_binary_path)
|
let output = new_std_command(&self.git_binary_path)
|
||||||
.current_dir(&working_directory)
|
.current_dir(&working_directory)
|
||||||
|
.envs(env)
|
||||||
.args(["update-index", "--force-remove"])
|
.args(["update-index", "--force-remove"])
|
||||||
.arg(path.as_ref())
|
.arg(path.as_ref())
|
||||||
.output()?;
|
.output()?;
|
||||||
@@ -581,7 +625,6 @@ impl GitRepository for RealGitRepository {
|
|||||||
path,
|
path,
|
||||||
&content,
|
&content,
|
||||||
remote_url,
|
remote_url,
|
||||||
self.hosting_provider_registry.clone(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,12 +650,13 @@ impl GitRepository for RealGitRepository {
|
|||||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
|
fn stage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()> {
|
||||||
let working_directory = self.working_directory()?;
|
let working_directory = self.working_directory()?;
|
||||||
|
|
||||||
if !paths.is_empty() {
|
if !paths.is_empty() {
|
||||||
let output = new_std_command(&self.git_binary_path)
|
let output = new_std_command(&self.git_binary_path)
|
||||||
.current_dir(&working_directory)
|
.current_dir(&working_directory)
|
||||||
|
.envs(env)
|
||||||
.args(["update-index", "--add", "--remove", "--"])
|
.args(["update-index", "--add", "--remove", "--"])
|
||||||
.args(paths.iter().map(|p| p.as_ref()))
|
.args(paths.iter().map(|p| p.as_ref()))
|
||||||
.output()?;
|
.output()?;
|
||||||
@@ -627,12 +671,13 @@ impl GitRepository for RealGitRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()> {
|
fn unstage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()> {
|
||||||
let working_directory = self.working_directory()?;
|
let working_directory = self.working_directory()?;
|
||||||
|
|
||||||
if !paths.is_empty() {
|
if !paths.is_empty() {
|
||||||
let output = new_std_command(&self.git_binary_path)
|
let output = new_std_command(&self.git_binary_path)
|
||||||
.current_dir(&working_directory)
|
.current_dir(&working_directory)
|
||||||
|
.envs(env)
|
||||||
.args(["reset", "--quiet", "--"])
|
.args(["reset", "--quiet", "--"])
|
||||||
.args(paths.iter().map(|p| p.as_ref()))
|
.args(paths.iter().map(|p| p.as_ref()))
|
||||||
.output()?;
|
.output()?;
|
||||||
@@ -647,11 +692,17 @@ impl GitRepository for RealGitRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
|
fn commit(
|
||||||
|
&self,
|
||||||
|
message: &str,
|
||||||
|
name_and_email: Option<(&str, &str)>,
|
||||||
|
env: &HashMap<String, String>,
|
||||||
|
) -> Result<()> {
|
||||||
let working_directory = self.working_directory()?;
|
let working_directory = self.working_directory()?;
|
||||||
|
|
||||||
let mut cmd = new_std_command(&self.git_binary_path);
|
let mut cmd = new_std_command(&self.git_binary_path);
|
||||||
cmd.current_dir(&working_directory)
|
cmd.current_dir(&working_directory)
|
||||||
|
.envs(env)
|
||||||
.args(["commit", "--quiet", "-m"])
|
.args(["commit", "--quiet", "-m"])
|
||||||
.arg(message)
|
.arg(message)
|
||||||
.arg("--cleanup=strip");
|
.arg("--cleanup=strip");
|
||||||
@@ -677,11 +728,13 @@ impl GitRepository for RealGitRepository {
|
|||||||
remote_name: &str,
|
remote_name: &str,
|
||||||
options: Option<PushOptions>,
|
options: Option<PushOptions>,
|
||||||
ask_pass: AskPassSession,
|
ask_pass: AskPassSession,
|
||||||
|
env: &HashMap<String, String>,
|
||||||
) -> Result<RemoteCommandOutput> {
|
) -> Result<RemoteCommandOutput> {
|
||||||
let working_directory = self.working_directory()?;
|
let working_directory = self.working_directory()?;
|
||||||
|
|
||||||
let mut command = new_smol_command("git");
|
let mut command = new_smol_command("git");
|
||||||
command
|
command
|
||||||
|
.envs(env)
|
||||||
.env("GIT_ASKPASS", ask_pass.script_path())
|
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||||
.env("SSH_ASKPASS", ask_pass.script_path())
|
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||||
@@ -705,11 +758,13 @@ impl GitRepository for RealGitRepository {
|
|||||||
branch_name: &str,
|
branch_name: &str,
|
||||||
remote_name: &str,
|
remote_name: &str,
|
||||||
ask_pass: AskPassSession,
|
ask_pass: AskPassSession,
|
||||||
|
env: &HashMap<String, String>,
|
||||||
) -> Result<RemoteCommandOutput> {
|
) -> Result<RemoteCommandOutput> {
|
||||||
let working_directory = self.working_directory()?;
|
let working_directory = self.working_directory()?;
|
||||||
|
|
||||||
let mut command = new_smol_command("git");
|
let mut command = new_smol_command("git");
|
||||||
command
|
command
|
||||||
|
.envs(env)
|
||||||
.env("GIT_ASKPASS", ask_pass.script_path())
|
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||||
.env("SSH_ASKPASS", ask_pass.script_path())
|
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||||
@@ -724,11 +779,16 @@ impl GitRepository for RealGitRepository {
|
|||||||
run_remote_command(ask_pass, git_process)
|
run_remote_command(ask_pass, git_process)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch(&self, ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
|
fn fetch(
|
||||||
|
&self,
|
||||||
|
ask_pass: AskPassSession,
|
||||||
|
env: &HashMap<String, String>,
|
||||||
|
) -> Result<RemoteCommandOutput> {
|
||||||
let working_directory = self.working_directory()?;
|
let working_directory = self.working_directory()?;
|
||||||
|
|
||||||
let mut command = new_smol_command("git");
|
let mut command = new_smol_command("git");
|
||||||
command
|
command
|
||||||
|
.envs(env)
|
||||||
.env("GIT_ASKPASS", ask_pass.script_path())
|
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||||
.env("SSH_ASKPASS", ask_pass.script_path())
|
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||||
@@ -852,7 +912,7 @@ fn run_remote_command(
|
|||||||
let output = output?;
|
let output = output?;
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
Err(anyhow!(
|
Err(anyhow!(
|
||||||
"Operation failed:\n{}",
|
"{}",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
@@ -919,7 +979,12 @@ impl GitRepository for FakeGitRepository {
|
|||||||
state.head_contents.get(path.as_ref()).cloned()
|
state.head_contents.get(path.as_ref()).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
|
fn set_index_text(
|
||||||
|
&self,
|
||||||
|
path: &RepoPath,
|
||||||
|
content: Option<String>,
|
||||||
|
_env: &HashMap<String, String>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
if let Some(message) = state.simulated_index_write_error_message.clone() {
|
if let Some(message) = state.simulated_index_write_error_message.clone() {
|
||||||
return Err(anyhow::anyhow!(message));
|
return Err(anyhow::anyhow!(message));
|
||||||
@@ -952,11 +1017,11 @@ impl GitRepository for FakeGitRepository {
|
|||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(&self, _: &str, _: ResetMode) -> Result<()> {
|
fn reset(&self, _: &str, _: ResetMode, _: &HashMap<String, String>) -> Result<()> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn checkout_files(&self, _: &str, _: &[RepoPath]) -> Result<()> {
|
fn checkout_files(&self, _: &str, _: &[RepoPath], _: &HashMap<String, String>) -> Result<()> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1042,15 +1107,20 @@ impl GitRepository for FakeGitRepository {
|
|||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
|
fn stage_paths(&self, _paths: &[RepoPath], _env: &HashMap<String, String>) -> Result<()> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unstage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
|
fn unstage_paths(&self, _paths: &[RepoPath], _env: &HashMap<String, String>) -> Result<()> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
|
fn commit(
|
||||||
|
&self,
|
||||||
|
_message: &str,
|
||||||
|
_name_and_email: Option<(&str, &str)>,
|
||||||
|
_env: &HashMap<String, String>,
|
||||||
|
) -> Result<()> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1060,6 +1130,7 @@ impl GitRepository for FakeGitRepository {
|
|||||||
_remote: &str,
|
_remote: &str,
|
||||||
_options: Option<PushOptions>,
|
_options: Option<PushOptions>,
|
||||||
_ask_pass: AskPassSession,
|
_ask_pass: AskPassSession,
|
||||||
|
_env: &HashMap<String, String>,
|
||||||
) -> Result<RemoteCommandOutput> {
|
) -> Result<RemoteCommandOutput> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
@@ -1069,11 +1140,16 @@ impl GitRepository for FakeGitRepository {
|
|||||||
_branch: &str,
|
_branch: &str,
|
||||||
_remote: &str,
|
_remote: &str,
|
||||||
_ask_pass: AskPassSession,
|
_ask_pass: AskPassSession,
|
||||||
|
_env: &HashMap<String, String>,
|
||||||
) -> Result<RemoteCommandOutput> {
|
) -> Result<RemoteCommandOutput> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch(&self, _ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
|
fn fetch(
|
||||||
|
&self,
|
||||||
|
_ask_pass: AskPassSession,
|
||||||
|
_env: &HashMap<String, String>,
|
||||||
|
) -> Result<RemoteCommandOutput> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,12 @@ test-support = ["multi_buffer/test-support"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
askpass.workspace= true
|
askpass.workspace = true
|
||||||
buffer_diff.workspace = true
|
buffer_diff.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
feature_flags.workspace = true
|
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
git.workspace = true
|
git.workspace = true
|
||||||
@@ -37,6 +36,7 @@ linkme.workspace = true
|
|||||||
log.workspace = true
|
log.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
multi_buffer.workspace = true
|
multi_buffer.workspace = true
|
||||||
|
notifications.workspace = true
|
||||||
panel.workspace = true
|
panel.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
@@ -51,6 +51,7 @@ strum.workspace = true
|
|||||||
telemetry.workspace = true
|
telemetry.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
time.workspace = true
|
time.workspace = true
|
||||||
|
time_format.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
use anyhow::{anyhow, Context as _};
|
use anyhow::{anyhow, Context as _};
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::StringMatchCandidate;
|
||||||
|
|
||||||
use git::repository::Branch;
|
use git::repository::Branch;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
rems, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
rems, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render,
|
||||||
Task, Window,
|
SharedString, Styled, Subscription, Task, Window,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::git::Repository;
|
use project::git::Repository;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle};
|
use time::OffsetDateTime;
|
||||||
|
use time_format::format_local_timestamp;
|
||||||
|
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::notifications::DetachAndPromptErr;
|
use workspace::notifications::DetachAndPromptErr;
|
||||||
use workspace::{ModalView, Workspace};
|
use workspace::{ModalView, Workspace};
|
||||||
@@ -51,7 +53,7 @@ pub fn open(
|
|||||||
let repository = workspace.project().read(cx).active_repository(cx).clone();
|
let repository = workspace.project().read(cx).active_repository(cx).clone();
|
||||||
let style = BranchListStyle::Modal;
|
let style = BranchListStyle::Modal;
|
||||||
workspace.toggle_modal(window, cx, |window, cx| {
|
workspace.toggle_modal(window, cx, |window, cx| {
|
||||||
BranchList::new(repository, style, 34., window, cx)
|
BranchList::new(repository, style, rems(34.), window, cx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +63,7 @@ pub fn popover(
|
|||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Entity<BranchList> {
|
) -> Entity<BranchList> {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let list = BranchList::new(repository, BranchListStyle::Popover, 15., window, cx);
|
let list = BranchList::new(repository, BranchListStyle::Popover, rems(20.), window, cx);
|
||||||
list.focus_handle(cx).focus(window);
|
list.focus_handle(cx).focus(window);
|
||||||
list
|
list
|
||||||
})
|
})
|
||||||
@@ -74,8 +76,7 @@ enum BranchListStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct BranchList {
|
pub struct BranchList {
|
||||||
rem_width: f32,
|
width: Rems,
|
||||||
pub popover_handle: PopoverMenuHandle<Self>,
|
|
||||||
pub picker: Entity<Picker<BranchListDelegate>>,
|
pub picker: Entity<Picker<BranchListDelegate>>,
|
||||||
_subscription: Subscription,
|
_subscription: Subscription,
|
||||||
}
|
}
|
||||||
@@ -84,20 +85,26 @@ impl BranchList {
|
|||||||
fn new(
|
fn new(
|
||||||
repository: Option<Entity<Repository>>,
|
repository: Option<Entity<Repository>>,
|
||||||
style: BranchListStyle,
|
style: BranchListStyle,
|
||||||
rem_width: f32,
|
width: Rems,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let popover_handle = PopoverMenuHandle::default();
|
|
||||||
let all_branches_request = repository
|
let all_branches_request = repository
|
||||||
.clone()
|
.clone()
|
||||||
.map(|repository| repository.read(cx).branches());
|
.map(|repository| repository.read(cx).branches());
|
||||||
|
|
||||||
cx.spawn_in(window, |this, mut cx| async move {
|
cx.spawn_in(window, |this, mut cx| async move {
|
||||||
let all_branches = all_branches_request
|
let mut all_branches = all_branches_request
|
||||||
.context("No active repository")?
|
.context("No active repository")?
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
|
all_branches.sort_by_key(|branch| {
|
||||||
|
branch
|
||||||
|
.most_recent_commit
|
||||||
|
.as_ref()
|
||||||
|
.map(|commit| 0 - commit.commit_timestamp)
|
||||||
|
});
|
||||||
|
|
||||||
this.update_in(&mut cx, |this, window, cx| {
|
this.update_in(&mut cx, |this, window, cx| {
|
||||||
this.picker.update(cx, |picker, cx| {
|
this.picker.update(cx, |picker, cx| {
|
||||||
picker.delegate.all_branches = Some(all_branches);
|
picker.delegate.all_branches = Some(all_branches);
|
||||||
@@ -109,7 +116,7 @@ impl BranchList {
|
|||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
let delegate = BranchListDelegate::new(repository.clone(), style, 20);
|
let delegate = BranchListDelegate::new(repository.clone(), style);
|
||||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||||
|
|
||||||
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
|
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
|
||||||
@@ -118,11 +125,20 @@ impl BranchList {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
picker,
|
picker,
|
||||||
rem_width,
|
width,
|
||||||
popover_handle,
|
|
||||||
_subscription,
|
_subscription,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_modifiers_changed(
|
||||||
|
&mut self,
|
||||||
|
ev: &ModifiersChangedEvent,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.picker
|
||||||
|
.update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl ModalView for BranchList {}
|
impl ModalView for BranchList {}
|
||||||
impl EventEmitter<DismissEvent> for BranchList {}
|
impl EventEmitter<DismissEvent> for BranchList {}
|
||||||
@@ -136,7 +152,8 @@ impl Focusable for BranchList {
|
|||||||
impl Render for BranchList {
|
impl Render for BranchList {
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
v_flex()
|
v_flex()
|
||||||
.w(rems(self.rem_width))
|
.w(self.width)
|
||||||
|
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
|
||||||
.child(self.picker.clone())
|
.child(self.picker.clone())
|
||||||
.on_mouse_down_out({
|
.on_mouse_down_out({
|
||||||
cx.listener(move |this, _, window, cx| {
|
cx.listener(move |this, _, window, cx| {
|
||||||
@@ -149,20 +166,10 @@ impl Render for BranchList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum BranchEntry {
|
struct BranchEntry {
|
||||||
Branch(StringMatch),
|
branch: Branch,
|
||||||
History(String),
|
positions: Vec<usize>,
|
||||||
NewBranch { name: String },
|
is_new: bool,
|
||||||
}
|
|
||||||
|
|
||||||
impl BranchEntry {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Branch(branch) => &branch.string,
|
|
||||||
Self::History(branch) => &branch,
|
|
||||||
Self::NewBranch { name } => &name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BranchListDelegate {
|
pub struct BranchListDelegate {
|
||||||
@@ -172,16 +179,11 @@ pub struct BranchListDelegate {
|
|||||||
style: BranchListStyle,
|
style: BranchListStyle,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
last_query: String,
|
last_query: String,
|
||||||
/// Max length of branch name before we truncate it and add a trailing `...`.
|
modifiers: Modifiers,
|
||||||
branch_name_trailoff_after: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BranchListDelegate {
|
impl BranchListDelegate {
|
||||||
fn new(
|
fn new(repo: Option<Entity<Repository>>, style: BranchListStyle) -> Self {
|
||||||
repo: Option<Entity<Repository>>,
|
|
||||||
style: BranchListStyle,
|
|
||||||
branch_name_trailoff_after: usize,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
matches: vec![],
|
matches: vec![],
|
||||||
repo,
|
repo,
|
||||||
@@ -189,15 +191,30 @@ impl BranchListDelegate {
|
|||||||
all_branches: None,
|
all_branches: None,
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
last_query: Default::default(),
|
last_query: Default::default(),
|
||||||
branch_name_trailoff_after,
|
modifiers: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn branch_count(&self) -> usize {
|
fn create_branch(
|
||||||
self.matches
|
&self,
|
||||||
.iter()
|
new_branch_name: SharedString,
|
||||||
.filter(|item| matches!(item, BranchEntry::Branch(_)))
|
window: &mut Window,
|
||||||
.count()
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) {
|
||||||
|
let Some(repo) = self.repo.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
cx.spawn(|_, cx| async move {
|
||||||
|
cx.update(|cx| repo.read(cx).create_branch(&new_branch_name))?
|
||||||
|
.await??;
|
||||||
|
cx.update(|cx| repo.read(cx).change_branch(&new_branch_name))?
|
||||||
|
.await??;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_prompt_err("Failed to create branch", window, cx, |e, _, _| {
|
||||||
|
Some(e.to_string())
|
||||||
|
});
|
||||||
|
cx.emit(DismissEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,37 +248,28 @@ impl PickerDelegate for BranchListDelegate {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Picker<Self>>,
|
cx: &mut Context<Picker<Self>>,
|
||||||
) -> Task<()> {
|
) -> Task<()> {
|
||||||
let Some(mut all_branches) = self.all_branches.clone() else {
|
let Some(all_branches) = self.all_branches.clone() else {
|
||||||
return Task::ready(());
|
return Task::ready(());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RECENT_BRANCHES_COUNT: usize = 10;
|
||||||
cx.spawn_in(window, move |picker, mut cx| async move {
|
cx.spawn_in(window, move |picker, mut cx| async move {
|
||||||
const RECENT_BRANCHES_COUNT: usize = 10;
|
let mut matches: Vec<BranchEntry> = if query.is_empty() {
|
||||||
if query.is_empty() {
|
all_branches
|
||||||
if all_branches.len() > RECENT_BRANCHES_COUNT {
|
|
||||||
// Truncate list of recent branches
|
|
||||||
// Do a partial sort to show recent-ish branches first.
|
|
||||||
all_branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
|
|
||||||
rhs.priority_key().cmp(&lhs.priority_key())
|
|
||||||
});
|
|
||||||
all_branches.truncate(RECENT_BRANCHES_COUNT);
|
|
||||||
}
|
|
||||||
all_branches.sort_unstable_by(|lhs, rhs| {
|
|
||||||
rhs.is_head.cmp(&lhs.is_head).then(lhs.name.cmp(&rhs.name))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let candidates = all_branches
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
|
|
||||||
.collect::<Vec<StringMatchCandidate>>();
|
|
||||||
let matches: Vec<BranchEntry> = if query.is_empty() {
|
|
||||||
candidates
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|candidate| BranchEntry::History(candidate.string))
|
.take(RECENT_BRANCHES_COUNT)
|
||||||
|
.map(|branch| BranchEntry {
|
||||||
|
branch,
|
||||||
|
positions: Vec::new(),
|
||||||
|
is_new: false,
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
|
let candidates = all_branches
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name.clone()))
|
||||||
|
.collect::<Vec<StringMatchCandidate>>();
|
||||||
fuzzy::match_strings(
|
fuzzy::match_strings(
|
||||||
&candidates,
|
&candidates,
|
||||||
&query,
|
&query,
|
||||||
@@ -273,20 +281,35 @@ impl PickerDelegate for BranchListDelegate {
|
|||||||
.await
|
.await
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(BranchEntry::Branch)
|
.map(|candidate| BranchEntry {
|
||||||
|
branch: all_branches[candidate.candidate_id].clone(),
|
||||||
|
positions: candidate.positions,
|
||||||
|
is_new: false,
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
picker
|
picker
|
||||||
.update(&mut cx, |picker, _| {
|
.update(&mut cx, |picker, _| {
|
||||||
|
#[allow(clippy::nonminimal_bool)]
|
||||||
|
if !query.is_empty()
|
||||||
|
&& !matches
|
||||||
|
.first()
|
||||||
|
.is_some_and(|entry| entry.branch.name == query)
|
||||||
|
{
|
||||||
|
matches.push(BranchEntry {
|
||||||
|
branch: Branch {
|
||||||
|
name: query.clone().into(),
|
||||||
|
is_head: false,
|
||||||
|
upstream: None,
|
||||||
|
most_recent_commit: None,
|
||||||
|
},
|
||||||
|
positions: Vec::new(),
|
||||||
|
is_new: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
let delegate = &mut picker.delegate;
|
let delegate = &mut picker.delegate;
|
||||||
delegate.matches = matches;
|
delegate.matches = matches;
|
||||||
if delegate.matches.is_empty() {
|
if delegate.matches.is_empty() {
|
||||||
if !query.is_empty() {
|
|
||||||
delegate.matches.push(BranchEntry::NewBranch {
|
|
||||||
name: query.trim().replace(' ', "-"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate.selected_index = 0;
|
delegate.selected_index = 0;
|
||||||
} else {
|
} else {
|
||||||
delegate.selected_index =
|
delegate.selected_index =
|
||||||
@@ -298,10 +321,14 @@ impl PickerDelegate for BranchListDelegate {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
let Some(branch) = self.matches.get(self.selected_index()) else {
|
let Some(entry) = self.matches.get(self.selected_index()) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if entry.is_new {
|
||||||
|
self.create_branch(entry.branch.name.clone(), window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let current_branch = self.repo.as_ref().map(|repo| {
|
let current_branch = self.repo.as_ref().map(|repo| {
|
||||||
repo.update(cx, |repo, _| {
|
repo.update(cx, |repo, _| {
|
||||||
@@ -311,14 +338,14 @@ impl PickerDelegate for BranchListDelegate {
|
|||||||
|
|
||||||
if current_branch
|
if current_branch
|
||||||
.flatten()
|
.flatten()
|
||||||
.is_some_and(|current_branch| current_branch == branch.name())
|
.is_some_and(|current_branch| current_branch == entry.branch.name)
|
||||||
{
|
{
|
||||||
cx.emit(DismissEvent);
|
cx.emit(DismissEvent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.spawn_in(window, {
|
cx.spawn_in(window, {
|
||||||
let branch = branch.clone();
|
let branch = entry.branch.clone();
|
||||||
|picker, mut cx| async move {
|
|picker, mut cx| async move {
|
||||||
let branch_change_task = picker.update(&mut cx, |this, cx| {
|
let branch_change_task = picker.update(&mut cx, |this, cx| {
|
||||||
let repo = this
|
let repo = this
|
||||||
@@ -331,22 +358,8 @@ impl PickerDelegate for BranchListDelegate {
|
|||||||
let cx = cx.to_async();
|
let cx = cx.to_async();
|
||||||
|
|
||||||
anyhow::Ok(async move {
|
anyhow::Ok(async move {
|
||||||
match branch {
|
cx.update(|cx| repo.read(cx).change_branch(&branch.name))?
|
||||||
BranchEntry::Branch(StringMatch {
|
.await?
|
||||||
string: branch_name,
|
|
||||||
..
|
|
||||||
})
|
|
||||||
| BranchEntry::History(branch_name) => {
|
|
||||||
cx.update(|cx| repo.read(cx).change_branch(branch_name))?
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
BranchEntry::NewBranch { name: branch_name } => {
|
|
||||||
cx.update(|cx| repo.read(cx).create_branch(branch_name.clone()))?
|
|
||||||
.await??;
|
|
||||||
cx.update(|cx| repo.read(cx).change_branch(branch_name))?
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})??;
|
})??;
|
||||||
|
|
||||||
@@ -366,16 +379,35 @@ impl PickerDelegate for BranchListDelegate {
|
|||||||
cx.emit(DismissEvent);
|
cx.emit(DismissEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_header(&self, _: &mut Window, _cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
_cx: &mut Context<Picker<Self>>,
|
cx: &mut Context<Picker<Self>>,
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
let hit = &self.matches[ix];
|
let entry = &self.matches[ix];
|
||||||
let shortened_branch_name =
|
|
||||||
util::truncate_and_trailoff(&hit.name(), self.branch_name_trailoff_after);
|
let (commit_time, subject) = entry
|
||||||
|
.branch
|
||||||
|
.most_recent_commit
|
||||||
|
.as_ref()
|
||||||
|
.map(|commit| {
|
||||||
|
let subject = commit.subject.clone();
|
||||||
|
let commit_time = OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)
|
||||||
|
.unwrap_or_else(|_| OffsetDateTime::now_utc());
|
||||||
|
let formatted_time = format_local_timestamp(
|
||||||
|
commit_time,
|
||||||
|
OffsetDateTime::now_utc(),
|
||||||
|
time_format::TimestampFormat::Relative,
|
||||||
|
);
|
||||||
|
(Some(formatted_time), Some(subject))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| (None, None));
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
|
ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
|
||||||
@@ -386,29 +418,67 @@ impl PickerDelegate for BranchListDelegate {
|
|||||||
})
|
})
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.spacing(ListItemSpacing::Sparse)
|
||||||
.toggle_state(selected)
|
.toggle_state(selected)
|
||||||
.when(matches!(hit, BranchEntry::History(_)), |el| {
|
.child(
|
||||||
el.end_slot(
|
v_flex()
|
||||||
Icon::new(IconName::HistoryRerun)
|
.w_full()
|
||||||
.color(Color::Muted)
|
.child(
|
||||||
.size(IconSize::Small),
|
h_flex()
|
||||||
)
|
.w_full()
|
||||||
})
|
.flex_shrink()
|
||||||
.map(|el| match hit {
|
.overflow_x_hidden()
|
||||||
BranchEntry::Branch(branch) => {
|
.gap_2()
|
||||||
let highlights: Vec<_> = branch
|
.justify_between()
|
||||||
.positions
|
.child(div().flex_shrink().overflow_x_hidden().child(
|
||||||
.iter()
|
if entry.is_new {
|
||||||
.filter(|index| index < &&self.branch_name_trailoff_after)
|
Label::new(format!(
|
||||||
.copied()
|
"Create branch \"{}\"…",
|
||||||
.collect();
|
entry.branch.name
|
||||||
|
))
|
||||||
el.child(HighlightedLabel::new(shortened_branch_name, highlights))
|
.into_any_element()
|
||||||
}
|
} else {
|
||||||
BranchEntry::History(_) => el.child(Label::new(shortened_branch_name)),
|
HighlightedLabel::new(
|
||||||
BranchEntry::NewBranch { name } => {
|
entry.branch.name.clone(),
|
||||||
el.child(Label::new(format!("Create branch '{name}'")))
|
entry.positions.clone(),
|
||||||
}
|
)
|
||||||
}),
|
.truncate()
|
||||||
|
.into_any_element()
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.when_some(commit_time, |el, commit_time| {
|
||||||
|
el.child(
|
||||||
|
Label::new(commit_time)
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted)
|
||||||
|
.into_element(),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.when(self.style == BranchListStyle::Modal, |el| {
|
||||||
|
el.child(div().max_w_96().child({
|
||||||
|
let message = if entry.is_new {
|
||||||
|
if let Some(current_branch) =
|
||||||
|
self.repo.as_ref().and_then(|repo| {
|
||||||
|
repo.read(cx).current_branch().map(|b| b.name.clone())
|
||||||
|
})
|
||||||
|
{
|
||||||
|
format!("based off {}", current_branch)
|
||||||
|
} else {
|
||||||
|
"based off the current branch".to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subject.unwrap_or("no commits found".into()).to_string()
|
||||||
|
};
|
||||||
|
Label::new(message)
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.truncate()
|
||||||
|
.color(Color::Muted)
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::branch_picker::{self, BranchList};
|
|||||||
use crate::git_panel::{commit_message_editor, GitPanel};
|
use crate::git_panel::{commit_message_editor, GitPanel};
|
||||||
use git::{Commit, GenerateCommitMessage};
|
use git::{Commit, GenerateCommitMessage};
|
||||||
use panel::{panel_button, panel_editor_style, panel_filled_button};
|
use panel::{panel_button, panel_editor_style, panel_filled_button};
|
||||||
use ui::{prelude::*, KeybindingHint, PopoverMenu, Tooltip};
|
use ui::{prelude::*, KeybindingHint, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||||
|
|
||||||
use editor::{Editor, EditorElement};
|
use editor::{Editor, EditorElement};
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
@@ -65,11 +65,11 @@ pub fn init(cx: &mut App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct CommitModal {
|
pub struct CommitModal {
|
||||||
branch_list: Entity<BranchList>,
|
|
||||||
git_panel: Entity<GitPanel>,
|
git_panel: Entity<GitPanel>,
|
||||||
commit_editor: Entity<Editor>,
|
commit_editor: Entity<Editor>,
|
||||||
restore_dock: RestoreDock,
|
restore_dock: RestoreDock,
|
||||||
properties: ModalContainerProperties,
|
properties: ModalContainerProperties,
|
||||||
|
branch_list_handle: PopoverMenuHandle<BranchList>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable for CommitModal {
|
impl Focusable for CommitModal {
|
||||||
@@ -146,7 +146,6 @@ impl CommitModal {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let panel = git_panel.read(cx);
|
let panel = git_panel.read(cx);
|
||||||
let active_repository = panel.active_repository.clone();
|
|
||||||
let suggested_commit_message = panel.suggest_commit_message();
|
let suggested_commit_message = panel.suggest_commit_message();
|
||||||
|
|
||||||
let commit_editor = git_panel.update(cx, |git_panel, cx| {
|
let commit_editor = git_panel.update(cx, |git_panel, cx| {
|
||||||
@@ -177,11 +176,7 @@ impl CommitModal {
|
|||||||
let focus_handle = commit_editor.focus_handle(cx);
|
let focus_handle = commit_editor.focus_handle(cx);
|
||||||
|
|
||||||
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
|
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
|
||||||
if !this
|
if !this.branch_list_handle.is_focused(window, cx) {
|
||||||
.branch_list
|
|
||||||
.focus_handle(cx)
|
|
||||||
.contains_focused(window, cx)
|
|
||||||
{
|
|
||||||
cx.emit(DismissEvent);
|
cx.emit(DismissEvent);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -190,11 +185,11 @@ impl CommitModal {
|
|||||||
let properties = ModalContainerProperties::new(window, 50);
|
let properties = ModalContainerProperties::new(window, 50);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
branch_list: branch_picker::popover(active_repository.clone(), window, cx),
|
|
||||||
git_panel,
|
git_panel,
|
||||||
commit_editor,
|
commit_editor,
|
||||||
restore_dock,
|
restore_dock,
|
||||||
properties,
|
properties,
|
||||||
|
branch_list_handle: PopoverMenuHandle::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,34 +227,29 @@ impl CommitModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let git_panel = self.git_panel.clone();
|
let (can_commit, tooltip, commit_label, co_authors, generate_commit_message, active_repo) =
|
||||||
|
|
||||||
let (branch, can_commit, tooltip, commit_label, co_authors, generate_commit_message) =
|
|
||||||
self.git_panel.update(cx, |git_panel, cx| {
|
self.git_panel.update(cx, |git_panel, cx| {
|
||||||
let branch = git_panel
|
|
||||||
.active_repository
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|repo| {
|
|
||||||
repo.read(cx)
|
|
||||||
.repository_entry
|
|
||||||
.branch()
|
|
||||||
.map(|b| b.name.clone())
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| "<no branch>".into());
|
|
||||||
let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
|
let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
|
||||||
let title = git_panel.commit_button_title();
|
let title = git_panel.commit_button_title();
|
||||||
let co_authors = git_panel.render_co_authors(cx);
|
let co_authors = git_panel.render_co_authors(cx);
|
||||||
let generate_commit_message = git_panel.render_generate_commit_message_button(cx);
|
let generate_commit_message = git_panel.render_generate_commit_message_button(cx);
|
||||||
|
let active_repo = git_panel.active_repository.clone();
|
||||||
(
|
(
|
||||||
branch,
|
|
||||||
can_commit,
|
can_commit,
|
||||||
tooltip,
|
tooltip,
|
||||||
title,
|
title,
|
||||||
co_authors,
|
co_authors,
|
||||||
generate_commit_message,
|
generate_commit_message,
|
||||||
|
active_repo,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let branch = active_repo
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|repo| repo.read(cx).repository_entry.branch())
|
||||||
|
.map(|b| b.name.clone())
|
||||||
|
.unwrap_or_else(|| "<no branch>".into());
|
||||||
|
|
||||||
let branch_picker_button = panel_button(branch)
|
let branch_picker_button = panel_button(branch)
|
||||||
.icon(IconName::GitBranch)
|
.icon(IconName::GitBranch)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
@@ -276,10 +266,8 @@ impl CommitModal {
|
|||||||
.style(ButtonStyle::Transparent);
|
.style(ButtonStyle::Transparent);
|
||||||
|
|
||||||
let branch_picker = PopoverMenu::new("popover-button")
|
let branch_picker = PopoverMenu::new("popover-button")
|
||||||
.menu({
|
.menu(move |window, cx| Some(branch_picker::popover(active_repo.clone(), window, cx)))
|
||||||
let branch_list = self.branch_list.clone();
|
.with_handle(self.branch_list_handle.clone())
|
||||||
move |_window, _cx| Some(branch_list.clone())
|
|
||||||
})
|
|
||||||
.trigger_with_tooltip(
|
.trigger_with_tooltip(
|
||||||
branch_picker_button,
|
branch_picker_button,
|
||||||
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
|
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
|
||||||
@@ -289,6 +277,7 @@ impl CommitModal {
|
|||||||
x: px(0.0),
|
x: px(0.0),
|
||||||
y: px(-2.0),
|
y: px(-2.0),
|
||||||
});
|
});
|
||||||
|
let focus_handle = self.focus_handle(cx);
|
||||||
|
|
||||||
let close_kb_hint =
|
let close_kb_hint =
|
||||||
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
|
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
|
||||||
@@ -300,12 +289,9 @@ impl CommitModal {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let panel_editor_focus_handle =
|
|
||||||
git_panel.update(cx, |git_panel, cx| git_panel.editor_focus_handle(cx));
|
|
||||||
|
|
||||||
let commit_button = panel_filled_button(commit_label)
|
let commit_button = panel_filled_button(commit_label)
|
||||||
.tooltip({
|
.tooltip({
|
||||||
let panel_editor_focus_handle = panel_editor_focus_handle.clone();
|
let panel_editor_focus_handle = focus_handle.clone();
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
Tooltip::for_action_in(tooltip, &Commit, &panel_editor_focus_handle, window, cx)
|
Tooltip::for_action_in(tooltip, &Commit, &panel_editor_focus_handle, window, cx)
|
||||||
}
|
}
|
||||||
@@ -330,7 +316,14 @@ impl CommitModal {
|
|||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(branch_picker)
|
.flex_shrink()
|
||||||
|
.overflow_x_hidden()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.flex_shrink()
|
||||||
|
.overflow_x_hidden()
|
||||||
|
.child(branch_picker),
|
||||||
|
)
|
||||||
.children(generate_commit_message)
|
.children(generate_commit_message)
|
||||||
.children(co_authors),
|
.children(co_authors),
|
||||||
)
|
)
|
||||||
@@ -357,6 +350,14 @@ impl CommitModal {
|
|||||||
.update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
|
.update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
|
||||||
cx.emit(DismissEvent);
|
cx.emit(DismissEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn toggle_branch_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.branch_list_handle.is_focused(window, cx) {
|
||||||
|
self.focus_handle(cx).focus(window)
|
||||||
|
} else {
|
||||||
|
self.branch_list_handle.toggle(window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for CommitModal {
|
impl Render for CommitModal {
|
||||||
@@ -379,17 +380,17 @@ impl Render for CommitModal {
|
|||||||
}))
|
}))
|
||||||
.on_action(
|
.on_action(
|
||||||
cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
|
cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
|
||||||
toggle_branch_picker(this, window, cx);
|
this.toggle_branch_selector(window, cx);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.on_action(
|
.on_action(
|
||||||
cx.listener(|this, _: &zed_actions::git::CheckoutBranch, window, cx| {
|
cx.listener(|this, _: &zed_actions::git::CheckoutBranch, window, cx| {
|
||||||
toggle_branch_picker(this, window, cx);
|
this.toggle_branch_selector(window, cx);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.on_action(
|
.on_action(
|
||||||
cx.listener(|this, _: &zed_actions::git::Switch, window, cx| {
|
cx.listener(|this, _: &zed_actions::git::Switch, window, cx| {
|
||||||
toggle_branch_picker(this, window, cx);
|
this.toggle_branch_selector(window, cx);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.elevation_3(cx)
|
.elevation_3(cx)
|
||||||
@@ -428,13 +429,3 @@ impl Render for CommitModal {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_branch_picker(
|
|
||||||
this: &mut CommitModal,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<'_, CommitModal>,
|
|
||||||
) {
|
|
||||||
this.branch_list.update(cx, |branch_list, cx| {
|
|
||||||
branch_list.popover_handle.toggle(window, cx);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
|||||||
use ::settings::Settings;
|
use ::settings::Settings;
|
||||||
use git::{
|
use git::{
|
||||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||||
status::FileStatus,
|
status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
|
||||||
};
|
};
|
||||||
use git_panel_settings::GitPanelSettings;
|
use git_panel_settings::GitPanelSettings;
|
||||||
use gpui::{App, Entity, FocusHandle};
|
use gpui::{App, Entity, FocusHandle};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use project_diff::ProjectDiff;
|
use project_diff::ProjectDiff;
|
||||||
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement, SharedString};
|
use ui::prelude::*;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
mod askpass_modal;
|
mod askpass_modal;
|
||||||
@@ -17,7 +17,7 @@ pub mod git_panel;
|
|||||||
mod git_panel_settings;
|
mod git_panel_settings;
|
||||||
pub mod picker_prompt;
|
pub mod picker_prompt;
|
||||||
pub mod project_diff;
|
pub mod project_diff;
|
||||||
mod remote_output_toast;
|
pub(crate) mod remote_output;
|
||||||
pub mod repository_selector;
|
pub mod repository_selector;
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
@@ -29,41 +29,43 @@ pub fn init(cx: &mut App) {
|
|||||||
|
|
||||||
cx.observe_new(|workspace: &mut Workspace, _, cx| {
|
cx.observe_new(|workspace: &mut Workspace, _, cx| {
|
||||||
let project = workspace.project().read(cx);
|
let project = workspace.project().read(cx);
|
||||||
if project.is_via_collab() {
|
if project.is_read_only(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
|
if !project.is_via_collab() {
|
||||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
|
||||||
return;
|
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||||
};
|
return;
|
||||||
panel.update(cx, |panel, cx| {
|
};
|
||||||
panel.fetch(window, cx);
|
panel.update(cx, |panel, cx| {
|
||||||
|
panel.fetch(window, cx);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
workspace.register_action(|workspace, _: &git::Push, window, cx| {
|
||||||
workspace.register_action(|workspace, _: &git::Push, window, cx| {
|
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
return;
|
||||||
return;
|
};
|
||||||
};
|
panel.update(cx, |panel, cx| {
|
||||||
panel.update(cx, |panel, cx| {
|
panel.push(false, window, cx);
|
||||||
panel.push(false, window, cx);
|
});
|
||||||
});
|
});
|
||||||
});
|
workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
|
||||||
workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
|
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
return;
|
||||||
return;
|
};
|
||||||
};
|
panel.update(cx, |panel, cx| {
|
||||||
panel.update(cx, |panel, cx| {
|
panel.push(true, window, cx);
|
||||||
panel.push(true, window, cx);
|
});
|
||||||
});
|
});
|
||||||
});
|
workspace.register_action(|workspace, _: &git::Pull, window, cx| {
|
||||||
workspace.register_action(|workspace, _: &git::Pull, window, cx| {
|
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
return;
|
||||||
return;
|
};
|
||||||
};
|
panel.update(cx, |panel, cx| {
|
||||||
panel.update(cx, |panel, cx| {
|
panel.pull(window, cx);
|
||||||
panel.pull(window, cx);
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
workspace.register_action(|workspace, action: &git::StageAll, window, cx| {
|
workspace.register_action(|workspace, action: &git::StageAll, window, cx| {
|
||||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||||
return;
|
return;
|
||||||
@@ -84,30 +86,8 @@ pub fn init(cx: &mut App) {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add updated status colors to theme
|
pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
|
||||||
pub fn git_status_icon(status: FileStatus, cx: &App) -> impl IntoElement {
|
GitStatusIcon::new(status)
|
||||||
let (icon_name, color) = if status.is_conflicted() {
|
|
||||||
(
|
|
||||||
IconName::Warning,
|
|
||||||
cx.theme().colors().version_control_conflict,
|
|
||||||
)
|
|
||||||
} else if status.is_deleted() {
|
|
||||||
(
|
|
||||||
IconName::SquareMinus,
|
|
||||||
cx.theme().colors().version_control_deleted,
|
|
||||||
)
|
|
||||||
} else if status.is_modified() {
|
|
||||||
(
|
|
||||||
IconName::SquareDot,
|
|
||||||
cx.theme().colors().version_control_modified,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
IconName::SquarePlus,
|
|
||||||
cx.theme().colors().version_control_added,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
Icon::new(icon_name).color(Color::Custom(color))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_push_and_pull(project: &Entity<Project>, cx: &App) -> bool {
|
fn can_push_and_pull(project: &Entity<Project>, cx: &App) -> bool {
|
||||||
@@ -173,6 +153,7 @@ mod remote_button {
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
Some(IconName::ArrowCircle),
|
Some(IconName::ArrowCircle),
|
||||||
|
keybinding_target.clone(),
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
window.dispatch_action(Box::new(git::Fetch), cx);
|
window.dispatch_action(Box::new(git::Fetch), cx);
|
||||||
},
|
},
|
||||||
@@ -200,6 +181,7 @@ mod remote_button {
|
|||||||
ahead as usize,
|
ahead as usize,
|
||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
|
keybinding_target.clone(),
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
window.dispatch_action(Box::new(git::Push), cx);
|
window.dispatch_action(Box::new(git::Push), cx);
|
||||||
},
|
},
|
||||||
@@ -228,6 +210,7 @@ mod remote_button {
|
|||||||
ahead as usize,
|
ahead as usize,
|
||||||
behind as usize,
|
behind as usize,
|
||||||
None,
|
None,
|
||||||
|
keybinding_target.clone(),
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
window.dispatch_action(Box::new(git::Pull), cx);
|
window.dispatch_action(Box::new(git::Pull), cx);
|
||||||
},
|
},
|
||||||
@@ -254,6 +237,7 @@ mod remote_button {
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
Some(IconName::ArrowUpFromLine),
|
Some(IconName::ArrowUpFromLine),
|
||||||
|
keybinding_target.clone(),
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
window.dispatch_action(Box::new(git::Push), cx);
|
window.dispatch_action(Box::new(git::Push), cx);
|
||||||
},
|
},
|
||||||
@@ -280,6 +264,7 @@ mod remote_button {
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
Some(IconName::ArrowUpFromLine),
|
Some(IconName::ArrowUpFromLine),
|
||||||
|
keybinding_target.clone(),
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
window.dispatch_action(Box::new(git::Push), cx);
|
window.dispatch_action(Box::new(git::Push), cx);
|
||||||
},
|
},
|
||||||
@@ -321,7 +306,10 @@ mod remote_button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_git_action_menu(id: impl Into<ElementId>) -> impl IntoElement {
|
fn render_git_action_menu(
|
||||||
|
id: impl Into<ElementId>,
|
||||||
|
keybinding_target: Option<FocusHandle>,
|
||||||
|
) -> impl IntoElement {
|
||||||
PopoverMenu::new(id.into())
|
PopoverMenu::new(id.into())
|
||||||
.trigger(
|
.trigger(
|
||||||
ui::ButtonLike::new_rounded_right("split-button-right")
|
ui::ButtonLike::new_rounded_right("split-button-right")
|
||||||
@@ -336,6 +324,9 @@ mod remote_button {
|
|||||||
.menu(move |window, cx| {
|
.menu(move |window, cx| {
|
||||||
Some(ContextMenu::build(window, cx, |context_menu, _, _| {
|
Some(ContextMenu::build(window, cx, |context_menu, _, _| {
|
||||||
context_menu
|
context_menu
|
||||||
|
.when_some(keybinding_target.clone(), |el, keybinding_target| {
|
||||||
|
el.context(keybinding_target.clone())
|
||||||
|
})
|
||||||
.action("Fetch", git::Fetch.boxed_clone())
|
.action("Fetch", git::Fetch.boxed_clone())
|
||||||
.action("Pull", git::Pull.boxed_clone())
|
.action("Pull", git::Pull.boxed_clone())
|
||||||
.separator()
|
.separator()
|
||||||
@@ -353,12 +344,14 @@ mod remote_button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SplitButton {
|
impl SplitButton {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn new(
|
fn new(
|
||||||
id: impl Into<SharedString>,
|
id: impl Into<SharedString>,
|
||||||
left_label: impl Into<SharedString>,
|
left_label: impl Into<SharedString>,
|
||||||
ahead_count: usize,
|
ahead_count: usize,
|
||||||
behind_count: usize,
|
behind_count: usize,
|
||||||
left_icon: Option<IconName>,
|
left_icon: Option<IconName>,
|
||||||
|
keybinding_target: Option<FocusHandle>,
|
||||||
left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||||
tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
|
tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -416,9 +409,10 @@ mod remote_button {
|
|||||||
.on_click(left_on_click)
|
.on_click(left_on_click)
|
||||||
.tooltip(tooltip);
|
.tooltip(tooltip);
|
||||||
|
|
||||||
let right = render_git_action_menu(ElementId::Name(
|
let right = render_git_action_menu(
|
||||||
format!("split-button-right-{}", id).into(),
|
ElementId::Name(format!("split-button-right-{}", id).into()),
|
||||||
))
|
keybinding_target,
|
||||||
|
)
|
||||||
.into_any_element();
|
.into_any_element();
|
||||||
|
|
||||||
Self { left, right }
|
Self { left, right }
|
||||||
@@ -449,3 +443,79 @@ mod remote_button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement, IntoComponent)]
|
||||||
|
#[component(scope = "Version Control")]
|
||||||
|
pub struct GitStatusIcon {
|
||||||
|
status: FileStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitStatusIcon {
|
||||||
|
pub fn new(status: FileStatus) -> Self {
|
||||||
|
Self { status }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for GitStatusIcon {
|
||||||
|
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let status = self.status;
|
||||||
|
|
||||||
|
let (icon_name, color) = if status.is_conflicted() {
|
||||||
|
(
|
||||||
|
IconName::Warning,
|
||||||
|
cx.theme().colors().version_control_conflict,
|
||||||
|
)
|
||||||
|
} else if status.is_deleted() {
|
||||||
|
(
|
||||||
|
IconName::SquareMinus,
|
||||||
|
cx.theme().colors().version_control_deleted,
|
||||||
|
)
|
||||||
|
} else if status.is_modified() {
|
||||||
|
(
|
||||||
|
IconName::SquareDot,
|
||||||
|
cx.theme().colors().version_control_modified,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
IconName::SquarePlus,
|
||||||
|
cx.theme().colors().version_control_added,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Icon::new(icon_name).color(Color::Custom(color))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// View this component preview using `workspace: open component-preview`
|
||||||
|
impl ComponentPreview for GitStatusIcon {
|
||||||
|
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
|
||||||
|
fn tracked_file_status(code: StatusCode) -> FileStatus {
|
||||||
|
FileStatus::Tracked(git::status::TrackedStatus {
|
||||||
|
index_status: code,
|
||||||
|
worktree_status: code,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let modified = tracked_file_status(StatusCode::Modified);
|
||||||
|
let added = tracked_file_status(StatusCode::Added);
|
||||||
|
let deleted = tracked_file_status(StatusCode::Deleted);
|
||||||
|
let conflict = UnmergedStatus {
|
||||||
|
first_head: UnmergedStatusCode::Updated,
|
||||||
|
second_head: UnmergedStatusCode::Updated,
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.gap_6()
|
||||||
|
.children(vec![example_group(vec![
|
||||||
|
single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
|
||||||
|
single_example("Added", GitStatusIcon::new(added).into_any_element()),
|
||||||
|
single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
|
||||||
|
single_example(
|
||||||
|
"Conflicted",
|
||||||
|
GitStatusIcon::new(conflict).into_any_element(),
|
||||||
|
),
|
||||||
|
])])
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use editor::{
|
|||||||
scroll::Autoscroll,
|
scroll::Autoscroll,
|
||||||
Editor, EditorEvent,
|
Editor, EditorEvent,
|
||||||
};
|
};
|
||||||
use feature_flags::FeatureFlagViewExt;
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use git::{
|
use git::{
|
||||||
repository::Branch, status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged,
|
repository::Branch, status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged,
|
||||||
@@ -64,16 +63,13 @@ const NEW_NAMESPACE: &'static str = "2";
|
|||||||
|
|
||||||
impl ProjectDiff {
|
impl ProjectDiff {
|
||||||
pub(crate) fn register(
|
pub(crate) fn register(
|
||||||
_: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
window: Option<&mut Window>,
|
_window: Option<&mut Window>,
|
||||||
cx: &mut Context<Workspace>,
|
cx: &mut Context<Workspace>,
|
||||||
) {
|
) {
|
||||||
let Some(window) = window else { return };
|
workspace.register_action(Self::deploy);
|
||||||
cx.when_flag_enabled::<feature_flags::GitUiFeatureFlag>(window, |workspace, _, _cx| {
|
workspace.register_action(|workspace, _: &Add, window, cx| {
|
||||||
workspace.register_action(Self::deploy);
|
Self::deploy(workspace, &Diff, window, cx);
|
||||||
workspace.register_action(|workspace, _: &Add, window, cx| {
|
|
||||||
Self::deploy(workspace, &Diff, window, cx);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
workspace::register_serializable_item::<ProjectDiff>(cx);
|
workspace::register_serializable_item::<ProjectDiff>(cx);
|
||||||
@@ -125,6 +121,12 @@ impl ProjectDiff {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn autoscroll(&self, cx: &mut Context<Self>) {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.request_autoscroll(Autoscroll::fit(), cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn new(
|
fn new(
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
workspace: Entity<Workspace>,
|
workspace: Entity<Workspace>,
|
||||||
@@ -472,7 +474,10 @@ impl ProjectDiff {
|
|||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.update(&mut cx, |this, _| this.pending_scroll.take())?;
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.pending_scroll.take();
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
152
crates/git_ui/src/remote_output.rs
Normal file
152
crates/git_ui/src/remote_output.rs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
use anyhow::Context as _;
|
||||||
|
use git::repository::{Remote, RemoteCommandOutput};
|
||||||
|
use linkify::{LinkFinder, LinkKind};
|
||||||
|
use ui::SharedString;
|
||||||
|
use util::ResultExt as _;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum RemoteAction {
|
||||||
|
Fetch,
|
||||||
|
Pull(Remote),
|
||||||
|
Push(SharedString, Remote),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RemoteAction {
|
||||||
|
pub fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
RemoteAction::Fetch => "fetch",
|
||||||
|
RemoteAction::Pull(_) => "pull",
|
||||||
|
RemoteAction::Push(_, _) => "push",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SuccessStyle {
|
||||||
|
Toast,
|
||||||
|
ToastWithLog { output: RemoteCommandOutput },
|
||||||
|
PushPrLink { link: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SuccessMessage {
|
||||||
|
pub message: String,
|
||||||
|
pub style: SuccessStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> SuccessMessage {
|
||||||
|
match action {
|
||||||
|
RemoteAction::Fetch => {
|
||||||
|
if output.stderr.is_empty() {
|
||||||
|
SuccessMessage {
|
||||||
|
message: "Already up to date".into(),
|
||||||
|
style: SuccessStyle::Toast,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SuccessMessage {
|
||||||
|
message: "Synchronized with remotes".into(),
|
||||||
|
style: SuccessStyle::ToastWithLog { output },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RemoteAction::Pull(remote_ref) => {
|
||||||
|
let get_changes = |output: &RemoteCommandOutput| -> anyhow::Result<u32> {
|
||||||
|
let last_line = output
|
||||||
|
.stdout
|
||||||
|
.lines()
|
||||||
|
.last()
|
||||||
|
.context("Failed to get last line of output")?
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
let files_changed = last_line
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.context("Failed to get first word of last line")?
|
||||||
|
.parse()?;
|
||||||
|
|
||||||
|
Ok(files_changed)
|
||||||
|
};
|
||||||
|
|
||||||
|
if output.stderr.starts_with("Everything up to date") {
|
||||||
|
SuccessMessage {
|
||||||
|
message: output.stderr.trim().to_owned(),
|
||||||
|
style: SuccessStyle::Toast,
|
||||||
|
}
|
||||||
|
} else if output.stdout.starts_with("Updating") {
|
||||||
|
let files_changed = get_changes(&output).log_err();
|
||||||
|
let message = if let Some(files_changed) = files_changed {
|
||||||
|
format!(
|
||||||
|
"Received {} file change{} from {}",
|
||||||
|
files_changed,
|
||||||
|
if files_changed == 1 { "" } else { "s" },
|
||||||
|
remote_ref.name
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("Fast forwarded from {}", remote_ref.name)
|
||||||
|
};
|
||||||
|
SuccessMessage {
|
||||||
|
message,
|
||||||
|
style: SuccessStyle::ToastWithLog { output },
|
||||||
|
}
|
||||||
|
} else if output.stdout.starts_with("Merge") {
|
||||||
|
let files_changed = get_changes(&output).log_err();
|
||||||
|
let message = if let Some(files_changed) = files_changed {
|
||||||
|
format!(
|
||||||
|
"Merged {} file change{} from {}",
|
||||||
|
files_changed,
|
||||||
|
if files_changed == 1 { "" } else { "s" },
|
||||||
|
remote_ref.name
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("Merged from {}", remote_ref.name)
|
||||||
|
};
|
||||||
|
SuccessMessage {
|
||||||
|
message,
|
||||||
|
style: SuccessStyle::ToastWithLog { output },
|
||||||
|
}
|
||||||
|
} else if output.stdout.contains("Successfully rebased") {
|
||||||
|
SuccessMessage {
|
||||||
|
message: format!("Successfully rebased from {}", remote_ref.name),
|
||||||
|
style: SuccessStyle::ToastWithLog { output },
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SuccessMessage {
|
||||||
|
message: format!("Successfully pulled from {}", remote_ref.name),
|
||||||
|
style: SuccessStyle::ToastWithLog { output },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RemoteAction::Push(branch_name, remote_ref) => {
|
||||||
|
if output.stderr.contains("* [new branch]") {
|
||||||
|
let style = if output.stderr.contains("Create a pull request") {
|
||||||
|
let finder = LinkFinder::new();
|
||||||
|
let first_link = finder
|
||||||
|
.links(&output.stderr)
|
||||||
|
.filter(|link| *link.kind() == LinkKind::Url)
|
||||||
|
.map(|link| link.start()..link.end())
|
||||||
|
.next();
|
||||||
|
if let Some(link) = first_link {
|
||||||
|
let link = output.stderr[link].to_string();
|
||||||
|
SuccessStyle::PushPrLink { link }
|
||||||
|
} else {
|
||||||
|
SuccessStyle::ToastWithLog { output }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SuccessStyle::ToastWithLog { output }
|
||||||
|
};
|
||||||
|
SuccessMessage {
|
||||||
|
message: format!("Published {} to {}", branch_name, remote_ref.name),
|
||||||
|
style,
|
||||||
|
}
|
||||||
|
} else if output.stderr.starts_with("Everything up to date") {
|
||||||
|
SuccessMessage {
|
||||||
|
message: output.stderr.trim().to_owned(),
|
||||||
|
style: SuccessStyle::Toast,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SuccessMessage {
|
||||||
|
message: "Successfully pushed new branch".to_owned(),
|
||||||
|
style: SuccessStyle::ToastWithLog { output },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
use std::{ops::Range, time::Duration};
|
|
||||||
|
|
||||||
use git::repository::{Remote, RemoteCommandOutput};
|
|
||||||
use gpui::{
|
|
||||||
DismissEvent, EventEmitter, FocusHandle, Focusable, HighlightStyle, InteractiveText,
|
|
||||||
StyledText, Task, UnderlineStyle, WeakEntity,
|
|
||||||
};
|
|
||||||
use itertools::Itertools;
|
|
||||||
use linkify::{LinkFinder, LinkKind};
|
|
||||||
use ui::{
|
|
||||||
div, h_flex, px, v_flex, vh, Clickable, Color, Context, FluentBuilder, Icon, IconButton,
|
|
||||||
IconName, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
|
|
||||||
Render, SharedString, Styled, StyledExt, Window,
|
|
||||||
};
|
|
||||||
use workspace::{
|
|
||||||
notifications::{Notification, NotificationId},
|
|
||||||
Workspace,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub enum RemoteAction {
|
|
||||||
Fetch,
|
|
||||||
Pull,
|
|
||||||
Push(Remote),
|
|
||||||
}
|
|
||||||
|
|
||||||
struct InfoFromRemote {
|
|
||||||
name: SharedString,
|
|
||||||
remote_text: SharedString,
|
|
||||||
links: Vec<Range<usize>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RemoteOutputToast {
|
|
||||||
_workspace: WeakEntity<Workspace>,
|
|
||||||
_id: NotificationId,
|
|
||||||
message: SharedString,
|
|
||||||
remote_info: Option<InfoFromRemote>,
|
|
||||||
_dismiss_task: Task<()>,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Focusable for RemoteOutputToast {
|
|
||||||
fn focus_handle(&self, _cx: &ui::App) -> FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Notification for RemoteOutputToast {}
|
|
||||||
|
|
||||||
const REMOTE_OUTPUT_TOAST_SECONDS: u64 = 5;
|
|
||||||
|
|
||||||
impl RemoteOutputToast {
|
|
||||||
pub fn new(
|
|
||||||
action: RemoteAction,
|
|
||||||
output: RemoteCommandOutput,
|
|
||||||
id: NotificationId,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let task = cx.spawn({
|
|
||||||
let workspace = workspace.clone();
|
|
||||||
let id = id.clone();
|
|
||||||
|_, mut cx| async move {
|
|
||||||
cx.background_executor()
|
|
||||||
.timer(Duration::from_secs(REMOTE_OUTPUT_TOAST_SECONDS))
|
|
||||||
.await;
|
|
||||||
workspace
|
|
||||||
.update(&mut cx, |workspace, cx| {
|
|
||||||
workspace.dismiss_notification(&id, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut message: SharedString;
|
|
||||||
let remote;
|
|
||||||
|
|
||||||
match action {
|
|
||||||
RemoteAction::Fetch | RemoteAction::Pull => {
|
|
||||||
if output.is_empty() {
|
|
||||||
message = "Up to date".into();
|
|
||||||
} else {
|
|
||||||
message = output.stderr.into();
|
|
||||||
}
|
|
||||||
remote = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoteAction::Push(remote_ref) => {
|
|
||||||
message = output.stdout.trim().to_string().into();
|
|
||||||
if message.is_empty() {
|
|
||||||
message = output.stderr.trim().to_string().into();
|
|
||||||
if message.is_empty() {
|
|
||||||
message = "Push Successful".into();
|
|
||||||
}
|
|
||||||
remote = None;
|
|
||||||
} else {
|
|
||||||
let remote_message = get_remote_lines(&output.stderr);
|
|
||||||
|
|
||||||
remote = if remote_message.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let finder = LinkFinder::new();
|
|
||||||
let links = finder
|
|
||||||
.links(&remote_message)
|
|
||||||
.filter(|link| *link.kind() == LinkKind::Url)
|
|
||||||
.map(|link| link.start()..link.end())
|
|
||||||
.collect_vec();
|
|
||||||
|
|
||||||
Some(InfoFromRemote {
|
|
||||||
name: remote_ref.name,
|
|
||||||
remote_text: remote_message.into(),
|
|
||||||
links,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
_workspace: workspace,
|
|
||||||
_id: id,
|
|
||||||
message,
|
|
||||||
remote_info: remote,
|
|
||||||
_dismiss_task: task,
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for RemoteOutputToast {
|
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
|
|
||||||
div()
|
|
||||||
.occlude()
|
|
||||||
.w_full()
|
|
||||||
.max_h(vh(0.8, window))
|
|
||||||
.elevation_3(cx)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.p_3()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.justify_between()
|
|
||||||
.items_start()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(Icon::new(IconName::GitBranch).color(Color::Default))
|
|
||||||
.child(Label::new("Git")),
|
|
||||||
)
|
|
||||||
.child(h_flex().child(
|
|
||||||
IconButton::new("close", IconName::Close).on_click(
|
|
||||||
cx.listener(|_, _, _, cx| cx.emit(gpui::DismissEvent)),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.child(Label::new(self.message.clone()).size(LabelSize::Default))
|
|
||||||
.when_some(self.remote_info.as_ref(), |this, remote_info| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.border_1()
|
|
||||||
.border_color(Color::Muted.color(cx))
|
|
||||||
.rounded_lg()
|
|
||||||
.text_sm()
|
|
||||||
.mt_1()
|
|
||||||
.p_1()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(Icon::new(IconName::Cloud).color(Color::Default))
|
|
||||||
.child(
|
|
||||||
Label::new(remote_info.name.clone())
|
|
||||||
.size(LabelSize::Default),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.map(|div| {
|
|
||||||
let styled_text =
|
|
||||||
StyledText::new(remote_info.remote_text.clone())
|
|
||||||
.with_highlights(remote_info.links.iter().map(
|
|
||||||
|link| {
|
|
||||||
(
|
|
||||||
link.clone(),
|
|
||||||
HighlightStyle {
|
|
||||||
underline: Some(UnderlineStyle {
|
|
||||||
thickness: px(1.0),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
));
|
|
||||||
let this = cx.weak_entity();
|
|
||||||
let text = InteractiveText::new("remote-message", styled_text)
|
|
||||||
.on_click(
|
|
||||||
remote_info.links.clone(),
|
|
||||||
move |ix, _window, cx| {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
if let Some(remote_info) = &this.remote_info {
|
|
||||||
cx.open_url(
|
|
||||||
&remote_info.remote_text
|
|
||||||
[remote_info.links[ix].clone()],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
div.child(text)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for RemoteOutputToast {}
|
|
||||||
|
|
||||||
fn get_remote_lines(output: &str) -> String {
|
|
||||||
output
|
|
||||||
.lines()
|
|
||||||
.filter_map(|line| line.strip_prefix("remote:"))
|
|
||||||
.map(|line| line.trim())
|
|
||||||
.filter(|line| !line.is_empty())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n")
|
|
||||||
}
|
|
||||||
@@ -207,7 +207,7 @@ blade-macros.workspace = true
|
|||||||
flume = "0.11"
|
flume = "0.11"
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
windows.workspace = true
|
windows.workspace = true
|
||||||
windows-core = "0.58"
|
windows-core = "0.60"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ impl EntityMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
fn double_lease_panic<T>(operation: &str) -> ! {
|
fn double_lease_panic<T>(operation: &str) -> ! {
|
||||||
panic!(
|
panic!(
|
||||||
"cannot {operation} {} while it is already being updated",
|
"cannot {operation} {} while it is already being updated",
|
||||||
|
|||||||
@@ -683,11 +683,11 @@ impl Default for Background {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a hash pattern background
|
/// Creates a hash pattern background
|
||||||
pub fn pattern_slash(color: Hsla, thickness: f32) -> Background {
|
pub fn pattern_slash(color: Hsla, height: f32) -> Background {
|
||||||
Background {
|
Background {
|
||||||
tag: BackgroundTag::PatternSlash,
|
tag: BackgroundTag::PatternSlash,
|
||||||
solid: color,
|
solid: color,
|
||||||
gradient_angle_or_pattern_height: thickness,
|
gradient_angle_or_pattern_height: height,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -401,10 +401,7 @@ impl DispatchTree {
|
|||||||
.bindings_for_action(action)
|
.bindings_for_action(action)
|
||||||
.filter(|binding| {
|
.filter(|binding| {
|
||||||
let (bindings, _) = keymap.bindings_for_input(&binding.keystrokes, context_stack);
|
let (bindings, _) = keymap.bindings_for_input(&binding.keystrokes, context_stack);
|
||||||
bindings
|
bindings.iter().any(|b| b.action.partial_eq(action))
|
||||||
.iter()
|
|
||||||
.next()
|
|
||||||
.is_some_and(|b| b.action.partial_eq(action))
|
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -76,8 +76,9 @@ impl Keystroke {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// key syntax is:
|
/// key syntax is:
|
||||||
/// [ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
|
/// [secondary-][ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
|
||||||
/// key_char syntax is only used for generating test events,
|
/// key_char syntax is only used for generating test events,
|
||||||
|
/// secondary means "cmd" on macOS and "ctrl" on other platforms
|
||||||
/// when matching a key with an key_char set will be matched without it.
|
/// when matching a key with an key_char set will be matched without it.
|
||||||
pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
|
pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||||
let mut control = false;
|
let mut control = false;
|
||||||
@@ -95,6 +96,13 @@ impl Keystroke {
|
|||||||
"alt" => alt = true,
|
"alt" => alt = true,
|
||||||
"shift" => shift = true,
|
"shift" => shift = true,
|
||||||
"fn" => function = true,
|
"fn" => function = true,
|
||||||
|
"secondary" => {
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
platform = true
|
||||||
|
} else {
|
||||||
|
control = true
|
||||||
|
};
|
||||||
|
}
|
||||||
"cmd" | "super" | "win" => platform = true,
|
"cmd" | "super" | "win" => platform = true,
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(next) = components.peek() {
|
if let Some(next) = components.peek() {
|
||||||
|
|||||||
@@ -115,7 +115,6 @@ pub struct WaylandWindowStatePtr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl WaylandWindowState {
|
impl WaylandWindowState {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
handle: AnyWindowHandle,
|
handle: AnyWindowHandle,
|
||||||
surface: wl_surface::WlSurface,
|
surface: wl_surface::WlSurface,
|
||||||
|
|||||||
@@ -353,7 +353,6 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl X11WindowState {
|
impl X11WindowState {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
handle: AnyWindowHandle,
|
handle: AnyWindowHandle,
|
||||||
client: X11ClientStatePtr,
|
client: X11ClientStatePtr,
|
||||||
@@ -712,7 +711,6 @@ enum WmHintPropertyState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl X11Window {
|
impl X11Window {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
handle: AnyWindowHandle,
|
handle: AnyWindowHandle,
|
||||||
client: X11ClientStatePtr,
|
client: X11ClientStatePtr,
|
||||||
|
|||||||
@@ -347,20 +347,7 @@ impl MacPlatform {
|
|||||||
msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO];
|
msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO];
|
||||||
}
|
}
|
||||||
item.setKeyEquivalentModifierMask_(mask);
|
item.setKeyEquivalentModifierMask_(mask);
|
||||||
}
|
} else {
|
||||||
// For multi-keystroke bindings, render the keystroke as part of the title.
|
|
||||||
else {
|
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
let mut name = format!("{name} [");
|
|
||||||
for (i, keystroke) in keystrokes.iter().enumerate() {
|
|
||||||
if i > 0 {
|
|
||||||
name.push(' ');
|
|
||||||
}
|
|
||||||
write!(&mut name, "{}", keystroke).unwrap();
|
|
||||||
}
|
|
||||||
name.push(']');
|
|
||||||
|
|
||||||
item = NSMenuItem::alloc(nil)
|
item = NSMenuItem::alloc(nil)
|
||||||
.initWithTitle_action_keyEquivalent_(
|
.initWithTitle_action_keyEquivalent_(
|
||||||
ns_string(&name),
|
ns_string(&name),
|
||||||
|
|||||||
@@ -268,6 +268,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
|
|||||||
sel!(windowWillEnterFullScreen:),
|
sel!(windowWillEnterFullScreen:),
|
||||||
window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
|
window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
|
||||||
);
|
);
|
||||||
|
decl.add_method(
|
||||||
|
sel!(windowWillExitFullScreen:),
|
||||||
|
window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
|
||||||
|
);
|
||||||
decl.add_method(
|
decl.add_method(
|
||||||
sel!(windowDidMove:),
|
sel!(windowDidMove:),
|
||||||
window_did_move as extern "C" fn(&Object, Sel, id),
|
window_did_move as extern "C" fn(&Object, Sel, id),
|
||||||
@@ -334,6 +338,7 @@ struct MacWindowState {
|
|||||||
last_key_equivalent: Option<KeyDownEvent>,
|
last_key_equivalent: Option<KeyDownEvent>,
|
||||||
synthetic_drag_counter: usize,
|
synthetic_drag_counter: usize,
|
||||||
traffic_light_position: Option<Point<Pixels>>,
|
traffic_light_position: Option<Point<Pixels>>,
|
||||||
|
transparent_titlebar: bool,
|
||||||
previous_modifiers_changed_event: Option<PlatformInput>,
|
previous_modifiers_changed_event: Option<PlatformInput>,
|
||||||
keystroke_for_do_command: Option<Keystroke>,
|
keystroke_for_do_command: Option<Keystroke>,
|
||||||
do_command_handled: Option<bool>,
|
do_command_handled: Option<bool>,
|
||||||
@@ -613,6 +618,9 @@ impl MacWindow {
|
|||||||
traffic_light_position: titlebar
|
traffic_light_position: titlebar
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|titlebar| titlebar.traffic_light_position),
|
.and_then(|titlebar| titlebar.traffic_light_position),
|
||||||
|
transparent_titlebar: titlebar
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |titlebar| titlebar.appears_transparent),
|
||||||
previous_modifiers_changed_event: None,
|
previous_modifiers_changed_event: None,
|
||||||
keystroke_for_do_command: None,
|
keystroke_for_do_command: None,
|
||||||
do_command_handled: None,
|
do_command_handled: None,
|
||||||
@@ -1490,6 +1498,19 @@ extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) {
|
|||||||
let window_state = unsafe { get_window_state(this) };
|
let window_state = unsafe { get_window_state(this) };
|
||||||
let mut lock = window_state.as_ref().lock();
|
let mut lock = window_state.as_ref().lock();
|
||||||
lock.fullscreen_restore_bounds = lock.bounds();
|
lock.fullscreen_restore_bounds = lock.bounds();
|
||||||
|
unsafe {
|
||||||
|
lock.native_window.setTitlebarAppearsTransparent_(NO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) {
|
||||||
|
let window_state = unsafe { get_window_state(this) };
|
||||||
|
let mut lock = window_state.as_ref().lock();
|
||||||
|
if lock.transparent_titlebar {
|
||||||
|
unsafe {
|
||||||
|
lock.native_window.setTitlebarAppearsTransparent_(YES);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
|
extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ fn set_data_to_clipboard<T>(data: &[T], format: u32) -> Result<()> {
|
|||||||
let handle = GlobalLock(global);
|
let handle = GlobalLock(global);
|
||||||
std::ptr::copy_nonoverlapping(data.as_ptr(), handle as _, data.len());
|
std::ptr::copy_nonoverlapping(data.as_ptr(), handle as _, data.len());
|
||||||
let _ = GlobalUnlock(global);
|
let _ = GlobalUnlock(global);
|
||||||
SetClipboardData(format, HANDLE(global.0))?;
|
SetClipboardData(format, Some(HANDLE(global.0)))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1049,7 +1049,7 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
|
|||||||
_measuringmode: DWRITE_MEASURING_MODE,
|
_measuringmode: DWRITE_MEASURING_MODE,
|
||||||
glyphrun: *const DWRITE_GLYPH_RUN,
|
glyphrun: *const DWRITE_GLYPH_RUN,
|
||||||
glyphrundescription: *const DWRITE_GLYPH_RUN_DESCRIPTION,
|
glyphrundescription: *const DWRITE_GLYPH_RUN_DESCRIPTION,
|
||||||
_clientdrawingeffect: Option<&windows::core::IUnknown>,
|
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
|
||||||
) -> windows::core::Result<()> {
|
) -> windows::core::Result<()> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let glyphrun = &*glyphrun;
|
let glyphrun = &*glyphrun;
|
||||||
@@ -1113,7 +1113,7 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
|
|||||||
_baselineoriginx: f32,
|
_baselineoriginx: f32,
|
||||||
_baselineoriginy: f32,
|
_baselineoriginy: f32,
|
||||||
_underline: *const DWRITE_UNDERLINE,
|
_underline: *const DWRITE_UNDERLINE,
|
||||||
_clientdrawingeffect: Option<&windows::core::IUnknown>,
|
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
|
||||||
) -> windows::core::Result<()> {
|
) -> windows::core::Result<()> {
|
||||||
Err(windows::core::Error::new(
|
Err(windows::core::Error::new(
|
||||||
E_NOTIMPL,
|
E_NOTIMPL,
|
||||||
@@ -1127,7 +1127,7 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
|
|||||||
_baselineoriginx: f32,
|
_baselineoriginx: f32,
|
||||||
_baselineoriginy: f32,
|
_baselineoriginy: f32,
|
||||||
_strikethrough: *const DWRITE_STRIKETHROUGH,
|
_strikethrough: *const DWRITE_STRIKETHROUGH,
|
||||||
_clientdrawingeffect: Option<&windows::core::IUnknown>,
|
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
|
||||||
) -> windows::core::Result<()> {
|
) -> windows::core::Result<()> {
|
||||||
Err(windows::core::Error::new(
|
Err(windows::core::Error::new(
|
||||||
E_NOTIMPL,
|
E_NOTIMPL,
|
||||||
@@ -1140,10 +1140,10 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
|
|||||||
_clientdrawingcontext: *const ::core::ffi::c_void,
|
_clientdrawingcontext: *const ::core::ffi::c_void,
|
||||||
_originx: f32,
|
_originx: f32,
|
||||||
_originy: f32,
|
_originy: f32,
|
||||||
_inlineobject: Option<&IDWriteInlineObject>,
|
_inlineobject: windows::core::Ref<IDWriteInlineObject>,
|
||||||
_issideways: BOOL,
|
_issideways: BOOL,
|
||||||
_isrighttoleft: BOOL,
|
_isrighttoleft: BOOL,
|
||||||
_clientdrawingeffect: Option<&windows::core::IUnknown>,
|
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
|
||||||
) -> windows::core::Result<()> {
|
) -> windows::core::Result<()> {
|
||||||
Err(windows::core::Error::new(
|
Err(windows::core::Error::new(
|
||||||
E_NOTIMPL,
|
E_NOTIMPL,
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ fn available_monitors() -> SmallVec<[HMONITOR; 4]> {
|
|||||||
let mut monitors: SmallVec<[HMONITOR; 4]> = SmallVec::new();
|
let mut monitors: SmallVec<[HMONITOR; 4]> = SmallVec::new();
|
||||||
unsafe {
|
unsafe {
|
||||||
EnumDisplayMonitors(
|
EnumDisplayMonitors(
|
||||||
HDC::default(),
|
None,
|
||||||
None,
|
None,
|
||||||
Some(monitor_enum_proc),
|
Some(monitor_enum_proc),
|
||||||
LPARAM(&mut monitors as *mut _ as _),
|
LPARAM(&mut monitors as *mut _ as _),
|
||||||
|
|||||||
@@ -177,7 +177,12 @@ fn handle_size_msg(
|
|||||||
|
|
||||||
fn handle_size_move_loop(handle: HWND) -> Option<isize> {
|
fn handle_size_move_loop(handle: HWND) -> Option<isize> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let ret = SetTimer(handle, SIZE_MOVE_LOOP_TIMER_ID, USER_TIMER_MINIMUM, None);
|
let ret = SetTimer(
|
||||||
|
Some(handle),
|
||||||
|
SIZE_MOVE_LOOP_TIMER_ID,
|
||||||
|
USER_TIMER_MINIMUM,
|
||||||
|
None,
|
||||||
|
);
|
||||||
if ret == 0 {
|
if ret == 0 {
|
||||||
log::error!(
|
log::error!(
|
||||||
"unable to create timer: {}",
|
"unable to create timer: {}",
|
||||||
@@ -190,7 +195,7 @@ fn handle_size_move_loop(handle: HWND) -> Option<isize> {
|
|||||||
|
|
||||||
fn handle_size_move_loop_exit(handle: HWND) -> Option<isize> {
|
fn handle_size_move_loop_exit(handle: HWND) -> Option<isize> {
|
||||||
unsafe {
|
unsafe {
|
||||||
KillTimer(handle, SIZE_MOVE_LOOP_TIMER_ID).log_err();
|
KillTimer(Some(handle), SIZE_MOVE_LOOP_TIMER_ID).log_err();
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -217,7 +222,7 @@ fn handle_paint_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Optio
|
|||||||
request_frame(Default::default());
|
request_frame(Default::default());
|
||||||
state_ptr.state.borrow_mut().callbacks.request_frame = Some(request_frame);
|
state_ptr.state.borrow_mut().callbacks.request_frame = Some(request_frame);
|
||||||
}
|
}
|
||||||
unsafe { ValidateRect(handle, None).ok().log_err() };
|
unsafe { ValidateRect(Some(handle), None).ok().log_err() };
|
||||||
Some(0)
|
Some(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,7 +781,7 @@ fn handle_activate_msg(
|
|||||||
if state_ptr.hide_title_bar {
|
if state_ptr.hide_title_bar {
|
||||||
if let Some(titlebar_rect) = state_ptr.state.borrow().get_titlebar_rect().log_err() {
|
if let Some(titlebar_rect) = state_ptr.state.borrow().get_titlebar_rect().log_err() {
|
||||||
unsafe {
|
unsafe {
|
||||||
InvalidateRect(handle, Some(&titlebar_rect), FALSE)
|
InvalidateRect(Some(handle), Some(&titlebar_rect), false)
|
||||||
.ok()
|
.ok()
|
||||||
.log_err()
|
.log_err()
|
||||||
};
|
};
|
||||||
@@ -1105,7 +1110,7 @@ fn handle_nc_mouse_up_msg(
|
|||||||
HTCLOSE => {
|
HTCLOSE => {
|
||||||
if last_button == HTCLOSE {
|
if last_button == HTCLOSE {
|
||||||
unsafe {
|
unsafe {
|
||||||
PostMessageW(handle, WM_CLOSE, WPARAM::default(), LPARAM::default())
|
PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default())
|
||||||
.log_err()
|
.log_err()
|
||||||
};
|
};
|
||||||
handled = true;
|
handled = true;
|
||||||
@@ -1133,7 +1138,7 @@ fn handle_set_cursor(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Op
|
|||||||
) {
|
) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
unsafe { SetCursor(state_ptr.state.borrow().current_cursor) };
|
unsafe { SetCursor(Some(state_ptr.state.borrow().current_cursor)) };
|
||||||
Some(1)
|
Some(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,14 +130,9 @@ impl WindowsPlatform {
|
|||||||
fn redraw_all(&self) {
|
fn redraw_all(&self) {
|
||||||
for handle in self.raw_window_handles.read().iter() {
|
for handle in self.raw_window_handles.read().iter() {
|
||||||
unsafe {
|
unsafe {
|
||||||
RedrawWindow(
|
RedrawWindow(Some(*handle), None, None, RDW_INVALIDATE | RDW_UPDATENOW)
|
||||||
*handle,
|
.ok()
|
||||||
None,
|
.log_err();
|
||||||
HRGN::default(),
|
|
||||||
RDW_INVALIDATE | RDW_UPDATENOW,
|
|
||||||
)
|
|
||||||
.ok()
|
|
||||||
.log_err();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +151,7 @@ impl WindowsPlatform {
|
|||||||
.read()
|
.read()
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|handle| unsafe {
|
.for_each(|handle| unsafe {
|
||||||
PostMessageW(*handle, message, wparam, lparam).log_err();
|
PostMessageW(Some(*handle), message, wparam, lparam).log_err();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,7 +615,7 @@ impl Platform for WindowsPlatform {
|
|||||||
CredReadW(
|
CredReadW(
|
||||||
PCWSTR::from_raw(target_name.as_ptr()),
|
PCWSTR::from_raw(target_name.as_ptr()),
|
||||||
CRED_TYPE_GENERIC,
|
CRED_TYPE_GENERIC,
|
||||||
0,
|
None,
|
||||||
&mut credentials,
|
&mut credentials,
|
||||||
)?
|
)?
|
||||||
};
|
};
|
||||||
@@ -648,7 +643,13 @@ impl Platform for WindowsPlatform {
|
|||||||
.chain(Some(0))
|
.chain(Some(0))
|
||||||
.collect_vec();
|
.collect_vec();
|
||||||
self.foreground_executor().spawn(async move {
|
self.foreground_executor().spawn(async move {
|
||||||
unsafe { CredDeleteW(PCWSTR::from_raw(target_name.as_ptr()), CRED_TYPE_GENERIC, 0)? };
|
unsafe {
|
||||||
|
CredDeleteW(
|
||||||
|
PCWSTR::from_raw(target_name.as_ptr()),
|
||||||
|
CRED_TYPE_GENERIC,
|
||||||
|
None,
|
||||||
|
)?
|
||||||
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -805,7 +806,7 @@ fn load_icon() -> Result<HICON> {
|
|||||||
let module = unsafe { GetModuleHandleW(None).context("unable to get module handle")? };
|
let module = unsafe { GetModuleHandleW(None).context("unable to get module handle")? };
|
||||||
let handle = unsafe {
|
let handle = unsafe {
|
||||||
LoadImageW(
|
LoadImageW(
|
||||||
module,
|
Some(module.into()),
|
||||||
windows::core::PCWSTR(1 as _),
|
windows::core::PCWSTR(1 as _),
|
||||||
IMAGE_ICON,
|
IMAGE_ICON,
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::sync::OnceLock;
|
|||||||
|
|
||||||
use ::util::ResultExt;
|
use ::util::ResultExt;
|
||||||
use windows::{
|
use windows::{
|
||||||
|
core::BOOL,
|
||||||
Wdk::System::SystemServices::RtlGetVersion,
|
Wdk::System::SystemServices::RtlGetVersion,
|
||||||
Win32::{Foundation::*, Graphics::Dwm::*, UI::WindowsAndMessaging::*},
|
Win32::{Foundation::*, Graphics::Dwm::*, UI::WindowsAndMessaging::*},
|
||||||
UI::{
|
UI::{
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ impl WindowsWindowStatePtr {
|
|||||||
unsafe {
|
unsafe {
|
||||||
SetWindowPos(
|
SetWindowPos(
|
||||||
state_ptr.hwnd,
|
state_ptr.hwnd,
|
||||||
HWND::default(),
|
None,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
cx,
|
cx,
|
||||||
@@ -433,7 +433,7 @@ impl WindowsWindow {
|
|||||||
CW_USEDEFAULT,
|
CW_USEDEFAULT,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
hinstance,
|
Some(hinstance.into()),
|
||||||
lpparam,
|
lpparam,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -650,7 +650,7 @@ impl PlatformWindow for WindowsWindow {
|
|||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
this.set_window_placement().log_err();
|
this.set_window_placement().log_err();
|
||||||
unsafe { SetActiveWindow(hwnd).log_err() };
|
unsafe { SetActiveWindow(hwnd).log_err() };
|
||||||
unsafe { SetFocus(hwnd).log_err() };
|
unsafe { SetFocus(Some(hwnd)).log_err() };
|
||||||
// todo(windows)
|
// todo(windows)
|
||||||
// crate `windows 0.56` reports true as Err
|
// crate `windows 0.56` reports true as Err
|
||||||
unsafe { SetForegroundWindow(hwnd).as_bool() };
|
unsafe { SetForegroundWindow(hwnd).as_bool() };
|
||||||
@@ -817,16 +817,13 @@ impl WindowsDragDropHandler {
|
|||||||
impl IDropTarget_Impl for WindowsDragDropHandler_Impl {
|
impl IDropTarget_Impl for WindowsDragDropHandler_Impl {
|
||||||
fn DragEnter(
|
fn DragEnter(
|
||||||
&self,
|
&self,
|
||||||
pdataobj: Option<&IDataObject>,
|
pdataobj: windows::core::Ref<IDataObject>,
|
||||||
_grfkeystate: MODIFIERKEYS_FLAGS,
|
_grfkeystate: MODIFIERKEYS_FLAGS,
|
||||||
pt: &POINTL,
|
pt: &POINTL,
|
||||||
pdweffect: *mut DROPEFFECT,
|
pdweffect: *mut DROPEFFECT,
|
||||||
) -> windows::core::Result<()> {
|
) -> windows::core::Result<()> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let Some(idata_obj) = pdataobj else {
|
let idata_obj = pdataobj.ok()?;
|
||||||
log::info!("no dragging file or directory detected");
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let config = FORMATETC {
|
let config = FORMATETC {
|
||||||
cfFormat: CF_HDROP.0,
|
cfFormat: CF_HDROP.0,
|
||||||
ptd: std::ptr::null_mut() as _,
|
ptd: std::ptr::null_mut() as _,
|
||||||
@@ -905,7 +902,7 @@ impl IDropTarget_Impl for WindowsDragDropHandler_Impl {
|
|||||||
|
|
||||||
fn Drop(
|
fn Drop(
|
||||||
&self,
|
&self,
|
||||||
_pdataobj: Option<&IDataObject>,
|
_pdataobj: windows::core::Ref<IDataObject>,
|
||||||
_grfkeystate: MODIFIERKEYS_FLAGS,
|
_grfkeystate: MODIFIERKEYS_FLAGS,
|
||||||
pt: &POINTL,
|
pt: &POINTL,
|
||||||
_pdweffect: *mut DROPEFFECT,
|
_pdweffect: *mut DROPEFFECT,
|
||||||
|
|||||||
@@ -81,6 +81,29 @@ impl ShapedLine {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Paint the background of the line to the window.
|
||||||
|
pub fn paint_background(
|
||||||
|
&self,
|
||||||
|
origin: Point<Pixels>,
|
||||||
|
line_height: Pixels,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Result<()> {
|
||||||
|
paint_line_background(
|
||||||
|
origin,
|
||||||
|
&self.layout,
|
||||||
|
line_height,
|
||||||
|
TextAlign::default(),
|
||||||
|
None,
|
||||||
|
&self.decoration_runs,
|
||||||
|
&[],
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A line of text that has been shaped, decorated, and wrapped by the text layout system.
|
/// A line of text that has been shaped, decorated, and wrapped by the text layout system.
|
||||||
@@ -132,7 +155,6 @@ impl WrappedLine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn paint_line(
|
fn paint_line(
|
||||||
origin: Point<Pixels>,
|
origin: Point<Pixels>,
|
||||||
layout: &LineLayout,
|
layout: &LineLayout,
|
||||||
@@ -160,7 +182,6 @@ fn paint_line(
|
|||||||
let mut color = black();
|
let mut color = black();
|
||||||
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
||||||
let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
|
let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
|
||||||
let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
|
|
||||||
let text_system = cx.text_system().clone();
|
let text_system = cx.text_system().clone();
|
||||||
let mut glyph_origin = point(
|
let mut glyph_origin = point(
|
||||||
aligned_origin_x(
|
aligned_origin_x(
|
||||||
@@ -183,21 +204,6 @@ fn paint_line(
|
|||||||
|
|
||||||
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
|
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
|
||||||
wraps.next();
|
wraps.next();
|
||||||
if let Some((background_origin, background_color)) = current_background.as_mut()
|
|
||||||
{
|
|
||||||
if glyph_origin.x == background_origin.x {
|
|
||||||
background_origin.x -= max_glyph_size.width.half()
|
|
||||||
}
|
|
||||||
window.paint_quad(fill(
|
|
||||||
Bounds {
|
|
||||||
origin: *background_origin,
|
|
||||||
size: size(glyph_origin.x - background_origin.x, line_height),
|
|
||||||
},
|
|
||||||
*background_color,
|
|
||||||
));
|
|
||||||
background_origin.x = origin.x;
|
|
||||||
background_origin.y += line_height;
|
|
||||||
}
|
|
||||||
if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
|
if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
|
||||||
if glyph_origin.x == underline_origin.x {
|
if glyph_origin.x == underline_origin.x {
|
||||||
underline_origin.x -= max_glyph_size.width.half();
|
underline_origin.x -= max_glyph_size.width.half();
|
||||||
@@ -237,7 +243,6 @@ fn paint_line(
|
|||||||
}
|
}
|
||||||
prev_glyph_position = glyph.position;
|
prev_glyph_position = glyph.position;
|
||||||
|
|
||||||
let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
|
|
||||||
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
||||||
let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
|
let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
|
||||||
if glyph.index >= run_end {
|
if glyph.index >= run_end {
|
||||||
@@ -253,18 +258,6 @@ fn paint_line(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(style_run) = style_run {
|
if let Some(style_run) = style_run {
|
||||||
if let Some((_, background_color)) = &mut current_background {
|
|
||||||
if style_run.background_color.as_ref() != Some(background_color) {
|
|
||||||
finished_background = current_background.take();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(run_background) = style_run.background_color {
|
|
||||||
current_background.get_or_insert((
|
|
||||||
point(glyph_origin.x, glyph_origin.y),
|
|
||||||
run_background,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((_, underline_style)) = &mut current_underline {
|
if let Some((_, underline_style)) = &mut current_underline {
|
||||||
if style_run.underline.as_ref() != Some(underline_style) {
|
if style_run.underline.as_ref() != Some(underline_style) {
|
||||||
finished_underline = current_underline.take();
|
finished_underline = current_underline.take();
|
||||||
@@ -306,26 +299,11 @@ fn paint_line(
|
|||||||
color = style_run.color;
|
color = style_run.color;
|
||||||
} else {
|
} else {
|
||||||
run_end = layout.len;
|
run_end = layout.len;
|
||||||
finished_background = current_background.take();
|
|
||||||
finished_underline = current_underline.take();
|
finished_underline = current_underline.take();
|
||||||
finished_strikethrough = current_strikethrough.take();
|
finished_strikethrough = current_strikethrough.take();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((mut background_origin, background_color)) = finished_background {
|
|
||||||
let mut width = glyph_origin.x - background_origin.x;
|
|
||||||
if background_origin.x == glyph_origin.x {
|
|
||||||
background_origin.x -= max_glyph_size.width.half();
|
|
||||||
};
|
|
||||||
window.paint_quad(fill(
|
|
||||||
Bounds {
|
|
||||||
origin: background_origin,
|
|
||||||
size: size(width, line_height),
|
|
||||||
},
|
|
||||||
background_color,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((mut underline_origin, underline_style)) = finished_underline {
|
if let Some((mut underline_origin, underline_style)) = finished_underline {
|
||||||
if underline_origin.x == glyph_origin.x {
|
if underline_origin.x == glyph_origin.x {
|
||||||
underline_origin.x -= max_glyph_size.width.half();
|
underline_origin.x -= max_glyph_size.width.half();
|
||||||
@@ -384,19 +362,6 @@ fn paint_line(
|
|||||||
last_line_end_x -= glyph.position.x;
|
last_line_end_x -= glyph.position.x;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((mut background_origin, background_color)) = current_background.take() {
|
|
||||||
if last_line_end_x == background_origin.x {
|
|
||||||
background_origin.x -= max_glyph_size.width.half()
|
|
||||||
};
|
|
||||||
window.paint_quad(fill(
|
|
||||||
Bounds {
|
|
||||||
origin: background_origin,
|
|
||||||
size: size(last_line_end_x - background_origin.x, line_height),
|
|
||||||
},
|
|
||||||
background_color,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((mut underline_start, underline_style)) = current_underline.take() {
|
if let Some((mut underline_start, underline_style)) = current_underline.take() {
|
||||||
if last_line_end_x == underline_start.x {
|
if last_line_end_x == underline_start.x {
|
||||||
underline_start.x -= max_glyph_size.width.half()
|
underline_start.x -= max_glyph_size.width.half()
|
||||||
@@ -423,6 +388,141 @@ fn paint_line(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paint_line_background(
|
||||||
|
origin: Point<Pixels>,
|
||||||
|
layout: &LineLayout,
|
||||||
|
line_height: Pixels,
|
||||||
|
align: TextAlign,
|
||||||
|
align_width: Option<Pixels>,
|
||||||
|
decoration_runs: &[DecorationRun],
|
||||||
|
wrap_boundaries: &[WrapBoundary],
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Result<()> {
|
||||||
|
let line_bounds = Bounds::new(
|
||||||
|
origin,
|
||||||
|
size(
|
||||||
|
layout.width,
|
||||||
|
line_height * (wrap_boundaries.len() as f32 + 1.),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
window.paint_layer(line_bounds, |window| {
|
||||||
|
let mut decoration_runs = decoration_runs.iter();
|
||||||
|
let mut wraps = wrap_boundaries.iter().peekable();
|
||||||
|
let mut run_end = 0;
|
||||||
|
let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
|
||||||
|
let text_system = cx.text_system().clone();
|
||||||
|
let mut glyph_origin = point(
|
||||||
|
aligned_origin_x(
|
||||||
|
origin,
|
||||||
|
align_width.unwrap_or(layout.width),
|
||||||
|
px(0.0),
|
||||||
|
&align,
|
||||||
|
layout,
|
||||||
|
wraps.peek(),
|
||||||
|
),
|
||||||
|
origin.y,
|
||||||
|
);
|
||||||
|
let mut prev_glyph_position = Point::default();
|
||||||
|
let mut max_glyph_size = size(px(0.), px(0.));
|
||||||
|
for (run_ix, run) in layout.runs.iter().enumerate() {
|
||||||
|
max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
|
||||||
|
|
||||||
|
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
|
||||||
|
glyph_origin.x += glyph.position.x - prev_glyph_position.x;
|
||||||
|
|
||||||
|
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
|
||||||
|
wraps.next();
|
||||||
|
if let Some((background_origin, background_color)) = current_background.as_mut()
|
||||||
|
{
|
||||||
|
if glyph_origin.x == background_origin.x {
|
||||||
|
background_origin.x -= max_glyph_size.width.half()
|
||||||
|
}
|
||||||
|
window.paint_quad(fill(
|
||||||
|
Bounds {
|
||||||
|
origin: *background_origin,
|
||||||
|
size: size(glyph_origin.x - background_origin.x, line_height),
|
||||||
|
},
|
||||||
|
*background_color,
|
||||||
|
));
|
||||||
|
background_origin.x = origin.x;
|
||||||
|
background_origin.y += line_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prev_glyph_position = glyph.position;
|
||||||
|
|
||||||
|
let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
|
||||||
|
if glyph.index >= run_end {
|
||||||
|
let mut style_run = decoration_runs.next();
|
||||||
|
|
||||||
|
// ignore style runs that apply to a partial glyph
|
||||||
|
while let Some(run) = style_run {
|
||||||
|
if glyph.index < run_end + (run.len as usize) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
run_end += run.len as usize;
|
||||||
|
style_run = decoration_runs.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(style_run) = style_run {
|
||||||
|
if let Some((_, background_color)) = &mut current_background {
|
||||||
|
if style_run.background_color.as_ref() != Some(background_color) {
|
||||||
|
finished_background = current_background.take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(run_background) = style_run.background_color {
|
||||||
|
current_background.get_or_insert((
|
||||||
|
point(glyph_origin.x, glyph_origin.y),
|
||||||
|
run_background,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
run_end += style_run.len as usize;
|
||||||
|
} else {
|
||||||
|
run_end = layout.len;
|
||||||
|
finished_background = current_background.take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((mut background_origin, background_color)) = finished_background {
|
||||||
|
let mut width = glyph_origin.x - background_origin.x;
|
||||||
|
if background_origin.x == glyph_origin.x {
|
||||||
|
background_origin.x -= max_glyph_size.width.half();
|
||||||
|
};
|
||||||
|
window.paint_quad(fill(
|
||||||
|
Bounds {
|
||||||
|
origin: background_origin,
|
||||||
|
size: size(width, line_height),
|
||||||
|
},
|
||||||
|
background_color,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut last_line_end_x = origin.x + layout.width;
|
||||||
|
if let Some(boundary) = wrap_boundaries.last() {
|
||||||
|
let run = &layout.runs[boundary.run_ix];
|
||||||
|
let glyph = &run.glyphs[boundary.glyph_ix];
|
||||||
|
last_line_end_x -= glyph.position.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((mut background_origin, background_color)) = current_background.take() {
|
||||||
|
if last_line_end_x == background_origin.x {
|
||||||
|
background_origin.x -= max_glyph_size.width.half()
|
||||||
|
};
|
||||||
|
window.paint_quad(fill(
|
||||||
|
Bounds {
|
||||||
|
origin: background_origin,
|
||||||
|
size: size(last_line_end_x - background_origin.x, line_height),
|
||||||
|
},
|
||||||
|
background_color,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn aligned_origin_x(
|
fn aligned_origin_x(
|
||||||
origin: Point<Pixels>,
|
origin: Point<Pixels>,
|
||||||
align_width: Pixels,
|
align_width: Pixels,
|
||||||
|
|||||||
@@ -1250,7 +1250,6 @@ fn parse_text(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn get_injections(
|
fn get_injections(
|
||||||
config: &InjectionConfig,
|
config: &InjectionConfig,
|
||||||
text: &BufferSnapshot,
|
text: &BufferSnapshot,
|
||||||
|
|||||||
@@ -301,7 +301,6 @@ pub struct AdapterServerCapabilities {
|
|||||||
|
|
||||||
impl LanguageServer {
|
impl LanguageServer {
|
||||||
/// Starts a language server process.
|
/// Starts a language server process.
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
stderr_capture: Arc<Mutex<Option<String>>>,
|
stderr_capture: Arc<Mutex<Option<String>>>,
|
||||||
server_id: LanguageServerId,
|
server_id: LanguageServerId,
|
||||||
@@ -372,7 +371,6 @@ impl LanguageServer {
|
|||||||
Ok(server)
|
Ok(server)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn new_internal<Stdin, Stdout, Stderr, F>(
|
fn new_internal<Stdin, Stdout, Stderr, F>(
|
||||||
server_id: LanguageServerId,
|
server_id: LanguageServerId,
|
||||||
server_name: LanguageServerName,
|
server_name: LanguageServerName,
|
||||||
|
|||||||
@@ -2147,6 +2147,7 @@ impl MultiBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited);
|
self.sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited);
|
||||||
|
self.buffer_changed_since_sync.replace(true);
|
||||||
cx.emit(Event::Edited {
|
cx.emit(Event::Edited {
|
||||||
singleton_buffer_edited: false,
|
singleton_buffer_edited: false,
|
||||||
edited_buffer: None,
|
edited_buffer: None,
|
||||||
@@ -2982,7 +2983,6 @@ impl MultiBuffer {
|
|||||||
snapshot.check_invariants();
|
snapshot.check_invariants();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn recompute_diff_transforms_for_edit(
|
fn recompute_diff_transforms_for_edit(
|
||||||
&self,
|
&self,
|
||||||
edit: &Edit<TypedOffset<Excerpt>>,
|
edit: &Edit<TypedOffset<Excerpt>>,
|
||||||
@@ -4176,6 +4176,9 @@ impl MultiBufferSnapshot {
|
|||||||
let region = cursor.region()?;
|
let region = cursor.region()?;
|
||||||
let overshoot = offset - region.range.start;
|
let overshoot = offset - region.range.start;
|
||||||
let buffer_offset = region.buffer_range.start + overshoot;
|
let buffer_offset = region.buffer_range.start + overshoot;
|
||||||
|
if buffer_offset > region.buffer.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
Some((region.buffer, buffer_offset))
|
Some((region.buffer, buffer_offset))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4184,8 +4187,11 @@ impl MultiBufferSnapshot {
|
|||||||
cursor.seek(&point);
|
cursor.seek(&point);
|
||||||
let region = cursor.region()?;
|
let region = cursor.region()?;
|
||||||
let overshoot = point - region.range.start;
|
let overshoot = point - region.range.start;
|
||||||
let buffer_offset = region.buffer_range.start + overshoot;
|
let buffer_point = region.buffer_range.start + overshoot;
|
||||||
Some((region.buffer, buffer_offset, region.is_main_buffer))
|
if buffer_point > region.buffer.max_point() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((region.buffer, buffer_point, region.is_main_buffer))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn suggested_indents(
|
pub fn suggested_indents(
|
||||||
|
|||||||
@@ -3378,6 +3378,17 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let point = snapshot.max_point();
|
||||||
|
let Some((buffer, offset)) = snapshot.point_to_buffer_offset(point) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
assert!(offset <= buffer.len(),);
|
||||||
|
|
||||||
|
let Some((buffer, point, _)) = snapshot.point_to_buffer_point(point) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
assert!(point <= buffer.max_point(),);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_line_indents(snapshot: &MultiBufferSnapshot) {
|
fn assert_line_indents(snapshot: &MultiBufferSnapshot) {
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement};
|
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement};
|
||||||
use ui::prelude::*;
|
use ui::{prelude::*, Tooltip};
|
||||||
use workspace::ToastView;
|
use workspace::{ToastAction, ToastView};
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ToastAction {
|
|
||||||
id: ElementId,
|
|
||||||
label: SharedString,
|
|
||||||
on_click: Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct ToastIcon {
|
pub struct ToastIcon {
|
||||||
@@ -40,49 +33,33 @@ impl From<IconName> for ToastIcon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToastAction {
|
|
||||||
pub fn new(
|
|
||||||
label: SharedString,
|
|
||||||
on_click: Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
|
||||||
) -> Self {
|
|
||||||
let id = ElementId::Name(label.clone());
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
label,
|
|
||||||
on_click,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(IntoComponent)]
|
#[derive(IntoComponent)]
|
||||||
#[component(scope = "Notification")]
|
#[component(scope = "Notification")]
|
||||||
pub struct StatusToast {
|
pub struct StatusToast {
|
||||||
icon: Option<ToastIcon>,
|
icon: Option<ToastIcon>,
|
||||||
text: SharedString,
|
text: SharedString,
|
||||||
action: Option<ToastAction>,
|
action: Option<ToastAction>,
|
||||||
|
this_handle: Entity<Self>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusToast {
|
impl StatusToast {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
text: impl Into<SharedString>,
|
text: impl Into<SharedString>,
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
|
f: impl FnOnce(Self, &mut Context<Self>) -> Self,
|
||||||
) -> Entity<Self> {
|
) -> Entity<Self> {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
|
|
||||||
window.refresh();
|
|
||||||
f(
|
f(
|
||||||
Self {
|
Self {
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
icon: None,
|
icon: None,
|
||||||
action: None,
|
action: None,
|
||||||
|
this_handle: cx.entity(),
|
||||||
focus_handle,
|
focus_handle,
|
||||||
},
|
},
|
||||||
window,
|
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -96,9 +73,18 @@ impl StatusToast {
|
|||||||
pub fn action(
|
pub fn action(
|
||||||
mut self,
|
mut self,
|
||||||
label: impl Into<SharedString>,
|
label: impl Into<SharedString>,
|
||||||
f: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
f: impl Fn(&mut Window, &mut App) + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.action = Some(ToastAction::new(label.into(), Some(Arc::new(f))));
|
let this_handle = self.this_handle.clone();
|
||||||
|
self.action = Some(ToastAction::new(
|
||||||
|
label.into(),
|
||||||
|
Some(Rc::new(move |window, cx| {
|
||||||
|
this_handle.update(cx, |_, cx| {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
});
|
||||||
|
f(window, cx);
|
||||||
|
})),
|
||||||
|
));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,18 +108,24 @@ impl Render for StatusToast {
|
|||||||
.when_some(self.action.as_ref(), |this, action| {
|
.when_some(self.action.as_ref(), |this, action| {
|
||||||
this.child(
|
this.child(
|
||||||
Button::new(action.id.clone(), action.label.clone())
|
Button::new(action.id.clone(), action.label.clone())
|
||||||
|
.tooltip(Tooltip::for_action_title(
|
||||||
|
action.label.clone(),
|
||||||
|
&workspace::RunAction,
|
||||||
|
))
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.when_some(action.on_click.clone(), |el, handler| {
|
.when_some(action.on_click.clone(), |el, handler| {
|
||||||
el.on_click(move |click_event, window, cx| {
|
el.on_click(move |_click_event, window, cx| handler(window, cx))
|
||||||
handler(click_event, window, cx)
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToastView for StatusToast {}
|
impl ToastView for StatusToast {
|
||||||
|
fn action(&self) -> Option<ToastAction> {
|
||||||
|
self.action.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Focusable for StatusToast {
|
impl Focusable for StatusToast {
|
||||||
fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
|
fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
|
||||||
@@ -144,56 +136,44 @@ impl Focusable for StatusToast {
|
|||||||
impl EventEmitter<DismissEvent> for StatusToast {}
|
impl EventEmitter<DismissEvent> for StatusToast {}
|
||||||
|
|
||||||
impl ComponentPreview for StatusToast {
|
impl ComponentPreview for StatusToast {
|
||||||
fn preview(window: &mut Window, cx: &mut App) -> AnyElement {
|
fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
|
||||||
let text_example = StatusToast::new("Operation completed", window, cx, |this, _, _| this);
|
let text_example = StatusToast::new("Operation completed", cx, |this, _| this);
|
||||||
|
|
||||||
let action_example =
|
let action_example = StatusToast::new("Update ready to install", cx, |this, _cx| {
|
||||||
StatusToast::new("Update ready to install", window, cx, |this, _, cx| {
|
this.action("Restart", |_, _| {})
|
||||||
this.action("Restart", cx.listener(|_, _, _, _| {}))
|
});
|
||||||
});
|
|
||||||
|
|
||||||
let icon_example = StatusToast::new(
|
let icon_example = StatusToast::new(
|
||||||
"Nathan Sobo accepted your contact request",
|
"Nathan Sobo accepted your contact request",
|
||||||
window,
|
|
||||||
cx,
|
cx,
|
||||||
|this, _, _| this.icon(ToastIcon::new(IconName::Check).color(Color::Muted)),
|
|this, _| this.icon(ToastIcon::new(IconName::Check).color(Color::Muted)),
|
||||||
);
|
);
|
||||||
|
|
||||||
let success_example = StatusToast::new(
|
let success_example = StatusToast::new("Pushed 4 changes to `zed/main`", cx, |this, _| {
|
||||||
"Pushed 4 changes to `zed/main`",
|
this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
|
||||||
window,
|
});
|
||||||
cx,
|
|
||||||
|this, _, _| this.icon(ToastIcon::new(IconName::Check).color(Color::Success)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let error_example = StatusToast::new(
|
let error_example = StatusToast::new(
|
||||||
"git push: Couldn't find remote origin `iamnbutler/zed`",
|
"git push: Couldn't find remote origin `iamnbutler/zed`",
|
||||||
window,
|
|
||||||
cx,
|
cx,
|
||||||
|this, _, cx| {
|
|this, _cx| {
|
||||||
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
||||||
.action("More Info", cx.listener(|_, _, _, _| {}))
|
.action("More Info", |_, _| {})
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let warning_example =
|
let warning_example = StatusToast::new("You have outdated settings", cx, |this, _cx| {
|
||||||
StatusToast::new("You have outdated settings", window, cx, |this, _, cx| {
|
this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
|
||||||
this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
|
.action("More Info", |_, _| {})
|
||||||
.action("More Info", cx.listener(|_, _, _, _| {}))
|
});
|
||||||
});
|
|
||||||
|
|
||||||
let pr_example = StatusToast::new(
|
let pr_example =
|
||||||
"`zed/new-notification-system` created!",
|
StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
|this, _, cx| {
|
|
||||||
this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
|
this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
|
||||||
.action(
|
.action("Open Pull Request", |_, cx| {
|
||||||
"Open Pull Request",
|
cx.open_url("https://github.com/")
|
||||||
cx.listener(|_, _, _, cx| cx.open_url("https://github.com/")),
|
})
|
||||||
)
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_6()
|
.gap_6()
|
||||||
|
|||||||
@@ -2360,7 +2360,6 @@ impl OutlinePanel {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn render_search_match(
|
fn render_search_match(
|
||||||
&mut self,
|
&mut self,
|
||||||
multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
|
multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
|
||||||
@@ -2452,7 +2451,6 @@ impl OutlinePanel {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn entry_element(
|
fn entry_element(
|
||||||
&self,
|
&self,
|
||||||
rendered_entry: PanelEntry,
|
rendered_entry: PanelEntry,
|
||||||
@@ -3836,7 +3834,6 @@ impl OutlinePanel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn push_entry(
|
fn push_entry(
|
||||||
&self,
|
&self,
|
||||||
state: &mut GenerationState,
|
state: &mut GenerationState,
|
||||||
@@ -4054,7 +4051,6 @@ impl OutlinePanel {
|
|||||||
update_cached_entries
|
update_cached_entries
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn add_excerpt_entries(
|
fn add_excerpt_entries(
|
||||||
&self,
|
&self,
|
||||||
state: &mut GenerationState,
|
state: &mut GenerationState,
|
||||||
@@ -4113,7 +4109,6 @@ impl OutlinePanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn add_search_entries(
|
fn add_search_entries(
|
||||||
&mut self,
|
&mut self,
|
||||||
state: &mut GenerationState,
|
state: &mut GenerationState,
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ pub trait PanelHeader: workspace::Panel {
|
|||||||
.w_full()
|
.w_full()
|
||||||
.px_1()
|
.px_1()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.border_b_1()
|
|
||||||
.border_color(cx.theme().colors().border)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ pub trait PickerDelegate: Sized + 'static {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str>;
|
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str>;
|
||||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
|
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||||
"No matches".into()
|
Some("No matches".into())
|
||||||
}
|
}
|
||||||
fn update_matches(
|
fn update_matches(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -844,18 +844,17 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(self.delegate.match_count() == 0, |el| {
|
.when(self.delegate.match_count() == 0, |el| {
|
||||||
el.child(
|
el.when_some(self.delegate.no_matches_text(window, cx), |el, text| {
|
||||||
v_flex().flex_grow().py_2().child(
|
el.child(
|
||||||
ListItem::new("empty_state")
|
v_flex().flex_grow().py_2().child(
|
||||||
.inset(true)
|
ListItem::new("empty_state")
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.inset(true)
|
||||||
.disabled(true)
|
.spacing(ListItemSpacing::Sparse)
|
||||||
.child(
|
.disabled(true)
|
||||||
Label::new(self.delegate.no_matches_text(window, cx))
|
.child(Label::new(text).color(Color::Muted)),
|
||||||
.color(Color::Muted),
|
),
|
||||||
),
|
)
|
||||||
),
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.children(self.delegate.render_footer(window, cx))
|
.children(self.delegate.render_footer(window, cx))
|
||||||
.children(match &self.head {
|
.children(match &self.head {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
|
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
|
||||||
use anyhow::{anyhow, bail, Context as _, Result};
|
use anyhow::{anyhow, bail, Context as _, Result};
|
||||||
use buffer_diff::{BufferDiff, BufferDiffEvent};
|
use buffer_diff::BufferDiff;
|
||||||
use client::Client;
|
use client::Client;
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
use collections::{hash_map, HashMap, HashSet};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
@@ -15,7 +15,6 @@ use git::{blame::Blame, repository::RepoPath};
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity,
|
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity,
|
||||||
};
|
};
|
||||||
use http_client::Url;
|
|
||||||
use language::{
|
use language::{
|
||||||
proto::{
|
proto::{
|
||||||
deserialize_line_ending, deserialize_version, serialize_line_ending, serialize_version,
|
deserialize_line_ending, deserialize_version, serialize_line_ending, serialize_version,
|
||||||
@@ -34,7 +33,6 @@ use std::{
|
|||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
pin::pin,
|
pin::pin,
|
||||||
str::FromStr as _,
|
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
@@ -217,39 +215,29 @@ impl BufferDiffState {
|
|||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move {
|
self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move {
|
||||||
let mut unstaged_changed_range = None;
|
let mut new_unstaged_diff = None;
|
||||||
if let Some(unstaged_diff) = &unstaged_diff {
|
if let Some(unstaged_diff) = &unstaged_diff {
|
||||||
unstaged_changed_range = BufferDiff::update_diff(
|
new_unstaged_diff = Some(
|
||||||
unstaged_diff.clone(),
|
BufferDiff::update_diff(
|
||||||
buffer.clone(),
|
unstaged_diff.clone(),
|
||||||
index,
|
buffer.clone(),
|
||||||
index_changed,
|
index,
|
||||||
language_changed,
|
index_changed,
|
||||||
language.clone(),
|
language_changed,
|
||||||
language_registry.clone(),
|
language.clone(),
|
||||||
&mut cx,
|
language_registry.clone(),
|
||||||
)
|
&mut cx,
|
||||||
.await?;
|
)
|
||||||
|
.await?,
|
||||||
unstaged_diff.update(&mut cx, |_, cx| {
|
);
|
||||||
if language_changed {
|
|
||||||
cx.emit(BufferDiffEvent::LanguageChanged);
|
|
||||||
}
|
|
||||||
if let Some(changed_range) = unstaged_changed_range.clone() {
|
|
||||||
cx.emit(BufferDiffEvent::DiffChanged {
|
|
||||||
changed_range: Some(changed_range),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut new_uncommitted_diff = None;
|
||||||
if let Some(uncommitted_diff) = &uncommitted_diff {
|
if let Some(uncommitted_diff) = &uncommitted_diff {
|
||||||
let uncommitted_changed_range =
|
new_uncommitted_diff = if index_matches_head {
|
||||||
if let (Some(unstaged_diff), true) = (&unstaged_diff, index_matches_head) {
|
new_unstaged_diff.clone()
|
||||||
uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| {
|
} else {
|
||||||
uncommitted_diff.update_diff_from(&buffer, unstaged_diff, cx)
|
Some(
|
||||||
})?
|
|
||||||
} else {
|
|
||||||
BufferDiff::update_diff(
|
BufferDiff::update_diff(
|
||||||
uncommitted_diff.clone(),
|
uncommitted_diff.clone(),
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
@@ -260,32 +248,32 @@ impl BufferDiffState {
|
|||||||
language_registry.clone(),
|
language_registry.clone(),
|
||||||
&mut cx,
|
&mut cx,
|
||||||
)
|
)
|
||||||
.await?
|
.await?,
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) =
|
||||||
|
unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
|
||||||
|
{
|
||||||
|
unstaged_diff.update(&mut cx, |diff, cx| {
|
||||||
|
diff.set_snapshot(&buffer, new_unstaged_diff, language_changed, None, cx)
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((uncommitted_diff, new_uncommitted_diff)) =
|
||||||
|
uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone())
|
||||||
|
{
|
||||||
uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| {
|
uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| {
|
||||||
if language_changed {
|
uncommitted_diff.set_snapshot(
|
||||||
cx.emit(BufferDiffEvent::LanguageChanged);
|
&buffer,
|
||||||
}
|
new_uncommitted_diff,
|
||||||
let changed_range = match (unstaged_changed_range, uncommitted_changed_range) {
|
language_changed,
|
||||||
(None, None) => None,
|
unstaged_changed_range,
|
||||||
(Some(unstaged_range), None) => {
|
cx,
|
||||||
uncommitted_diff.range_to_hunk_range(unstaged_range, &buffer, cx)
|
);
|
||||||
}
|
|
||||||
(None, Some(uncommitted_range)) => Some(uncommitted_range),
|
|
||||||
(Some(unstaged_range), Some(uncommitted_range)) => {
|
|
||||||
let mut start = uncommitted_range.start;
|
|
||||||
let mut end = uncommitted_range.end;
|
|
||||||
if let Some(unstaged_range) =
|
|
||||||
uncommitted_diff.range_to_hunk_range(unstaged_range, &buffer, cx)
|
|
||||||
{
|
|
||||||
start = unstaged_range.start.min(&uncommitted_range.start, &buffer);
|
|
||||||
end = unstaged_range.end.max(&uncommitted_range.end, &buffer);
|
|
||||||
}
|
|
||||||
Some(start..end)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
cx.emit(BufferDiffEvent::DiffChanged { changed_range });
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -813,8 +801,7 @@ impl LocalBufferStore {
|
|||||||
let Some(buffer) = buffer.upgrade() else {
|
let Some(buffer) = buffer.upgrade() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let buffer = buffer.read(cx);
|
let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
|
||||||
let Some(file) = File::from_dyn(buffer.file()) else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if file.worktree != worktree_handle {
|
if file.worktree != worktree_handle {
|
||||||
@@ -825,7 +812,6 @@ impl LocalBufferStore {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|(work_dir, _)| file.path.starts_with(work_dir))
|
.any(|(work_dir, _)| file.path.starts_with(work_dir))
|
||||||
{
|
{
|
||||||
let snapshot = buffer.text_snapshot();
|
|
||||||
let has_unstaged_diff = diff_state
|
let has_unstaged_diff = diff_state
|
||||||
.unstaged_diff
|
.unstaged_diff
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -835,7 +821,7 @@ impl LocalBufferStore {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|set| set.is_upgradable());
|
.is_some_and(|set| set.is_upgradable());
|
||||||
diff_state_updates.push((
|
diff_state_updates.push((
|
||||||
snapshot.clone(),
|
buffer,
|
||||||
file.path.clone(),
|
file.path.clone(),
|
||||||
has_unstaged_diff.then(|| diff_state.index_text.clone()),
|
has_unstaged_diff.then(|| diff_state.index_text.clone()),
|
||||||
has_uncommitted_diff.then(|| diff_state.head_text.clone()),
|
has_uncommitted_diff.then(|| diff_state.head_text.clone()),
|
||||||
@@ -854,36 +840,33 @@ impl LocalBufferStore {
|
|||||||
.background_spawn(async move {
|
.background_spawn(async move {
|
||||||
diff_state_updates
|
diff_state_updates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(
|
.filter_map(|(buffer, path, current_index_text, current_head_text)| {
|
||||||
|(buffer_snapshot, path, current_index_text, current_head_text)| {
|
let local_repo = snapshot.local_repo_for_path(&path)?;
|
||||||
let local_repo = snapshot.local_repo_for_path(&path)?;
|
let relative_path = local_repo.relativize(&path).ok()?;
|
||||||
let relative_path = local_repo.relativize(&path).ok()?;
|
let index_text = if current_index_text.is_some() {
|
||||||
let index_text = if current_index_text.is_some() {
|
local_repo.repo().load_index_text(&relative_path)
|
||||||
local_repo.repo().load_index_text(&relative_path)
|
} else {
|
||||||
} else {
|
None
|
||||||
None
|
};
|
||||||
};
|
let head_text = if current_head_text.is_some() {
|
||||||
let head_text = if current_head_text.is_some() {
|
local_repo.repo().load_committed_text(&relative_path)
|
||||||
local_repo.repo().load_committed_text(&relative_path)
|
} else {
|
||||||
} else {
|
None
|
||||||
None
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// Avoid triggering a diff update if the base text has not changed.
|
// Avoid triggering a diff update if the base text has not changed.
|
||||||
if let Some((current_index, current_head)) =
|
if let Some((current_index, current_head)) =
|
||||||
current_index_text.as_ref().zip(current_head_text.as_ref())
|
current_index_text.as_ref().zip(current_head_text.as_ref())
|
||||||
|
{
|
||||||
|
if current_index.as_deref() == index_text.as_ref()
|
||||||
|
&& current_head.as_deref() == head_text.as_ref()
|
||||||
{
|
{
|
||||||
if current_index.as_deref() == index_text.as_ref()
|
return None;
|
||||||
&& current_head.as_deref() == head_text.as_ref()
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let diff_bases_change = match (
|
let diff_bases_change =
|
||||||
current_index_text.is_some(),
|
match (current_index_text.is_some(), current_head_text.is_some()) {
|
||||||
current_head_text.is_some(),
|
|
||||||
) {
|
|
||||||
(true, true) => Some(if index_text == head_text {
|
(true, true) => Some(if index_text == head_text {
|
||||||
DiffBasesChange::SetBoth(head_text)
|
DiffBasesChange::SetBoth(head_text)
|
||||||
} else {
|
} else {
|
||||||
@@ -896,17 +879,17 @@ impl LocalBufferStore {
|
|||||||
(false, true) => Some(DiffBasesChange::SetHead(head_text)),
|
(false, true) => Some(DiffBasesChange::SetHead(head_text)),
|
||||||
(false, false) => None,
|
(false, false) => None,
|
||||||
};
|
};
|
||||||
Some((buffer_snapshot, diff_bases_change))
|
|
||||||
},
|
Some((buffer, diff_bases_change))
|
||||||
)
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
for (buffer_snapshot, diff_bases_change) in diff_bases_changes_by_buffer {
|
for (buffer, diff_bases_change) in diff_bases_changes_by_buffer {
|
||||||
let Some(OpenBuffer::Complete { diff_state, .. }) =
|
let Some(OpenBuffer::Complete { diff_state, .. }) =
|
||||||
this.opened_buffers.get_mut(&buffer_snapshot.remote_id())
|
this.opened_buffers.get_mut(&buffer.read(cx).remote_id())
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -917,8 +900,9 @@ impl LocalBufferStore {
|
|||||||
diff_state.update(cx, |diff_state, cx| {
|
diff_state.update(cx, |diff_state, cx| {
|
||||||
use proto::update_diff_bases::Mode;
|
use proto::update_diff_bases::Mode;
|
||||||
|
|
||||||
|
let buffer = buffer.read(cx);
|
||||||
if let Some((client, project_id)) = this.downstream_client.as_ref() {
|
if let Some((client, project_id)) = this.downstream_client.as_ref() {
|
||||||
let buffer_id = buffer_snapshot.remote_id().to_proto();
|
let buffer_id = buffer.remote_id().to_proto();
|
||||||
let (staged_text, committed_text, mode) = match diff_bases_change
|
let (staged_text, committed_text, mode) = match diff_bases_change
|
||||||
.clone()
|
.clone()
|
||||||
{
|
{
|
||||||
@@ -942,8 +926,11 @@ impl LocalBufferStore {
|
|||||||
client.send(message).log_err();
|
client.send(message).log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ =
|
let _ = diff_state.diff_bases_changed(
|
||||||
diff_state.diff_bases_changed(buffer_snapshot, diff_bases_change, cx);
|
buffer.text_snapshot(),
|
||||||
|
diff_bases_change,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1705,11 +1692,17 @@ impl BufferStore {
|
|||||||
Err(e) => return Task::ready(Err(e)),
|
Err(e) => return Task::ready(Err(e)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let remote = repo_entry
|
||||||
|
.branch()
|
||||||
|
.and_then(|b| b.upstream.as_ref())
|
||||||
|
.and_then(|b| b.remote_name())
|
||||||
|
.unwrap_or("origin")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
cx.spawn(|cx| async move {
|
cx.spawn(|cx| async move {
|
||||||
const REMOTE_NAME: &str = "origin";
|
|
||||||
let origin_url = repo
|
let origin_url = repo
|
||||||
.remote_url(REMOTE_NAME)
|
.remote_url(&remote)
|
||||||
.ok_or_else(|| anyhow!("remote \"{REMOTE_NAME}\" not found"))?;
|
.ok_or_else(|| anyhow!("remote \"{remote}\" not found"))?;
|
||||||
|
|
||||||
let sha = repo
|
let sha = repo
|
||||||
.head_sha()
|
.head_sha()
|
||||||
@@ -2787,20 +2780,10 @@ fn serialize_blame_buffer_response(blame: Option<git::blame::Blame>) -> proto::B
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let permalinks = blame
|
|
||||||
.permalinks
|
|
||||||
.into_iter()
|
|
||||||
.map(|(oid, url)| proto::CommitPermalink {
|
|
||||||
oid: oid.as_bytes().into(),
|
|
||||||
permalink: url.to_string(),
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
proto::BlameBufferResponse {
|
proto::BlameBufferResponse {
|
||||||
blame_response: Some(proto::blame_buffer_response::BlameResponse {
|
blame_response: Some(proto::blame_buffer_response::BlameResponse {
|
||||||
entries,
|
entries,
|
||||||
messages,
|
messages,
|
||||||
permalinks,
|
|
||||||
remote_url: blame.remote_url,
|
remote_url: blame.remote_url,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -2839,20 +2822,8 @@ fn deserialize_blame_buffer_response(
|
|||||||
.filter_map(|message| Some((git::Oid::from_bytes(&message.oid).ok()?, message.message)))
|
.filter_map(|message| Some((git::Oid::from_bytes(&message.oid).ok()?, message.message)))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
let permalinks = response
|
|
||||||
.permalinks
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|permalink| {
|
|
||||||
Some((
|
|
||||||
git::Oid::from_bytes(&permalink.oid).ok()?,
|
|
||||||
Url::from_str(&permalink.permalink).ok()?,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
|
|
||||||
Some(Blame {
|
Some(Blame {
|
||||||
entries,
|
entries,
|
||||||
permalinks,
|
|
||||||
messages,
|
messages,
|
||||||
remote_url: response.remote_url,
|
remote_url: response.remote_url,
|
||||||
})
|
})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user