Compare commits
53 Commits
add-git-pa
...
multi_sele
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8f3e63b82 | ||
|
|
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 | ||
|
|
ed52e759d7 | ||
|
|
6da099a9d7 | ||
|
|
5f159bc95e | ||
|
|
a4462577bf | ||
|
|
c147b58558 | ||
|
|
84fe1bfe9b | ||
|
|
657d7a911d |
188
Cargo.lock
generated
188
Cargo.lock
generated
@@ -450,7 +450,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assistant_context_editor",
|
||||
"assistant_scripting",
|
||||
"assistant_settings",
|
||||
"assistant_slash_command",
|
||||
"assistant_tool",
|
||||
@@ -491,6 +490,7 @@ dependencies = [
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"rope",
|
||||
"scripting_tool",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -564,26 +564,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "assistant_settings"
|
||||
version = "0.1.0"
|
||||
@@ -2321,7 +2301,7 @@ dependencies = [
|
||||
"cap-primitives",
|
||||
"cap-std",
|
||||
"io-lifetimes",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2349,7 +2329,7 @@ dependencies = [
|
||||
"ipnet",
|
||||
"maybe-owned",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
"winx",
|
||||
]
|
||||
|
||||
@@ -2690,7 +2670,7 @@ dependencies = [
|
||||
"serde",
|
||||
"tempfile",
|
||||
"util",
|
||||
"windows 0.58.0",
|
||||
"windows 0.60.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2732,7 +2712,7 @@ dependencies = [
|
||||
"tokio-socks",
|
||||
"url",
|
||||
"util",
|
||||
"windows 0.58.0",
|
||||
"windows 0.60.0",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
@@ -4423,7 +4403,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4478,7 +4458,6 @@ dependencies = [
|
||||
"env_logger 0.11.6",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"git",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language",
|
||||
@@ -5074,7 +5053,7 @@ dependencies = [
|
||||
"text",
|
||||
"time",
|
||||
"util",
|
||||
"windows 0.58.0",
|
||||
"windows 0.60.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5085,7 +5064,7 @@ checksum = "5e2e6123af26f0f2c51cc66869137080199406754903cc926a7690401ce09cb4"
|
||||
dependencies = [
|
||||
"io-lifetimes",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5471,7 +5450,6 @@ dependencies = [
|
||||
"db",
|
||||
"editor",
|
||||
"env_logger 0.11.6",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"git",
|
||||
@@ -5499,10 +5477,11 @@ dependencies = [
|
||||
"telemetry",
|
||||
"theme",
|
||||
"time",
|
||||
"time_format",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"windows 0.58.0",
|
||||
"windows 0.60.0",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
]
|
||||
@@ -5702,8 +5681,8 @@ dependencies = [
|
||||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-plasma",
|
||||
"windows 0.58.0",
|
||||
"windows-core 0.58.0",
|
||||
"windows 0.60.0",
|
||||
"windows-core 0.60.1",
|
||||
"x11-clipboard",
|
||||
"x11rb",
|
||||
"xim",
|
||||
@@ -6730,7 +6709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65"
|
||||
dependencies = [
|
||||
"io-lifetimes",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10755,7 +10734,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11677,7 +11656,7 @@ dependencies = [
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"once_cell",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11931,6 +11910,28 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "scrypt"
|
||||
version = "0.11.0"
|
||||
@@ -13427,7 +13428,7 @@ dependencies = [
|
||||
"fd-lock",
|
||||
"io-lifetimes",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
"winx",
|
||||
]
|
||||
|
||||
@@ -13567,7 +13568,7 @@ dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13614,7 +13615,7 @@ dependencies = [
|
||||
"theme",
|
||||
"thiserror 1.0.69",
|
||||
"util",
|
||||
"windows 0.58.0",
|
||||
"windows 0.60.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13991,7 +13992,7 @@ dependencies = [
|
||||
"tree-sitter-md",
|
||||
"ui",
|
||||
"util",
|
||||
"windows 0.58.0",
|
||||
"windows 0.60.0",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
"zeta",
|
||||
@@ -14698,7 +14699,7 @@ dependencies = [
|
||||
"theme",
|
||||
"ui_macros",
|
||||
"util",
|
||||
"windows 0.58.0",
|
||||
"windows 0.60.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15913,7 +15914,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15970,6 +15971,28 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
@@ -16010,10 +16033,33 @@ dependencies = [
|
||||
"windows-implement 0.58.0",
|
||||
"windows-interface 0.58.0",
|
||||
"windows-result 0.2.0",
|
||||
"windows-strings",
|
||||
"windows-strings 0.1.0",
|
||||
"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]]
|
||||
name = "windows-implement"
|
||||
version = "0.57.0"
|
||||
@@ -16036,6 +16082,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-interface"
|
||||
version = "0.57.0"
|
||||
@@ -16058,12 +16115,33 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-link"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "windows-registry"
|
||||
version = "0.2.0"
|
||||
@@ -16071,7 +16149,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
|
||||
dependencies = [
|
||||
"windows-result 0.2.0",
|
||||
"windows-strings",
|
||||
"windows-strings 0.1.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
@@ -16093,6 +16171,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
@@ -16103,6 +16190,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
@@ -16378,7 +16474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -17019,7 +17115,7 @@ dependencies = [
|
||||
"vim",
|
||||
"vim_mode_setting",
|
||||
"welcome",
|
||||
"windows 0.58.0",
|
||||
"windows 0.60.0",
|
||||
"winresource",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
|
||||
16
Cargo.toml
16
Cargo.toml
@@ -118,7 +118,7 @@ members = [
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/schema_generator",
|
||||
"crates/assistant_scripting",
|
||||
"crates/scripting_tool",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
"crates/semantic_version",
|
||||
@@ -318,7 +318,7 @@ reqwest_client = { path = "crates/reqwest_client" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
assistant_scripting = { path = "crates/assistant_scripting" }
|
||||
scripting_tool = { path = "crates/scripting_tool" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_index = { path = "crates/semantic_index" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
@@ -597,12 +597,12 @@ features = [
|
||||
]
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.58"
|
||||
version = "0.60"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Collections",
|
||||
"Foundation_Numerics",
|
||||
"Storage",
|
||||
"Storage_Search",
|
||||
"Storage_Streams",
|
||||
"System_Threading",
|
||||
"UI_StartScreen",
|
||||
"UI_ViewManagement",
|
||||
@@ -623,9 +623,11 @@ features = [
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_Console",
|
||||
"Win32_System_DataExchange",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Ole",
|
||||
"Win32_System_Pipes",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_Threading",
|
||||
@@ -751,5 +753,9 @@ new_ret_no_self = { level = "allow" }
|
||||
should_implement_trait = { level = "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]
|
||||
ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme"]
|
||||
|
||||
7
assets/icons/file_icons/wgsl.svg
Normal file
7
assets/icons/file_icons/wgsl.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.5 13L1.5 5H11.5L6.5 13Z" fill="black"/>
|
||||
<path d="M14 9H9L11.5 5L14 9Z" fill="black" fill-opacity="0.75"/>
|
||||
<path d="M9 9L14 9L11.5 13L9 9Z" fill="black" fill-opacity="0.65"/>
|
||||
<path d="M14 5L15.25 7L12.75 7L14 5Z" fill="black" fill-opacity="0.5"/>
|
||||
<path d="M14 9L12.75 7H15.25L14 9Z" fill="black" fill-opacity="0.55"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 432 B |
@@ -731,28 +731,48 @@
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-y": "git::StageFile",
|
||||
"alt-shift-y": "git::UnstageFile",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"space": "git::ToggleStaged",
|
||||
"ctrl-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll",
|
||||
"tab": "git_panel::FocusEditor",
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"alt-enter": "menu::SecondaryConfirm"
|
||||
"alt-enter": "menu::SecondaryConfirm",
|
||||
"backspace": "git::RestoreFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"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",
|
||||
"bindings": {
|
||||
"ctrl-enter": "git::Commit"
|
||||
"ctrl-enter": "git::Commit",
|
||||
"ctrl-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -767,6 +787,7 @@
|
||||
"escape": "git_panel::FocusChanges",
|
||||
"tab": "git_panel::FocusChanges",
|
||||
"shift-tab": "git_panel::FocusChanges",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"alt-up": "git_panel::FocusChanges",
|
||||
"alt-l": "git::GenerateCommitMessage"
|
||||
|
||||
@@ -31,13 +31,13 @@
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"cmd-escape": "menu::Cancel",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-escape": "menu::Cancel",
|
||||
"cmd-o": "workspace::Open",
|
||||
"cmd-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
"cmd-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
@@ -763,28 +763,25 @@
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"cmd-alt-y": "git::ToggleStaged",
|
||||
"space": "git::ToggleStaged",
|
||||
"cmd-shift-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll",
|
||||
"cmd-y": "git::StageFile",
|
||||
"cmd-shift-y": "git::UnstageFile",
|
||||
"alt-down": "git_panel::FocusEditor",
|
||||
"tab": "git_panel::FocusEditor",
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"cmd-enter": "git::Commit"
|
||||
"cmd-enter": "git::Commit",
|
||||
"backspace": "git::RestoreFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitDiff > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "git::Commit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AskPass > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm"
|
||||
"cmd-enter": "git::Commit",
|
||||
"cmd-ctrl-y": "git::StageAll",
|
||||
"cmd-ctrl-shift-y": "git::UnstageAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -800,11 +797,27 @@
|
||||
"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",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"escape": "menu::Cancel",
|
||||
"cmd-enter": "git::Commit",
|
||||
"alt-tab": "git::GenerateCommitMessage"
|
||||
}
|
||||
|
||||
@@ -383,6 +383,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variable.special": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variant": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
@@ -771,6 +776,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variable.special": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variant": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
@@ -1159,6 +1169,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variable.special": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variant": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
@@ -1547,6 +1562,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variable.special": {
|
||||
"color": "#066578ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variant": {
|
||||
"color": "#0b6678ff",
|
||||
"font_style": null,
|
||||
@@ -1935,6 +1955,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variable.special": {
|
||||
"color": "#066578ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variant": {
|
||||
"color": "#0b6678ff",
|
||||
"font_style": null,
|
||||
@@ -2323,6 +2348,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variable.special": {
|
||||
"color": "#066578ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variant": {
|
||||
"color": "#0b6678ff",
|
||||
"font_style": null,
|
||||
|
||||
@@ -386,7 +386,6 @@ impl InlineAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn suggest_assist(
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
@@ -1674,7 +1673,6 @@ impl Focusable for PromptEditor {
|
||||
impl PromptEditor {
|
||||
const MAX_LINES: u8 = 8;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
id: InlineAssistId,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
@@ -2333,7 +2331,6 @@ struct InlineAssist {
|
||||
}
|
||||
|
||||
impl InlineAssist {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
assist_id: InlineAssistId,
|
||||
group_id: InlineAssistGroupId,
|
||||
|
||||
@@ -702,7 +702,6 @@ impl Focusable for PromptEditor {
|
||||
impl PromptEditor {
|
||||
const MAX_LINES: u8 = 8;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
id: TerminalInlineAssistId,
|
||||
prompt_history: VecDeque<String>,
|
||||
|
||||
@@ -59,11 +59,11 @@ prompt_library.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
rope.workspace = true
|
||||
scripting_tool.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
assistant_scripting.workspace = true
|
||||
streaming_diff.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal.workspace = true
|
||||
@@ -82,8 +82,8 @@ zed_actions.workspace = true
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, "features" = ["test-support"] }
|
||||
language_model = { workspace = true, "features" = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_scripting::{ScriptId, ScriptState};
|
||||
use collections::{HashMap, HashSet};
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
||||
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
|
||||
Task, TextStyleRefinement, UnderlineStyle, WeakEntity,
|
||||
Task, TextStyleRefinement, UnderlineStyle,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use scripting_tool::{ScriptingTool, ScriptingToolInput};
|
||||
use settings::Settings as _;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, Disclosure, KeyBinding};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
|
||||
use crate::thread_store::ThreadStore;
|
||||
@@ -23,7 +22,6 @@ use crate::tool_use::{ToolUse, ToolUseStatus};
|
||||
use crate::ui::ContextPill;
|
||||
|
||||
pub struct ActiveThread {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
thread: Entity<Thread>,
|
||||
@@ -31,9 +29,9 @@ pub struct ActiveThread {
|
||||
messages: Vec<MessageId>,
|
||||
list_state: ListState,
|
||||
rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
|
||||
rendered_scripting_tool_uses: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
|
||||
editing_message: Option<(MessageId, EditMessageState)>,
|
||||
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
|
||||
expanded_scripts: HashSet<ScriptId>,
|
||||
last_error: Option<ThreadError>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
@@ -44,7 +42,6 @@ struct EditMessageState {
|
||||
|
||||
impl ActiveThread {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
thread: Entity<Thread>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
@@ -57,15 +54,14 @@ impl ActiveThread {
|
||||
];
|
||||
|
||||
let mut this = Self {
|
||||
workspace,
|
||||
language_registry,
|
||||
thread_store,
|
||||
thread: thread.clone(),
|
||||
save_thread_task: None,
|
||||
messages: Vec::new(),
|
||||
rendered_messages_by_id: HashMap::default(),
|
||||
rendered_scripting_tool_uses: HashMap::default(),
|
||||
expanded_tool_uses: HashMap::default(),
|
||||
expanded_scripts: HashSet::default(),
|
||||
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
@@ -80,6 +76,16 @@ impl ActiveThread {
|
||||
|
||||
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
|
||||
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
|
||||
@@ -246,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(
|
||||
&mut self,
|
||||
_thread: &Entity<Thread>,
|
||||
@@ -303,7 +335,19 @@ impl ActiveThread {
|
||||
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() {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(model) = model_registry.active_model() {
|
||||
@@ -313,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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,11 +500,12 @@ impl ActiveThread {
|
||||
|
||||
let context = thread.context_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.
|
||||
if message.role == Role::User
|
||||
&& (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();
|
||||
}
|
||||
@@ -618,18 +655,23 @@ impl ActiveThread {
|
||||
Role::Assistant => div()
|
||||
.id(("message-container", ix))
|
||||
.child(message_content)
|
||||
.children(self.render_script(message_id, cx))
|
||||
.map(|parent| {
|
||||
if tool_uses.is_empty() {
|
||||
if tool_uses.is_empty() && scripting_tool_uses.is_empty() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent.child(
|
||||
v_flex().children(
|
||||
tool_uses
|
||||
.into_iter()
|
||||
.map(|tool_use| self.render_tool_use(tool_use, cx)),
|
||||
),
|
||||
v_flex()
|
||||
.children(
|
||||
tool_uses
|
||||
.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(
|
||||
@@ -663,8 +705,13 @@ impl ActiveThread {
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
|
||||
.when(is_open, |element| element.border_b_1().rounded_t(px(6.)))
|
||||
.when(!is_open, |element| element.rounded_md())
|
||||
.map(|element| {
|
||||
if is_open {
|
||||
element.border_b_1().rounded_t(px(6.))
|
||||
} else {
|
||||
element.rounded_md()
|
||||
}
|
||||
})
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -739,137 +786,117 @@ impl ActiveThread {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_script(&self, message_id: MessageId, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let script = self.thread.read(cx).script_for_message(message_id, cx)?;
|
||||
fn render_scripting_tool_use(
|
||||
&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);
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
let element = div().px_2p5().child(
|
||||
div().px_2p5().child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.py_0p5()
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.bg(colors.editor_foreground.opacity(0.02))
|
||||
.when(is_open, |element| element.border_b_1().rounded_t(px(6.)))
|
||||
.when(!is_open, |element| element.rounded_md())
|
||||
.border_color(colors.border)
|
||||
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
|
||||
.map(|element| {
|
||||
if is_open {
|
||||
element.border_b_1().rounded_t(px(6.))
|
||||
} else {
|
||||
element.rounded_md()
|
||||
}
|
||||
})
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Disclosure::new("script-disclosure", is_open).on_click(
|
||||
.child(Disclosure::new("tool-use-disclosure", is_open).on_click(
|
||||
cx.listener({
|
||||
let script_id = script.id;
|
||||
let tool_use_id = tool_use.id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
if this.expanded_scripts.contains(&script_id) {
|
||||
this.expanded_scripts.remove(&script_id);
|
||||
} else {
|
||||
this.expanded_scripts.insert(script_id);
|
||||
}
|
||||
let is_open = this
|
||||
.expanded_tool_uses
|
||||
.entry(tool_use_id.clone())
|
||||
.or_insert(false);
|
||||
|
||||
*is_open = !*is_open;
|
||||
}
|
||||
}),
|
||||
))
|
||||
// TODO: Generate script description
|
||||
.child(Label::new("Script")),
|
||||
.child(Label::new(tool_use.name)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(match script.state {
|
||||
ScriptState::Generating => "Generating",
|
||||
ScriptState::Running { .. } => "Running",
|
||||
ScriptState::Succeeded { .. } => "Finished",
|
||||
ScriptState::Failed { .. } => "Error",
|
||||
})
|
||||
.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);
|
||||
}
|
||||
})),
|
||||
),
|
||||
Label::new(match tool_use.status {
|
||||
ToolUseStatus::Pending => "Pending",
|
||||
ToolUseStatus::Running => "Running",
|
||||
ToolUseStatus::Finished(_) => "Finished",
|
||||
ToolUseStatus::Error(_) => "Error",
|
||||
})
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
)
|
||||
.when(is_open, |parent| {
|
||||
let stdout = script.stdout_snapshot();
|
||||
let error = script.error();
|
||||
.map(|parent| {
|
||||
if !is_open {
|
||||
return parent;
|
||||
}
|
||||
|
||||
let lua_script_markdown =
|
||||
self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
|
||||
|
||||
parent.child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
.bg(colors.editor_background)
|
||||
.gap_2()
|
||||
.child(if stdout.is_empty() && error.is_none() {
|
||||
Label::new("No output yet")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
} else {
|
||||
Label::new(stdout).size(LabelSize::Small).buffer_font(cx)
|
||||
})
|
||||
.children(script.error().map(|err| {
|
||||
Label::new(err.to_string())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Error)
|
||||
})),
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.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",
|
||||
))
|
||||
}
|
||||
}),
|
||||
)
|
||||
.map(|parent| match tool_use.status {
|
||||
ToolUseStatus::Finished(output) => parent.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.child(Label::new("Result:"))
|
||||
.child(Label::new(output)),
|
||||
),
|
||||
ToolUseStatus::Error(err) => parent.child(
|
||||
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);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,7 +168,6 @@ impl AssistantPanel {
|
||||
|
||||
let thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
workspace.clone(),
|
||||
thread.clone(),
|
||||
thread_store.clone(),
|
||||
language_registry.clone(),
|
||||
@@ -242,7 +241,6 @@ impl AssistantPanel {
|
||||
self.active_view = ActiveView::Thread;
|
||||
self.thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
self.workspace.clone(),
|
||||
thread.clone(),
|
||||
self.thread_store.clone(),
|
||||
self.language_registry.clone(),
|
||||
@@ -376,7 +374,6 @@ impl AssistantPanel {
|
||||
this.active_view = ActiveView::Thread;
|
||||
this.thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
this.workspace.clone(),
|
||||
thread.clone(),
|
||||
this.thread_store.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 {
|
||||
"Enter the URL that you would like to fetch".into()
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
Some("Enter the URL that you would like to fetch".into())
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::{
|
||||
|
||||
pub struct ContextStrip {
|
||||
context_store: Entity<ContextStore>,
|
||||
pub context_picker: Entity<ContextPicker>,
|
||||
context_picker: Entity<ContextPicker>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
focus_handle: FocusHandle,
|
||||
suggest_context_kind: SuggestContextKind,
|
||||
@@ -36,7 +36,6 @@ pub struct ContextStrip {
|
||||
}
|
||||
|
||||
impl ContextStrip {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
context_store: Entity<ContextStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
|
||||
@@ -480,7 +480,6 @@ impl InlineAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn suggest_assist(
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
@@ -1451,7 +1450,6 @@ struct InlineAssistScrollLock {
|
||||
}
|
||||
|
||||
impl EditorInlineAssists {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) -> Self {
|
||||
let (highlight_updates_tx, mut highlight_updates_rx) = async_watch::channel(());
|
||||
Self {
|
||||
@@ -1563,7 +1561,6 @@ pub struct InlineAssist {
|
||||
}
|
||||
|
||||
impl InlineAssist {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
assist_id: InlineAssistId,
|
||||
group_id: InlineAssistGroupId,
|
||||
|
||||
@@ -816,7 +816,6 @@ impl InlineAssistId {
|
||||
}
|
||||
|
||||
impl PromptEditor<BufferCodegen> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_buffer(
|
||||
id: InlineAssistId,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
@@ -976,7 +975,6 @@ impl TerminalInlineAssistId {
|
||||
}
|
||||
|
||||
impl PromptEditor<TerminalCodegen> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_terminal(
|
||||
id: TerminalInlineAssistId,
|
||||
prompt_history: VecDeque<String>,
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use editor::actions::MoveUp;
|
||||
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
|
||||
@@ -15,8 +16,8 @@ use std::time::Duration;
|
||||
use text::Bias;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Switch,
|
||||
Tooltip,
|
||||
prelude::*, ButtonLike, Disclosure, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle,
|
||||
Switch, Tooltip,
|
||||
};
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
use workspace::Workspace;
|
||||
@@ -39,6 +40,7 @@ pub struct MessageEditor {
|
||||
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Entity<AssistantModelSelector>,
|
||||
use_tools: bool,
|
||||
edits_expanded: bool,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -117,6 +119,7 @@ impl MessageEditor {
|
||||
)
|
||||
}),
|
||||
use_tools: false,
|
||||
edits_expanded: false,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -303,6 +306,9 @@ impl Render for MessageEditor {
|
||||
px(64.)
|
||||
};
|
||||
|
||||
let changed_buffers = self.thread.read(cx).scripting_changed_buffers(cx);
|
||||
let changed_buffers_count = changed_buffers.len();
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.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(
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_scripting::{
|
||||
Script, ScriptEvent, ScriptId, ScriptSession, ScriptTagParser, SCRIPTING_PROMPT,
|
||||
};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
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::{
|
||||
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
@@ -16,6 +13,7 @@ use language_model::{
|
||||
Role, StopReason,
|
||||
};
|
||||
use project::Project;
|
||||
use scripting_tool::{ScriptingSession, ScriptingTool};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::{post_inc, TryFutureExt as _};
|
||||
use uuid::Uuid;
|
||||
@@ -78,10 +76,8 @@ pub struct Thread {
|
||||
project: Entity<Project>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
tool_use: ToolUseState,
|
||||
scripts_by_assistant_message: HashMap<MessageId, ScriptId>,
|
||||
script_output_messages: HashSet<MessageId>,
|
||||
script_session: Entity<ScriptSession>,
|
||||
_script_session_subscription: Subscription,
|
||||
scripting_session: Entity<ScriptingSession>,
|
||||
scripting_tool_use: ToolUseState,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
@@ -90,8 +86,7 @@ impl Thread {
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let script_session = cx.new(|cx| ScriptSession::new(project.clone(), cx));
|
||||
let script_session_subscription = cx.subscribe(&script_session, Self::handle_script_event);
|
||||
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
|
||||
|
||||
Self {
|
||||
id: ThreadId::new(),
|
||||
@@ -107,10 +102,8 @@ impl Thread {
|
||||
project,
|
||||
tools,
|
||||
tool_use: ToolUseState::new(),
|
||||
scripts_by_assistant_message: HashMap::default(),
|
||||
script_output_messages: HashSet::default(),
|
||||
script_session,
|
||||
_script_session_subscription: script_session_subscription,
|
||||
scripting_session,
|
||||
scripting_tool_use: ToolUseState::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,9 +121,11 @@ impl Thread {
|
||||
.map(|message| message.id.0 + 1)
|
||||
.unwrap_or(0),
|
||||
);
|
||||
let tool_use = ToolUseState::from_saved_messages(&saved.messages);
|
||||
let script_session = cx.new(|cx| ScriptSession::new(project.clone(), cx));
|
||||
let script_session_subscription = cx.subscribe(&script_session, Self::handle_script_event);
|
||||
let tool_use =
|
||||
ToolUseState::from_saved_messages(&saved.messages, |name| name != ScriptingTool::NAME);
|
||||
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 {
|
||||
id,
|
||||
@@ -154,10 +149,8 @@ impl Thread {
|
||||
project,
|
||||
tools,
|
||||
tool_use,
|
||||
scripts_by_assistant_message: HashMap::default(),
|
||||
script_output_messages: HashSet::default(),
|
||||
script_session,
|
||||
_script_session_subscription: script_session_subscription,
|
||||
scripting_session,
|
||||
scripting_tool_use,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
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
|
||||
// of the pending tools.
|
||||
self.pending_tool_uses()
|
||||
.into_iter()
|
||||
.all(|tool_use| tool_use.status.is_error())
|
||||
all_pending_tool_uses.all(|tool_use| tool_use.status.is_error())
|
||||
}
|
||||
|
||||
pub fn tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
|
||||
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> {
|
||||
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 {
|
||||
self.tool_use.message_has_tool_results(message_id)
|
||||
}
|
||||
|
||||
pub fn message_has_script_output(&self, message_id: MessageId) -> bool {
|
||||
self.script_output_messages.contains(&message_id)
|
||||
pub fn message_has_scripting_tool_results(&self, message_id: MessageId) -> bool {
|
||||
self.scripting_tool_use.message_has_tool_results(message_id)
|
||||
}
|
||||
|
||||
pub fn insert_user_message(
|
||||
@@ -327,39 +338,6 @@ impl Thread {
|
||||
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(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
@@ -370,16 +348,25 @@ impl Thread {
|
||||
let mut request = self.to_completion_request(request_kind, cx);
|
||||
|
||||
if use_tools {
|
||||
request.tools = self
|
||||
.tools()
|
||||
.tools(cx)
|
||||
.into_iter()
|
||||
.map(|tool| LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema: tool.input_schema(),
|
||||
})
|
||||
.collect();
|
||||
let mut tools = Vec::new();
|
||||
tools.push(LanguageModelRequestTool {
|
||||
name: ScriptingTool::NAME.into(),
|
||||
description: ScriptingTool::DESCRIPTION.into(),
|
||||
input_schema: ScriptingTool::input_schema(),
|
||||
});
|
||||
|
||||
tools.extend(
|
||||
self.tools()
|
||||
.tools(cx)
|
||||
.into_iter()
|
||||
.map(|tool| LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema: tool.input_schema(),
|
||||
}),
|
||||
);
|
||||
|
||||
request.tools = tools;
|
||||
}
|
||||
|
||||
self.stream_completion(request, model, cx);
|
||||
@@ -388,7 +375,7 @@ impl Thread {
|
||||
pub fn to_completion_request(
|
||||
&self,
|
||||
request_kind: RequestKind,
|
||||
cx: &App,
|
||||
_cx: &App,
|
||||
) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
messages: vec![],
|
||||
@@ -397,12 +384,6 @@ impl Thread {
|
||||
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();
|
||||
|
||||
for message in &self.messages {
|
||||
@@ -420,6 +401,8 @@ impl Thread {
|
||||
RequestKind::Chat => {
|
||||
self.tool_use
|
||||
.attach_tool_results(message.id, &mut request_message);
|
||||
self.scripting_tool_use
|
||||
.attach_tool_results(message.id, &mut request_message);
|
||||
}
|
||||
RequestKind::Summarize => {
|
||||
// We don't care about tool use during summarization.
|
||||
@@ -436,15 +419,8 @@ impl Thread {
|
||||
RequestKind::Chat => {
|
||||
self.tool_use
|
||||
.attach_tool_uses(message.id, &mut request_message);
|
||||
|
||||
if matches!(message.role, Role::Assistant) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
self.scripting_tool_use
|
||||
.attach_tool_uses(message.id, &mut request_message);
|
||||
}
|
||||
RequestKind::Summarize => {
|
||||
// We don't care about tool use during summarization.
|
||||
@@ -486,8 +462,6 @@ impl Thread {
|
||||
let stream_completion = async {
|
||||
let mut events = stream.await?;
|
||||
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 {
|
||||
let event = event?;
|
||||
@@ -502,44 +476,20 @@ impl Thread {
|
||||
}
|
||||
LanguageModelCompletionEvent::Text(chunk) => {
|
||||
if let Some(last_message) = thread.messages.last_mut() {
|
||||
let chunk = script_tag_parser.parse_chunk(&chunk);
|
||||
|
||||
let message_id = if last_message.role == Role::Assistant {
|
||||
last_message.text.push_str(&chunk.content);
|
||||
if last_message.role == Role::Assistant {
|
||||
last_message.text.push_str(&chunk);
|
||||
cx.emit(ThreadEvent::StreamedAssistantText(
|
||||
last_message.id,
|
||||
chunk.content,
|
||||
chunk,
|
||||
));
|
||||
last_message.id
|
||||
} else {
|
||||
// If we won't have an Assistant message yet, assume this chunk marks the beginning
|
||||
// of a new Assistant response.
|
||||
//
|
||||
// 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.
|
||||
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) => {
|
||||
@@ -548,9 +498,15 @@ impl Thread {
|
||||
.iter()
|
||||
.rfind(|message| message.role == Role::Assistant)
|
||||
{
|
||||
thread
|
||||
.tool_use
|
||||
.request_tool_use(last_assistant_message.id, tool_use);
|
||||
if tool_use.name.as_ref() == ScriptingTool::NAME {
|
||||
thread
|
||||
.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 +626,7 @@ impl Thread {
|
||||
|
||||
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) {
|
||||
let pending_tool_uses = self
|
||||
.tool_use
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.filter(|tool_use| tool_use.status.is_idle())
|
||||
@@ -683,6 +640,45 @@ impl Thread {
|
||||
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(
|
||||
@@ -697,11 +693,14 @@ impl Thread {
|
||||
let output = output.await;
|
||||
thread
|
||||
.update(&mut cx, |thread, cx| {
|
||||
thread
|
||||
let pending_tool_use = thread
|
||||
.tool_use
|
||||
.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();
|
||||
}
|
||||
@@ -711,6 +710,35 @@ impl Thread {
|
||||
.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(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
@@ -760,8 +788,9 @@ pub enum ThreadEvent {
|
||||
ToolFinished {
|
||||
#[allow(unused)]
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
/// The pending tool use that corresponds to this tool.
|
||||
pending_tool_use: Option<PendingToolUse>,
|
||||
},
|
||||
ScriptFinished,
|
||||
}
|
||||
|
||||
impl EventEmitter<ThreadEvent> for Thread {}
|
||||
|
||||
@@ -116,28 +116,35 @@ impl ThreadStore {
|
||||
updated_at: thread.updated_at(),
|
||||
messages: thread
|
||||
.messages()
|
||||
.map(|message| SavedMessage {
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
text: message.text.clone(),
|
||||
tool_uses: thread
|
||||
.map(|message| {
|
||||
let all_tool_uses = thread
|
||||
.tool_uses_for_message(message.id)
|
||||
.into_iter()
|
||||
.chain(thread.scripting_tool_uses_for_message(message.id))
|
||||
.map(|tool_use| SavedToolUse {
|
||||
id: tool_use.id,
|
||||
name: tool_use.name,
|
||||
input: tool_use.input,
|
||||
})
|
||||
.collect(),
|
||||
tool_results: thread
|
||||
.collect();
|
||||
let all_tool_results = thread
|
||||
.tool_results_for_message(message.id)
|
||||
.into_iter()
|
||||
.chain(thread.scripting_tool_results_for_message(message.id))
|
||||
.map(|tool_result| SavedToolResult {
|
||||
tool_use_id: tool_result.tool_use_id.clone(),
|
||||
is_error: tool_result.is_error,
|
||||
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(),
|
||||
};
|
||||
|
||||
@@ -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 tool_names_by_id = HashMap::default();
|
||||
|
||||
for message in messages {
|
||||
match message.role {
|
||||
Role::Assistant => {
|
||||
if !message.tool_uses.is_empty() {
|
||||
this.tool_uses_by_assistant_message.insert(
|
||||
message.id,
|
||||
message
|
||||
.tool_uses
|
||||
let tool_uses = message
|
||||
.tool_uses
|
||||
.iter()
|
||||
.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()
|
||||
.map(|tool_use| LanguageModelToolUse {
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone().into(),
|
||||
input: tool_use.input.clone(),
|
||||
})
|
||||
.collect(),
|
||||
.map(|tool_use| (tool_use.id.clone(), tool_use.name.clone())),
|
||||
);
|
||||
|
||||
this.tool_uses_by_assistant_message
|
||||
.insert(message.id, tool_uses);
|
||||
}
|
||||
}
|
||||
Role::User => {
|
||||
@@ -76,6 +90,14 @@ impl ToolUseState {
|
||||
|
||||
for tool_result in &message.tool_results {
|
||||
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());
|
||||
this.tool_results.insert(
|
||||
@@ -202,7 +224,7 @@ impl ToolUseState {
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
output: Result<String>,
|
||||
) {
|
||||
) -> Option<PendingToolUse> {
|
||||
match output {
|
||||
Ok(output) => {
|
||||
self.tool_results.insert(
|
||||
@@ -213,7 +235,7 @@ impl ToolUseState {
|
||||
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) => {
|
||||
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) {
|
||||
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 id: LanguageModelToolUseId,
|
||||
/// The ID of the Assistant message in which the tool use was requested.
|
||||
#[allow(unused)]
|
||||
pub assistant_message_id: MessageId,
|
||||
pub name: Arc<str>,
|
||||
pub input: serde_json::Value,
|
||||
|
||||
@@ -647,7 +647,6 @@ impl AssistantContext {
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
id: ContextId,
|
||||
replica_id: ReplicaId,
|
||||
@@ -768,7 +767,6 @@ impl AssistantContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn deserialize(
|
||||
saved_context: SavedContext,
|
||||
path: PathBuf,
|
||||
|
||||
@@ -535,7 +535,6 @@ impl ContextEditor {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn run_command(
|
||||
&mut self,
|
||||
command_range: Range<language::Anchor>,
|
||||
@@ -2057,7 +2056,6 @@ impl ContextEditor {
|
||||
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_patch_block(
|
||||
&mut self,
|
||||
range: Range<text::Anchor>,
|
||||
|
||||
@@ -134,7 +134,6 @@ impl SlashCommandCompletionProvider {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn complete_command_argument(
|
||||
&self,
|
||||
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,945 +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();
|
||||
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 {
|
||||
self.requires_argument()
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
|
||||
@@ -828,7 +828,6 @@ impl BufferDiff {
|
||||
Some(start..end)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn update_diff(
|
||||
this: Entity<BufferDiff>,
|
||||
buffer: text::BufferSnapshot,
|
||||
@@ -838,8 +837,8 @@ impl BufferDiff {
|
||||
language: Option<Arc<Language>>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<Option<Range<Anchor>>> {
|
||||
let snapshot = if base_text_changed || language_changed {
|
||||
) -> anyhow::Result<BufferDiffSnapshot> {
|
||||
let inner = if base_text_changed || language_changed {
|
||||
cx.update(|cx| {
|
||||
Self::build(
|
||||
buffer.clone(),
|
||||
@@ -861,18 +860,45 @@ impl BufferDiff {
|
||||
})?
|
||||
.await
|
||||
};
|
||||
|
||||
this.update(cx, |this, _| this.set_state(snapshot, &buffer))
|
||||
Ok(BufferDiffSnapshot {
|
||||
inner,
|
||||
secondary_diff: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_diff_from(
|
||||
pub fn set_snapshot(
|
||||
&mut self,
|
||||
buffer: &text::BufferSnapshot,
|
||||
other: &Entity<Self>,
|
||||
new_snapshot: BufferDiffSnapshot,
|
||||
language_changed: bool,
|
||||
secondary_changed_range: Option<Range<Anchor>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Range<Anchor>> {
|
||||
let other = other.read(cx).inner.clone();
|
||||
self.set_state(other, buffer)
|
||||
let changed_range = self.set_state(new_snapshot.inner, 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(
|
||||
|
||||
@@ -229,7 +229,6 @@ impl Database {
|
||||
}
|
||||
|
||||
/// Creates a new channel message.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn create_channel_message(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
||||
@@ -122,7 +122,6 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn get_or_create_user_by_github_account_tx(
|
||||
&self,
|
||||
github_login: &str,
|
||||
|
||||
@@ -289,7 +289,6 @@ impl LlmDatabase {
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn record_usage(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
@@ -554,7 +553,6 @@ impl LlmDatabase {
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn update_usage_for_measure(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
|
||||
@@ -33,7 +33,6 @@ pub struct LlmTokenClaims {
|
||||
const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
impl LlmTokenClaims {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn create(
|
||||
user: &user::Model,
|
||||
is_staff: bool,
|
||||
|
||||
@@ -697,7 +697,6 @@ impl Server {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn handle_connection(
|
||||
self: &Arc<Self>,
|
||||
connection: Connection,
|
||||
@@ -1081,7 +1080,6 @@ pub fn routes(server: Arc<Server>) -> Router<(), Body> {
|
||||
.layer(Extension(server))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn handle_websocket_request(
|
||||
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
|
||||
app_version_header: Option<TypedHeader<AppVersionHeader>>,
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::{
|
||||
tests::{rust_lang, TestServer},
|
||||
};
|
||||
use call::ActiveCall;
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
actions::{
|
||||
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("4c4c4c", 3..4),
|
||||
],
|
||||
permalinks: HashMap::default(), // This field is deprecrated
|
||||
messages: [
|
||||
("1b1b1b", "message for idx-0"),
|
||||
("0d0d0d", "message for idx-1"),
|
||||
|
||||
@@ -6770,7 +6770,7 @@ async fn test_remote_git_branches(
|
||||
|
||||
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
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -6790,23 +6790,15 @@ async fn test_remote_git_branches(
|
||||
assert_eq!(host_branch.name, branches[2]);
|
||||
|
||||
// Also try creating a new branch
|
||||
cx_b.update(|cx| {
|
||||
repo_b
|
||||
.read(cx)
|
||||
.create_branch("totally-new-branch".to_string())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
cx_b.update(|cx| repo_b.read(cx).create_branch("totally-new-branch"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
cx_b.update(|cx| {
|
||||
repo_b
|
||||
.read(cx)
|
||||
.change_branch("totally-new-branch".to_string())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
cx_b.update(|cx| repo_b.read(cx).change_branch("totally-new-branch"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
|
||||
@@ -463,7 +463,6 @@ impl<T: RandomizedTest> TestPlan<T> {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn apply_server_operation(
|
||||
plan: Arc<Mutex<Self>>,
|
||||
deterministic: BackgroundExecutor,
|
||||
|
||||
@@ -294,7 +294,7 @@ async fn test_ssh_collaboration_git_branches(
|
||||
|
||||
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
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -316,23 +316,15 @@ async fn test_ssh_collaboration_git_branches(
|
||||
assert_eq!(server_branch.name, branches[2]);
|
||||
|
||||
// Also try creating a new branch
|
||||
cx_b.update(|cx| {
|
||||
repo_b
|
||||
.read(cx)
|
||||
.create_branch("totally-new-branch".to_string())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
cx_b.update(|cx| repo_b.read(cx).create_branch("totally-new-branch"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
cx_b.update(|cx| {
|
||||
repo_b
|
||||
.read(cx)
|
||||
.change_branch("totally-new-branch".to_string())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
cx_b.update(|cx| repo_b.read(cx).change_branch("totally-new-branch"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
|
||||
@@ -869,7 +869,6 @@ impl CollabPanel {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_participant_project(
|
||||
&self,
|
||||
project_id: u64,
|
||||
|
||||
@@ -113,7 +113,6 @@ pub struct DisplayMap {
|
||||
}
|
||||
|
||||
impl DisplayMap {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
buffer: Entity<MultiBuffer>,
|
||||
font: Font,
|
||||
|
||||
@@ -726,7 +726,6 @@ impl BlockMap {
|
||||
self.show_excerpt_controls
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn header_and_footer_blocks<'a, R, T>(
|
||||
show_excerpt_controls: bool,
|
||||
excerpt_footer_height: u32,
|
||||
|
||||
@@ -2874,6 +2874,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
let selections = self.selections.all_adjusted(cx);
|
||||
let multi_section = selections.len() > 1;
|
||||
let mut bracket_inserted = false;
|
||||
let mut edits = Vec::new();
|
||||
let mut linked_edits = HashMap::<_, Vec<_>>::default();
|
||||
@@ -2890,7 +2891,7 @@ impl Editor {
|
||||
let mut bracket_pair = None;
|
||||
let mut is_bracket_pair_start = false;
|
||||
let mut is_bracket_pair_end = false;
|
||||
if !text.is_empty() {
|
||||
if !multi_section && !text.is_empty() {
|
||||
// `text` can be empty when a user is using IME (e.g. Chinese Wubi Simplified)
|
||||
// and they are removing the character that triggered IME popup.
|
||||
for (pair, enabled) in scope.brackets() {
|
||||
@@ -5931,7 +5932,6 @@ impl Editor {
|
||||
const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.);
|
||||
const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.);
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_popover(
|
||||
&mut self,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
@@ -6043,7 +6043,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_modifier_jump_popover(
|
||||
&mut self,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
@@ -6139,7 +6138,6 @@ impl Editor {
|
||||
Some((element, origin))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_scroll_popover(
|
||||
&mut self,
|
||||
to_y: impl Fn(Size<Pixels>) -> Pixels,
|
||||
@@ -6170,7 +6168,6 @@ impl Editor {
|
||||
Some((element, origin))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_eager_jump_popover(
|
||||
&mut self,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
@@ -6240,7 +6237,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_end_of_line_popover(
|
||||
self: &mut Editor,
|
||||
label: &'static str,
|
||||
@@ -6299,7 +6295,6 @@ impl Editor {
|
||||
Some((element, origin))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_diff_popover(
|
||||
self: &Editor,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
@@ -6607,7 +6602,6 @@ impl Editor {
|
||||
editor_bg_color.blend(accent_color.opacity(0.6))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_cursor_popover(
|
||||
&self,
|
||||
min_width: Pixels,
|
||||
@@ -11639,7 +11633,7 @@ impl Editor {
|
||||
fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
self.go_to_hunk_after_or_before_position(
|
||||
self.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
selection.head(),
|
||||
Direction::Next,
|
||||
@@ -11648,7 +11642,7 @@ impl Editor {
|
||||
);
|
||||
}
|
||||
|
||||
fn go_to_hunk_after_or_before_position(
|
||||
fn go_to_hunk_before_or_after_position(
|
||||
&mut self,
|
||||
snapshot: &EditorSnapshot,
|
||||
position: Point,
|
||||
@@ -11699,7 +11693,7 @@ impl Editor {
|
||||
) {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
self.go_to_hunk_after_or_before_position(
|
||||
self.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
selection.head(),
|
||||
Direction::Prev,
|
||||
@@ -13861,21 +13855,6 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let newest_range = self.selections.newest::<Point>(cx).range();
|
||||
|
||||
let run_twice = snapshot
|
||||
.hunks_for_ranges([newest_range])
|
||||
.first()
|
||||
.is_some_and(|hunk| {
|
||||
let next_line = Point::new(hunk.row_range.end.0 + 1, 0);
|
||||
self.hunk_after_position(&snapshot, next_line)
|
||||
.is_some_and(|other| other.row_range == hunk.row_range)
|
||||
});
|
||||
|
||||
if run_twice {
|
||||
self.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
}
|
||||
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
||||
self.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
}
|
||||
|
||||
@@ -958,7 +958,6 @@ impl EditorElement {
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_selections(
|
||||
&self,
|
||||
start_anchor: Anchor,
|
||||
@@ -1130,7 +1129,6 @@ impl EditorElement {
|
||||
cursors
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_visible_cursors(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
@@ -1484,7 +1482,6 @@ impl EditorElement {
|
||||
axis_pair(horizontal_scrollbar, vertical_scrollbar)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint_crease_toggles(
|
||||
&self,
|
||||
crease_toggles: &mut [Option<AnyElement>],
|
||||
@@ -1519,7 +1516,6 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint_crease_trailers(
|
||||
&self,
|
||||
trailers: Vec<Option<AnyElement>>,
|
||||
@@ -1596,7 +1592,6 @@ impl EditorElement {
|
||||
display_hunks
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_inline_diagnostics(
|
||||
&self,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
@@ -1747,7 +1742,6 @@ impl EditorElement {
|
||||
elements
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_inline_blame(
|
||||
&self,
|
||||
display_row: DisplayRow,
|
||||
@@ -1827,7 +1821,6 @@ impl EditorElement {
|
||||
Some(element)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_blame_entries(
|
||||
&self,
|
||||
buffer_rows: &[RowInfo],
|
||||
@@ -1896,7 +1889,6 @@ impl EditorElement {
|
||||
Some(shaped_lines)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_indent_guides(
|
||||
&self,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
@@ -2014,7 +2006,6 @@ impl EditorElement {
|
||||
(offset_y, length)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_run_indicators(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -2108,7 +2099,6 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_code_actions_indicator(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -2207,7 +2197,6 @@ impl EditorElement {
|
||||
relative_rows
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_line_numbers(
|
||||
&self,
|
||||
gutter_hitbox: Option<&Hitbox>,
|
||||
@@ -2423,7 +2412,6 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint_lines(
|
||||
&self,
|
||||
start_row: DisplayRow,
|
||||
@@ -2450,7 +2438,6 @@ impl EditorElement {
|
||||
line_elements
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_block(
|
||||
&self,
|
||||
block: &Block,
|
||||
@@ -2950,7 +2937,6 @@ impl EditorElement {
|
||||
}))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_blocks(
|
||||
&self,
|
||||
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
|
||||
/// a restart of rendering for the editor based on the new sizes.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_blocks(
|
||||
&self,
|
||||
blocks: &mut Vec<BlockLayout>,
|
||||
@@ -3179,7 +3164,6 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_sticky_buffer_header(
|
||||
&self,
|
||||
StickyHeaderExcerpt {
|
||||
@@ -3254,7 +3238,6 @@ impl EditorElement {
|
||||
header
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_cursor_popovers(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -3443,7 +3426,6 @@ impl EditorElement {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_gutter_menu(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -3496,7 +3478,6 @@ impl EditorElement {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_popovers_above_or_below_line(
|
||||
&self,
|
||||
target_position: gpui::Point<Pixels>,
|
||||
@@ -3610,7 +3591,6 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_context_menu_aside(
|
||||
&self,
|
||||
y_flipped: bool,
|
||||
@@ -3806,7 +3786,6 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_hover_popovers(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
@@ -3923,7 +3902,6 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_diff_hunk_controls(
|
||||
&self,
|
||||
row_range: Range<DisplayRow>,
|
||||
@@ -4008,7 +3986,6 @@ impl EditorElement {
|
||||
controls
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_signature_help(
|
||||
&self,
|
||||
hitbox: &Hitbox,
|
||||
@@ -4676,6 +4653,7 @@ impl EditorElement {
|
||||
};
|
||||
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);
|
||||
self.paint_lines(&invisible_display_ranges, layout, window, cx);
|
||||
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) {
|
||||
if layout.redacted_ranges.is_empty() {
|
||||
return;
|
||||
@@ -5304,7 +5294,6 @@ impl EditorElement {
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn paint_highlighted_range(
|
||||
&self,
|
||||
range: Range<DisplayPoint>,
|
||||
@@ -5730,7 +5719,6 @@ impl AcceptEditPredictionBinding {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint_gutter_button(
|
||||
button: IconButton,
|
||||
row: DisplayRow,
|
||||
@@ -5981,7 +5969,6 @@ impl fmt::Debug for LineFragment {
|
||||
}
|
||||
|
||||
impl LineWithInvisibles {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn from_chunks<'a>(
|
||||
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
|
||||
editor_style: &EditorStyle,
|
||||
@@ -6186,7 +6173,6 @@ impl LineWithInvisibles {
|
||||
layouts
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
line_height: Pixels,
|
||||
@@ -6221,7 +6207,6 @@ impl LineWithInvisibles {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw(
|
||||
&self,
|
||||
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(
|
||||
&self,
|
||||
selection_ranges: &[Range<DisplayPoint>],
|
||||
@@ -7659,7 +7672,6 @@ struct ScrollbarRangeData {
|
||||
}
|
||||
|
||||
impl ScrollbarRangeData {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
scrollbar_bounds: Bounds<Pixels>,
|
||||
letter_size: Size<Pixels>,
|
||||
@@ -9014,7 +9026,7 @@ fn diff_hunk_controls(
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let position =
|
||||
hunk_range.end.to_point(&snapshot.buffer_snapshot);
|
||||
editor.go_to_hunk_after_or_before_position(
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
position,
|
||||
Direction::Next,
|
||||
@@ -9050,7 +9062,7 @@ fn diff_hunk_controls(
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let point =
|
||||
hunk_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
editor.go_to_hunk_after_or_before_position(
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
point,
|
||||
Direction::Prev,
|
||||
|
||||
@@ -370,7 +370,6 @@ impl GitBlame {
|
||||
async move {
|
||||
let Some(Blame {
|
||||
entries,
|
||||
permalinks,
|
||||
messages,
|
||||
remote_url,
|
||||
}) = blame.await?
|
||||
@@ -379,13 +378,8 @@ impl GitBlame {
|
||||
};
|
||||
|
||||
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
|
||||
let commit_details = parse_commit_messages(
|
||||
messages,
|
||||
remote_url,
|
||||
&permalinks,
|
||||
provider_registry,
|
||||
)
|
||||
.await;
|
||||
let commit_details =
|
||||
parse_commit_messages(messages, remote_url, provider_registry).await;
|
||||
|
||||
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(
|
||||
messages: impl IntoIterator<Item = (Oid, String)>,
|
||||
remote_url: Option<String>,
|
||||
deprecated_permalinks: &HashMap<Oid, Url>,
|
||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
) -> HashMap<Oid, ParsedCommitMessage> {
|
||||
let mut commit_details = HashMap::default();
|
||||
@@ -495,11 +488,7 @@ async fn parse_commit_messages(
|
||||
},
|
||||
))
|
||||
} else {
|
||||
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
|
||||
// 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()
|
||||
continue;
|
||||
};
|
||||
|
||||
let remote = parsed_remote_url
|
||||
|
||||
@@ -223,7 +223,6 @@ impl ScrollManager {
|
||||
self.anchor.scroll_position(snapshot)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn set_scroll_position(
|
||||
&mut self,
|
||||
scroll_position: gpui::Point<f32>,
|
||||
@@ -298,7 +297,6 @@ impl ScrollManager {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn set_anchor(
|
||||
&mut self,
|
||||
anchor: ScrollAnchor,
|
||||
|
||||
@@ -22,7 +22,6 @@ collections.workspace = true
|
||||
env_logger.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
@@ -5,7 +5,6 @@ use client::{Client, UserStore};
|
||||
use clock::RealSystemClock;
|
||||
use collections::BTreeMap;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use git::GitHostingProviderRegistry;
|
||||
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Entity};
|
||||
use http_client::{HttpClient, Method};
|
||||
use language::LanguageRegistry;
|
||||
@@ -274,8 +273,7 @@ async fn run_evaluation(
|
||||
let repos_dir = Path::new(EVAL_REPOS_DIR);
|
||||
let db_path = Path::new(EVAL_DB_PATH);
|
||||
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(git_hosting_provider_registry, None)) as Arc<dyn Fs>;
|
||||
let fs = Arc::new(RealFs::new(None)) as Arc<dyn Fs>;
|
||||
let clock = Arc::new(RealSystemClock);
|
||||
let client = cx
|
||||
.update(|cx| {
|
||||
@@ -399,7 +397,6 @@ async fn run_evaluation(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn run_eval_project(
|
||||
evaluation_project: EvaluationProject,
|
||||
user_store: &Entity<UserStore>,
|
||||
|
||||
@@ -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"));
|
||||
|
||||
mod wit {
|
||||
#![allow(clippy::too_many_arguments, clippy::missing_safety_doc)]
|
||||
|
||||
wit_bindgen::generate!({
|
||||
skip: ["init-extension"],
|
||||
|
||||
@@ -218,7 +218,6 @@ impl ExtensionStore {
|
||||
cx.global::<GlobalExtensionStore>().0.clone()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
extensions_dir: PathBuf,
|
||||
build_dir: Option<PathBuf>,
|
||||
|
||||
@@ -80,11 +80,6 @@ impl FeatureFlag for PredictEditsNonEagerModeFeatureFlag {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GitUiFeatureFlag;
|
||||
impl FeatureFlag for GitUiFeatureFlag {
|
||||
const NAME: &'static str = "git-ui";
|
||||
}
|
||||
|
||||
pub struct Remoting {}
|
||||
impl FeatureFlag for Remoting {
|
||||
const NAME: &'static str = "remoting";
|
||||
|
||||
@@ -653,7 +653,6 @@ impl FileSearchQuery {
|
||||
}
|
||||
|
||||
impl FileFinderDelegate {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
file_finder: WeakEntity<FileFinder>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
|
||||
@@ -436,8 +436,8 @@ impl PickerDelegate for NewPathDelegate {
|
||||
)
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
|
||||
"Type a path...".into()
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
Some("Type a path...".into())
|
||||
}
|
||||
|
||||
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 {
|
||||
if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone()) {
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
let text = if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone())
|
||||
{
|
||||
error
|
||||
} else {
|
||||
"No such file or directory".into()
|
||||
}
|
||||
};
|
||||
Some(text)
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
|
||||
@@ -11,7 +11,6 @@ use collections::HashMap;
|
||||
use git::status::StatusCode;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use git::status::TrackedStatus;
|
||||
use git::GitHostingProviderRegistry;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use git::{repository::RepoPath, status::FileStatus};
|
||||
|
||||
@@ -247,7 +246,6 @@ impl From<MTime> for proto::Timestamp {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RealFs {
|
||||
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
git_binary_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
@@ -300,14 +298,8 @@ impl FileHandle for std::fs::File {
|
||||
pub struct RealWatcher {}
|
||||
|
||||
impl RealFs {
|
||||
pub fn new(
|
||||
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
git_binary_path: Option<PathBuf>,
|
||||
) -> Self {
|
||||
Self {
|
||||
git_hosting_provider_registry,
|
||||
git_binary_path,
|
||||
}
|
||||
pub fn new(git_binary_path: Option<PathBuf>) -> Self {
|
||||
Self { git_binary_path }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,7 +762,6 @@ impl Fs for RealFs {
|
||||
Some(Arc::new(RealGitRepository::new(
|
||||
repo,
|
||||
self.git_binary_path.clone(),
|
||||
self.git_hosting_provider_registry.clone(),
|
||||
)))
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,6 @@ impl<'a> Matcher<'a> {
|
||||
score
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn recursive_score_match(
|
||||
&mut self,
|
||||
path: &[char],
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
use crate::commit::get_messages;
|
||||
use crate::{parse_git_remote_url, BuildCommitPermalinkParams, GitHostingProviderRegistry, Oid};
|
||||
use crate::Oid;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::Write;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use std::{ops::Range, path::Path};
|
||||
use text::Rope;
|
||||
use time::macros::format_description;
|
||||
use time::OffsetDateTime;
|
||||
use time::UtcOffset;
|
||||
use url::Url;
|
||||
|
||||
pub use git2 as libgit;
|
||||
|
||||
@@ -19,7 +17,6 @@ pub use git2 as libgit;
|
||||
pub struct Blame {
|
||||
pub entries: Vec<BlameEntry>,
|
||||
pub messages: HashMap<Oid, String>,
|
||||
pub permalinks: HashMap<Oid, Url>,
|
||||
pub remote_url: Option<String>,
|
||||
}
|
||||
|
||||
@@ -30,32 +27,15 @@ impl Blame {
|
||||
path: &Path,
|
||||
content: &Rope,
|
||||
remote_url: Option<String>,
|
||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
) -> Result<Self> {
|
||||
let output = run_git_blame(git_binary, working_directory, path, content)?;
|
||||
let mut entries = parse_git_blame(&output)?;
|
||||
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 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() {
|
||||
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<_>>();
|
||||
@@ -64,7 +44,6 @@ impl Blame {
|
||||
|
||||
Ok(Self {
|
||||
entries,
|
||||
permalinks,
|
||||
messages,
|
||||
remote_url,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::status::FileStatus;
|
||||
use crate::GitHostingProviderRegistry;
|
||||
use crate::SHORT_SHA_LENGTH;
|
||||
use crate::{blame::Blame, status::GitStatus};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use askpass::{AskPassResult, AskPassSession};
|
||||
@@ -57,6 +57,14 @@ pub struct Upstream {
|
||||
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)]
|
||||
pub enum UpstreamTracking {
|
||||
/// Remote ref not present in local repository.
|
||||
@@ -120,6 +128,12 @@ pub struct CommitDetails {
|
||||
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)]
|
||||
pub struct Remote {
|
||||
pub name: SharedString,
|
||||
@@ -149,7 +163,12 @@ pub trait GitRepository: Send + Sync {
|
||||
/// Also returns `None` for symlinks.
|
||||
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.
|
||||
fn remote_url(&self, name: &str) -> Option<String>;
|
||||
@@ -167,8 +186,13 @@ pub trait GitRepository: Send + Sync {
|
||||
fn create_branch(&self, _: &str) -> Result<()>;
|
||||
fn branch_exits(&self, _: &str) -> Result<bool>;
|
||||
|
||||
fn reset(&self, commit: &str, mode: ResetMode) -> Result<()>;
|
||||
fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()>;
|
||||
fn reset(&self, commit: &str, mode: ResetMode, env: &HashMap<String, String>) -> Result<()>;
|
||||
fn checkout_files(
|
||||
&self,
|
||||
commit: &str,
|
||||
paths: &[RepoPath],
|
||||
env: &HashMap<String, String>,
|
||||
) -> Result<()>;
|
||||
|
||||
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.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// 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(
|
||||
&self,
|
||||
@@ -203,6 +232,7 @@ pub trait GitRepository: Send + Sync {
|
||||
upstream_name: &str,
|
||||
options: Option<PushOptions>,
|
||||
askpass: AskPassSession,
|
||||
env: &HashMap<String, String>,
|
||||
) -> Result<RemoteCommandOutput>;
|
||||
|
||||
fn pull(
|
||||
@@ -210,8 +240,13 @@ pub trait GitRepository: Send + Sync {
|
||||
branch_name: &str,
|
||||
upstream_name: &str,
|
||||
askpass: AskPassSession,
|
||||
env: &HashMap<String, String>,
|
||||
) -> Result<RemoteCommandOutput>;
|
||||
fn fetch(
|
||||
&self,
|
||||
askpass: AskPassSession,
|
||||
env: &HashMap<String, String>,
|
||||
) -> Result<RemoteCommandOutput>;
|
||||
fn fetch(&self, askpass: AskPassSession) -> Result<RemoteCommandOutput>;
|
||||
|
||||
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 repository: Mutex<git2::Repository>,
|
||||
pub git_binary_path: PathBuf,
|
||||
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
}
|
||||
|
||||
impl RealGitRepository {
|
||||
pub fn new(
|
||||
repository: git2::Repository,
|
||||
git_binary_path: Option<PathBuf>,
|
||||
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
) -> Self {
|
||||
pub fn new(repository: git2::Repository, git_binary_path: Option<PathBuf>) -> Self {
|
||||
Self {
|
||||
repository: Mutex::new(repository),
|
||||
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)
|
||||
}
|
||||
|
||||
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 mode_flag = match mode {
|
||||
@@ -317,6 +346,7 @@ impl GitRepository for RealGitRepository {
|
||||
};
|
||||
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.envs(env)
|
||||
.current_dir(&working_directory)
|
||||
.args(["reset", mode_flag, commit])
|
||||
.output()?;
|
||||
@@ -329,7 +359,12 @@ impl GitRepository for RealGitRepository {
|
||||
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() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -337,6 +372,7 @@ impl GitRepository for RealGitRepository {
|
||||
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.envs(env)
|
||||
.args(["checkout", commit, "--"])
|
||||
.args(paths.iter().map(|path| path.as_ref()))
|
||||
.output()?;
|
||||
@@ -385,11 +421,17 @@ impl GitRepository for RealGitRepository {
|
||||
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()?;
|
||||
if let Some(content) = content {
|
||||
let mut child = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.envs(env)
|
||||
.args(["hash-object", "-w", "--stdin"])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
@@ -402,6 +444,7 @@ impl GitRepository for RealGitRepository {
|
||||
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.envs(env)
|
||||
.args(["update-index", "--add", "--cacheinfo", "100644", &sha])
|
||||
.arg(path.as_ref())
|
||||
.output()?;
|
||||
@@ -415,6 +458,7 @@ impl GitRepository for RealGitRepository {
|
||||
} else {
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.envs(env)
|
||||
.args(["update-index", "--force-remove"])
|
||||
.arg(path.as_ref())
|
||||
.output()?;
|
||||
@@ -581,7 +625,6 @@ impl GitRepository for RealGitRepository {
|
||||
path,
|
||||
&content,
|
||||
remote_url,
|
||||
self.hosting_provider_registry.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -607,12 +650,13 @@ impl GitRepository for RealGitRepository {
|
||||
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()?;
|
||||
|
||||
if !paths.is_empty() {
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.envs(env)
|
||||
.args(["update-index", "--add", "--remove", "--"])
|
||||
.args(paths.iter().map(|p| p.as_ref()))
|
||||
.output()?;
|
||||
@@ -627,12 +671,13 @@ impl GitRepository for RealGitRepository {
|
||||
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()?;
|
||||
|
||||
if !paths.is_empty() {
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.envs(env)
|
||||
.args(["reset", "--quiet", "--"])
|
||||
.args(paths.iter().map(|p| p.as_ref()))
|
||||
.output()?;
|
||||
@@ -647,11 +692,17 @@ impl GitRepository for RealGitRepository {
|
||||
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 mut cmd = new_std_command(&self.git_binary_path);
|
||||
cmd.current_dir(&working_directory)
|
||||
.envs(env)
|
||||
.args(["commit", "--quiet", "-m"])
|
||||
.arg(message)
|
||||
.arg("--cleanup=strip");
|
||||
@@ -677,11 +728,13 @@ impl GitRepository for RealGitRepository {
|
||||
remote_name: &str,
|
||||
options: Option<PushOptions>,
|
||||
ask_pass: AskPassSession,
|
||||
env: &HashMap<String, String>,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
let mut command = new_smol_command("git");
|
||||
command
|
||||
.envs(env)
|
||||
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||
@@ -705,11 +758,13 @@ impl GitRepository for RealGitRepository {
|
||||
branch_name: &str,
|
||||
remote_name: &str,
|
||||
ask_pass: AskPassSession,
|
||||
env: &HashMap<String, String>,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
let mut command = new_smol_command("git");
|
||||
command
|
||||
.envs(env)
|
||||
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||
@@ -724,11 +779,16 @@ impl GitRepository for RealGitRepository {
|
||||
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 mut command = new_smol_command("git");
|
||||
command
|
||||
.envs(env)
|
||||
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||
@@ -919,7 +979,12 @@ impl GitRepository for FakeGitRepository {
|
||||
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();
|
||||
if let Some(message) = state.simulated_index_write_error_message.clone() {
|
||||
return Err(anyhow::anyhow!(message));
|
||||
@@ -952,11 +1017,11 @@ impl GitRepository for FakeGitRepository {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn reset(&self, _: &str, _: ResetMode) -> Result<()> {
|
||||
fn reset(&self, _: &str, _: ResetMode, _: &HashMap<String, String>) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn checkout_files(&self, _: &str, _: &[RepoPath]) -> Result<()> {
|
||||
fn checkout_files(&self, _: &str, _: &[RepoPath], _: &HashMap<String, String>) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -1042,15 +1107,20 @@ impl GitRepository for FakeGitRepository {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn stage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
|
||||
fn stage_paths(&self, _paths: &[RepoPath], _env: &HashMap<String, String>) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn unstage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
|
||||
fn unstage_paths(&self, _paths: &[RepoPath], _env: &HashMap<String, String>) -> Result<()> {
|
||||
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!()
|
||||
}
|
||||
|
||||
@@ -1060,6 +1130,7 @@ impl GitRepository for FakeGitRepository {
|
||||
_remote: &str,
|
||||
_options: Option<PushOptions>,
|
||||
_ask_pass: AskPassSession,
|
||||
_env: &HashMap<String, String>,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
@@ -1069,11 +1140,16 @@ impl GitRepository for FakeGitRepository {
|
||||
_branch: &str,
|
||||
_remote: &str,
|
||||
_ask_pass: AskPassSession,
|
||||
_env: &HashMap<String, String>,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn fetch(&self, _ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
|
||||
fn fetch(
|
||||
&self,
|
||||
_ask_pass: AskPassSession,
|
||||
_env: &HashMap<String, String>,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
||||
@@ -18,13 +18,12 @@ test-support = ["multi_buffer/test-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
askpass.workspace= true
|
||||
askpass.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
@@ -51,6 +50,7 @@ strum.workspace = true
|
||||
telemetry.workspace = true
|
||||
theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
|
||||
use git::repository::Branch;
|
||||
use gpui::{
|
||||
rems, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
Task, Window,
|
||||
InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render,
|
||||
SharedString, Styled, Subscription, Task, Window,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::git::Repository;
|
||||
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 workspace::notifications::DetachAndPromptErr;
|
||||
use workspace::{ModalView, Workspace};
|
||||
@@ -51,7 +53,7 @@ pub fn open(
|
||||
let repository = workspace.project().read(cx).active_repository(cx).clone();
|
||||
let style = BranchListStyle::Modal;
|
||||
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,
|
||||
) -> Entity<BranchList> {
|
||||
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
|
||||
})
|
||||
@@ -74,8 +76,7 @@ enum BranchListStyle {
|
||||
}
|
||||
|
||||
pub struct BranchList {
|
||||
rem_width: f32,
|
||||
pub popover_handle: PopoverMenuHandle<Self>,
|
||||
width: Rems,
|
||||
pub picker: Entity<Picker<BranchListDelegate>>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
@@ -84,20 +85,26 @@ impl BranchList {
|
||||
fn new(
|
||||
repository: Option<Entity<Repository>>,
|
||||
style: BranchListStyle,
|
||||
rem_width: f32,
|
||||
width: Rems,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let popover_handle = PopoverMenuHandle::default();
|
||||
let all_branches_request = repository
|
||||
.clone()
|
||||
.map(|repository| repository.read(cx).branches());
|
||||
|
||||
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")?
|
||||
.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.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.all_branches = Some(all_branches);
|
||||
@@ -109,7 +116,7 @@ impl BranchList {
|
||||
})
|
||||
.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 _subscription = cx.subscribe(&picker, |_, _, _, cx| {
|
||||
@@ -118,11 +125,20 @@ impl BranchList {
|
||||
|
||||
Self {
|
||||
picker,
|
||||
rem_width,
|
||||
popover_handle,
|
||||
width,
|
||||
_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 EventEmitter<DismissEvent> for BranchList {}
|
||||
@@ -136,7 +152,8 @@ impl Focusable for BranchList {
|
||||
impl Render for BranchList {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w(rems(self.rem_width))
|
||||
.w(self.width)
|
||||
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
|
||||
.child(self.picker.clone())
|
||||
.on_mouse_down_out({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
@@ -149,20 +166,10 @@ impl Render for BranchList {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum BranchEntry {
|
||||
Branch(StringMatch),
|
||||
History(String),
|
||||
NewBranch { name: String },
|
||||
}
|
||||
|
||||
impl BranchEntry {
|
||||
fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Branch(branch) => &branch.string,
|
||||
Self::History(branch) => &branch,
|
||||
Self::NewBranch { name } => &name,
|
||||
}
|
||||
}
|
||||
struct BranchEntry {
|
||||
branch: Branch,
|
||||
positions: Vec<usize>,
|
||||
is_new: bool,
|
||||
}
|
||||
|
||||
pub struct BranchListDelegate {
|
||||
@@ -172,16 +179,11 @@ pub struct BranchListDelegate {
|
||||
style: BranchListStyle,
|
||||
selected_index: usize,
|
||||
last_query: String,
|
||||
/// Max length of branch name before we truncate it and add a trailing `...`.
|
||||
branch_name_trailoff_after: usize,
|
||||
modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl BranchListDelegate {
|
||||
fn new(
|
||||
repo: Option<Entity<Repository>>,
|
||||
style: BranchListStyle,
|
||||
branch_name_trailoff_after: usize,
|
||||
) -> Self {
|
||||
fn new(repo: Option<Entity<Repository>>, style: BranchListStyle) -> Self {
|
||||
Self {
|
||||
matches: vec![],
|
||||
repo,
|
||||
@@ -189,15 +191,30 @@ impl BranchListDelegate {
|
||||
all_branches: None,
|
||||
selected_index: 0,
|
||||
last_query: Default::default(),
|
||||
branch_name_trailoff_after,
|
||||
modifiers: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn branch_count(&self) -> usize {
|
||||
self.matches
|
||||
.iter()
|
||||
.filter(|item| matches!(item, BranchEntry::Branch(_)))
|
||||
.count()
|
||||
fn create_branch(
|
||||
&self,
|
||||
new_branch_name: SharedString,
|
||||
window: &mut Window,
|
||||
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,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let Some(mut all_branches) = self.all_branches.clone() else {
|
||||
let Some(all_branches) = self.all_branches.clone() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
const RECENT_BRANCHES_COUNT: usize = 10;
|
||||
cx.spawn_in(window, move |picker, mut cx| async move {
|
||||
const RECENT_BRANCHES_COUNT: usize = 10;
|
||||
if query.is_empty() {
|
||||
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
|
||||
let mut matches: Vec<BranchEntry> = if query.is_empty() {
|
||||
all_branches
|
||||
.into_iter()
|
||||
.map(|candidate| BranchEntry::History(candidate.string))
|
||||
.take(RECENT_BRANCHES_COUNT)
|
||||
.map(|branch| BranchEntry {
|
||||
branch,
|
||||
positions: Vec::new(),
|
||||
is_new: false,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
let candidates = all_branches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name.clone()))
|
||||
.collect::<Vec<StringMatchCandidate>>();
|
||||
fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
@@ -273,20 +281,35 @@ impl PickerDelegate for BranchListDelegate {
|
||||
.await
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(BranchEntry::Branch)
|
||||
.map(|candidate| BranchEntry {
|
||||
branch: all_branches[candidate.candidate_id].clone(),
|
||||
positions: candidate.positions,
|
||||
is_new: false,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
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;
|
||||
delegate.matches = matches;
|
||||
if delegate.matches.is_empty() {
|
||||
if !query.is_empty() {
|
||||
delegate.matches.push(BranchEntry::NewBranch {
|
||||
name: query.trim().replace(' ', "-"),
|
||||
});
|
||||
}
|
||||
|
||||
delegate.selected_index = 0;
|
||||
} else {
|
||||
delegate.selected_index =
|
||||
@@ -298,10 +321,14 @@ impl PickerDelegate for BranchListDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let Some(branch) = self.matches.get(self.selected_index()) else {
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let Some(entry) = self.matches.get(self.selected_index()) else {
|
||||
return;
|
||||
};
|
||||
if entry.is_new {
|
||||
self.create_branch(entry.branch.name.clone(), window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let current_branch = self.repo.as_ref().map(|repo| {
|
||||
repo.update(cx, |repo, _| {
|
||||
@@ -311,14 +338,14 @@ impl PickerDelegate for BranchListDelegate {
|
||||
|
||||
if current_branch
|
||||
.flatten()
|
||||
.is_some_and(|current_branch| current_branch == branch.name())
|
||||
.is_some_and(|current_branch| current_branch == entry.branch.name)
|
||||
{
|
||||
cx.emit(DismissEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
cx.spawn_in(window, {
|
||||
let branch = branch.clone();
|
||||
let branch = entry.branch.clone();
|
||||
|picker, mut cx| async move {
|
||||
let branch_change_task = picker.update(&mut cx, |this, cx| {
|
||||
let repo = this
|
||||
@@ -331,22 +358,8 @@ impl PickerDelegate for BranchListDelegate {
|
||||
let cx = cx.to_async();
|
||||
|
||||
anyhow::Ok(async move {
|
||||
match branch {
|
||||
BranchEntry::Branch(StringMatch {
|
||||
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?
|
||||
}
|
||||
}
|
||||
cx.update(|cx| repo.read(cx).change_branch(&branch.name))?
|
||||
.await?
|
||||
})
|
||||
})??;
|
||||
|
||||
@@ -366,16 +379,35 @@ impl PickerDelegate for BranchListDelegate {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_header(&self, _: &mut Window, _cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let hit = &self.matches[ix];
|
||||
let shortened_branch_name =
|
||||
util::truncate_and_trailoff(&hit.name(), self.branch_name_trailoff_after);
|
||||
let entry = &self.matches[ix];
|
||||
|
||||
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(
|
||||
ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
|
||||
@@ -386,29 +418,67 @@ impl PickerDelegate for BranchListDelegate {
|
||||
})
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.when(matches!(hit, BranchEntry::History(_)), |el| {
|
||||
el.end_slot(
|
||||
Icon::new(IconName::HistoryRerun)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
})
|
||||
.map(|el| match hit {
|
||||
BranchEntry::Branch(branch) => {
|
||||
let highlights: Vec<_> = branch
|
||||
.positions
|
||||
.iter()
|
||||
.filter(|index| index < &&self.branch_name_trailoff_after)
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
el.child(HighlightedLabel::new(shortened_branch_name, highlights))
|
||||
}
|
||||
BranchEntry::History(_) => el.child(Label::new(shortened_branch_name)),
|
||||
BranchEntry::NewBranch { name } => {
|
||||
el.child(Label::new(format!("Create branch '{name}'")))
|
||||
}
|
||||
}),
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.flex_shrink()
|
||||
.overflow_x_hidden()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(div().flex_shrink().overflow_x_hidden().child(
|
||||
if entry.is_new {
|
||||
Label::new(format!(
|
||||
"Create branch \"{}\"…",
|
||||
entry.branch.name
|
||||
))
|
||||
.into_any_element()
|
||||
} else {
|
||||
HighlightedLabel::new(
|
||||
entry.branch.name.clone(),
|
||||
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 git::{Commit, GenerateCommitMessage};
|
||||
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 gpui::*;
|
||||
@@ -65,11 +65,11 @@ pub fn init(cx: &mut App) {
|
||||
}
|
||||
|
||||
pub struct CommitModal {
|
||||
branch_list: Entity<BranchList>,
|
||||
git_panel: Entity<GitPanel>,
|
||||
commit_editor: Entity<Editor>,
|
||||
restore_dock: RestoreDock,
|
||||
properties: ModalContainerProperties,
|
||||
branch_list_handle: PopoverMenuHandle<BranchList>,
|
||||
}
|
||||
|
||||
impl Focusable for CommitModal {
|
||||
@@ -146,7 +146,6 @@ impl CommitModal {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let panel = git_panel.read(cx);
|
||||
let active_repository = panel.active_repository.clone();
|
||||
let suggested_commit_message = panel.suggest_commit_message();
|
||||
|
||||
let commit_editor = git_panel.update(cx, |git_panel, cx| {
|
||||
@@ -177,11 +176,7 @@ impl CommitModal {
|
||||
let focus_handle = commit_editor.focus_handle(cx);
|
||||
|
||||
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
|
||||
if !this
|
||||
.branch_list
|
||||
.focus_handle(cx)
|
||||
.contains_focused(window, cx)
|
||||
{
|
||||
if !this.branch_list_handle.is_focused(window, cx) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
})
|
||||
@@ -190,11 +185,11 @@ impl CommitModal {
|
||||
let properties = ModalContainerProperties::new(window, 50);
|
||||
|
||||
Self {
|
||||
branch_list: branch_picker::popover(active_repository.clone(), window, cx),
|
||||
git_panel,
|
||||
commit_editor,
|
||||
restore_dock,
|
||||
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 {
|
||||
let git_panel = self.git_panel.clone();
|
||||
|
||||
let (branch, can_commit, tooltip, commit_label, co_authors, generate_commit_message) =
|
||||
let (can_commit, tooltip, commit_label, co_authors, generate_commit_message, active_repo) =
|
||||
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 title = git_panel.commit_button_title();
|
||||
let co_authors = git_panel.render_co_authors(cx);
|
||||
let generate_commit_message = git_panel.render_generate_commit_message_button(cx);
|
||||
let active_repo = git_panel.active_repository.clone();
|
||||
(
|
||||
branch,
|
||||
can_commit,
|
||||
tooltip,
|
||||
title,
|
||||
co_authors,
|
||||
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)
|
||||
.icon(IconName::GitBranch)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -276,10 +266,8 @@ impl CommitModal {
|
||||
.style(ButtonStyle::Transparent);
|
||||
|
||||
let branch_picker = PopoverMenu::new("popover-button")
|
||||
.menu({
|
||||
let branch_list = self.branch_list.clone();
|
||||
move |_window, _cx| Some(branch_list.clone())
|
||||
})
|
||||
.menu(move |window, cx| Some(branch_picker::popover(active_repo.clone(), window, cx)))
|
||||
.with_handle(self.branch_list_handle.clone())
|
||||
.trigger_with_tooltip(
|
||||
branch_picker_button,
|
||||
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
|
||||
@@ -289,6 +277,7 @@ impl CommitModal {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
});
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
let close_kb_hint =
|
||||
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
|
||||
@@ -300,12 +289,9 @@ impl CommitModal {
|
||||
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)
|
||||
.tooltip({
|
||||
let panel_editor_focus_handle = panel_editor_focus_handle.clone();
|
||||
let panel_editor_focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(tooltip, &Commit, &panel_editor_focus_handle, window, cx)
|
||||
}
|
||||
@@ -330,7 +316,14 @@ impl CommitModal {
|
||||
.child(
|
||||
h_flex()
|
||||
.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(co_authors),
|
||||
)
|
||||
@@ -357,6 +350,14 @@ impl CommitModal {
|
||||
.update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
|
||||
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 {
|
||||
@@ -379,17 +380,17 @@ impl Render for CommitModal {
|
||||
}))
|
||||
.on_action(
|
||||
cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
|
||||
toggle_branch_picker(this, window, cx);
|
||||
this.toggle_branch_selector(window, cx);
|
||||
}),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &zed_actions::git::CheckoutBranch, window, cx| {
|
||||
toggle_branch_picker(this, window, cx);
|
||||
this.toggle_branch_selector(window, cx);
|
||||
}),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &zed_actions::git::Switch, window, cx| {
|
||||
toggle_branch_picker(this, window, cx);
|
||||
this.toggle_branch_selector(window, 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);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -103,9 +103,14 @@ enum TrashCancel {
|
||||
Cancel,
|
||||
}
|
||||
|
||||
fn git_panel_context_menu(window: &mut Window, cx: &mut App) -> Entity<ContextMenu> {
|
||||
fn git_panel_context_menu(
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |context_menu, _, _| {
|
||||
context_menu
|
||||
.context(focus_handle)
|
||||
.action("Stage All", StageAll.boxed_clone())
|
||||
.action("Unstage All", UnstageAll.boxed_clone())
|
||||
.separator()
|
||||
@@ -199,21 +204,6 @@ pub struct GitStatusEntry {
|
||||
pub(crate) staging: StageStatus,
|
||||
}
|
||||
|
||||
impl GitStatusEntry {
|
||||
fn display_name(&self) -> String {
|
||||
self.worktree_path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| self.worktree_path.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
fn parent_dir(&self) -> Option<String> {
|
||||
self.worktree_path
|
||||
.parent()
|
||||
.map(|parent| parent.to_string_lossy().into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum TargetStatus {
|
||||
Staged,
|
||||
@@ -255,9 +245,7 @@ pub struct GitPanel {
|
||||
pending_serialization: Task<Option<()>>,
|
||||
pub(crate) project: Entity<Project>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
vertical_scrollbar_state: ScrollbarState,
|
||||
horizontal_scrollbar_state: ScrollbarState,
|
||||
max_width_item_index: Option<usize>,
|
||||
scrollbar_state: ScrollbarState,
|
||||
selected_entry: Option<usize>,
|
||||
marked_entries: Vec<usize>,
|
||||
show_scrollbar: bool,
|
||||
@@ -364,9 +352,7 @@ impl GitPanel {
|
||||
)
|
||||
.detach();
|
||||
|
||||
let vertical_scrollbar_state =
|
||||
ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity());
|
||||
let horizontal_scrollbar_state =
|
||||
let scrollbar_state =
|
||||
ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity());
|
||||
|
||||
let mut git_panel = Self {
|
||||
@@ -392,9 +378,7 @@ impl GitPanel {
|
||||
single_tracked_entry: None,
|
||||
project,
|
||||
scroll_handle,
|
||||
vertical_scrollbar_state,
|
||||
horizontal_scrollbar_state,
|
||||
max_width_item_index: None,
|
||||
scrollbar_state,
|
||||
selected_entry: None,
|
||||
marked_entries: Vec::new(),
|
||||
show_scrollbar: false,
|
||||
@@ -710,10 +694,6 @@ impl GitPanel {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn editor_focus_handle(&self, cx: &mut Context<Self>) -> FocusHandle {
|
||||
self.commit_editor.focus_handle(cx).clone()
|
||||
}
|
||||
|
||||
fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.commit_editor.update(cx, |editor, cx| {
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
@@ -766,6 +746,7 @@ impl GitPanel {
|
||||
.as_ref()
|
||||
{
|
||||
project_diff.focus_handle(cx).focus(window);
|
||||
project_diff.update(cx, |project_diff, cx| project_diff.autoscroll(cx));
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -923,13 +904,14 @@ impl GitPanel {
|
||||
let buffers = futures::future::join_all(tasks).await;
|
||||
|
||||
active_repository
|
||||
.update(&mut cx, |repo, _| {
|
||||
.update(&mut cx, |repo, cx| {
|
||||
repo.checkout_files(
|
||||
"HEAD",
|
||||
entries
|
||||
.iter()
|
||||
.map(|entries| entries.repo_path.clone())
|
||||
.collect(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await??;
|
||||
@@ -1253,6 +1235,35 @@ impl GitPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn stage_selected(&mut self, _: &git::StageFile, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(selected_entry) = self.get_selected_entry() else {
|
||||
return;
|
||||
};
|
||||
let Some(status_entry) = selected_entry.status_entry() else {
|
||||
return;
|
||||
};
|
||||
if status_entry.staging != StageStatus::Staged {
|
||||
self.change_file_stage(true, vec![status_entry.clone()], cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn unstage_selected(
|
||||
&mut self,
|
||||
_: &git::UnstageFile,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(selected_entry) = self.get_selected_entry() else {
|
||||
return;
|
||||
};
|
||||
let Some(status_entry) = selected_entry.status_entry() else {
|
||||
return;
|
||||
};
|
||||
if status_entry.staging != StageStatus::Unstaged {
|
||||
self.change_file_stage(false, vec![status_entry.clone()], cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self
|
||||
.commit_editor
|
||||
@@ -1311,7 +1322,8 @@ impl GitPanel {
|
||||
|
||||
let task = if self.has_staged_changes() {
|
||||
// Repository serializes all git operations, so we can just send a commit immediately
|
||||
let commit_task = active_repository.read(cx).commit(message.into(), None);
|
||||
let commit_task =
|
||||
active_repository.update(cx, |repo, cx| repo.commit(message.into(), None, cx));
|
||||
cx.background_spawn(async move { commit_task.await? })
|
||||
} else {
|
||||
let changed_files = self
|
||||
@@ -1332,7 +1344,7 @@ impl GitPanel {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
stage_task.await?;
|
||||
let commit_task = active_repository
|
||||
.update(&mut cx, |repo, _| repo.commit(message.into(), None))?;
|
||||
.update(&mut cx, |repo, cx| repo.commit(message.into(), None, cx))?;
|
||||
commit_task.await?
|
||||
})
|
||||
};
|
||||
@@ -1368,7 +1380,7 @@ impl GitPanel {
|
||||
if let Ok(true) = confirmation.await {
|
||||
let prior_head = prior_head.await?;
|
||||
|
||||
repo.update(&mut cx, |repo, _| repo.reset("HEAD^", ResetMode::Soft))?
|
||||
repo.update(&mut cx, |repo, cx| repo.reset("HEAD^", ResetMode::Soft, cx))?
|
||||
.await??;
|
||||
|
||||
Ok(Some(prior_head))
|
||||
@@ -2016,7 +2028,6 @@ impl GitPanel {
|
||||
let mut conflict_entries = Vec::new();
|
||||
let mut last_staged = None;
|
||||
let mut staged_count = 0;
|
||||
let mut max_width_item: Option<(RepoPath, usize)> = None;
|
||||
|
||||
let Some(repo) = self.active_repository.as_ref() else {
|
||||
// Just clear entries if no repository is active.
|
||||
@@ -2062,21 +2073,6 @@ impl GitPanel {
|
||||
last_staged = Some(entry.clone());
|
||||
}
|
||||
|
||||
let width_estimate = Self::item_width_estimate(
|
||||
entry.parent_dir().map(|s| s.len()).unwrap_or(0),
|
||||
entry.display_name().len(),
|
||||
);
|
||||
|
||||
match max_width_item.as_mut() {
|
||||
Some((repo_path, estimate)) => {
|
||||
if width_estimate > *estimate {
|
||||
*repo_path = entry.repo_path.clone();
|
||||
*estimate = width_estimate;
|
||||
}
|
||||
}
|
||||
None => max_width_item = Some((entry.repo_path.clone(), width_estimate)),
|
||||
}
|
||||
|
||||
if is_conflict {
|
||||
conflict_entries.push(entry);
|
||||
} else if is_new {
|
||||
@@ -2149,15 +2145,6 @@ impl GitPanel {
|
||||
.extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry));
|
||||
}
|
||||
|
||||
if let Some((repo_path, _)) = max_width_item {
|
||||
self.max_width_item_index = self.entries.iter().position(|entry| match entry {
|
||||
GitListEntry::GitStatusEntry(git_status_entry) => {
|
||||
git_status_entry.repo_path == repo_path
|
||||
}
|
||||
GitListEntry::Header(_) => false,
|
||||
});
|
||||
}
|
||||
|
||||
self.update_counts(repo);
|
||||
|
||||
self.select_first_entry_if_none(cx);
|
||||
@@ -2319,10 +2306,16 @@ impl GitPanel {
|
||||
self.has_staged_changes()
|
||||
}
|
||||
|
||||
// eventually we'll need to take depth into account here
|
||||
// if we add a tree view
|
||||
fn item_width_estimate(path: usize, file_name: usize) -> usize {
|
||||
path + file_name
|
||||
fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
PopoverMenu::new(id.into())
|
||||
.trigger(
|
||||
IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
)
|
||||
.menu(move |window, cx| Some(git_panel_context_menu(focus_handle.clone(), window, cx)))
|
||||
.anchor(Corner::TopRight)
|
||||
}
|
||||
|
||||
pub(crate) fn render_generate_commit_message_button(
|
||||
@@ -2368,10 +2361,7 @@ impl GitPanel {
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Tooltip::simple(
|
||||
"You must have either staged changes or tracked files to generate a commit message",
|
||||
cx,
|
||||
)
|
||||
Tooltip::simple("No changes to commit", cx)
|
||||
}
|
||||
})
|
||||
.disabled(!can_commit)
|
||||
@@ -2421,10 +2411,7 @@ impl GitPanel {
|
||||
if self.has_unstaged_conflicts() {
|
||||
(false, "You must resolve conflicts before committing")
|
||||
} else if !self.has_staged_changes() && !self.has_tracked_changes() {
|
||||
(
|
||||
false,
|
||||
"You must have either staged changes or tracked files to commit",
|
||||
)
|
||||
(false, "No changes to commit")
|
||||
} else if self.pending_commit.is_some() {
|
||||
(false, "Commit in progress")
|
||||
} else if self.custom_or_suggested_commit_message(cx).is_none() {
|
||||
@@ -2464,7 +2451,7 @@ impl GitPanel {
|
||||
let text;
|
||||
let action;
|
||||
let tooltip;
|
||||
if self.total_staged_count() == self.entry_count {
|
||||
if self.total_staged_count() == self.entry_count && self.entry_count > 0 {
|
||||
text = "Unstage All";
|
||||
action = git::UnstageAll.boxed_clone();
|
||||
tooltip = "git reset";
|
||||
@@ -2474,12 +2461,22 @@ impl GitPanel {
|
||||
tooltip = "git add --all ."
|
||||
}
|
||||
|
||||
let change_string = match self.entry_count {
|
||||
0 => "No Changes".to_string(),
|
||||
1 => "1 Change".to_string(),
|
||||
_ => format!("{} Changes", self.entry_count),
|
||||
};
|
||||
|
||||
self.panel_header_container(window, cx)
|
||||
.px_2()
|
||||
.child(
|
||||
panel_button("Open Diff")
|
||||
panel_button(change_string)
|
||||
.color(Color::Muted)
|
||||
.tooltip(Tooltip::for_action_title("Open diff", &Diff))
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Open diff",
|
||||
&Diff,
|
||||
&self.focus_handle,
|
||||
))
|
||||
.on_click(|_, _, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.dispatch_action(&Diff);
|
||||
@@ -2487,9 +2484,15 @@ impl GitPanel {
|
||||
}),
|
||||
)
|
||||
.child(div().flex_grow()) // spacer
|
||||
.child(self.render_overflow_menu("overflow_menu"))
|
||||
.child(
|
||||
panel_filled_button(text)
|
||||
.tooltip(Tooltip::for_action_title(tooltip, action.as_ref()))
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
tooltip,
|
||||
action.as_ref(),
|
||||
&self.focus_handle,
|
||||
))
|
||||
.disabled(self.entry_count == 0)
|
||||
.on_click(move |_, _, cx| {
|
||||
let action = action.boxed_clone();
|
||||
cx.defer(move |cx| {
|
||||
@@ -2712,12 +2715,12 @@ impl GitPanel {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
||||
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
||||
let scroll_bar_style = self.show_scrollbar(cx);
|
||||
let show_container = matches!(scroll_bar_style, ShowScrollbar::Always);
|
||||
|
||||
if !self.should_show_scrollbar(cx)
|
||||
|| !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
|
||||
|| !(self.show_scrollbar || self.scrollbar_state.is_dragging())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -2746,7 +2749,7 @@ impl GitPanel {
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
if !this.vertical_scrollbar_state.is_dragging()
|
||||
if !this.scrollbar_state.is_dragging()
|
||||
&& !this.focus_handle.contains_focused(window, cx)
|
||||
{
|
||||
this.hide_scrollbar(window, cx);
|
||||
@@ -2761,79 +2764,7 @@ impl GitPanel {
|
||||
}))
|
||||
.children(Scrollbar::vertical(
|
||||
// percentage as f32..end_offset as f32,
|
||||
self.vertical_scrollbar_state.clone(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_horizontal_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
||||
let scroll_bar_style = self.show_scrollbar(cx);
|
||||
let show_container = matches!(scroll_bar_style, ShowScrollbar::Always);
|
||||
|
||||
// if !self.should_show_scrollbar(cx)
|
||||
// || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
|
||||
// {
|
||||
// return None;
|
||||
// }
|
||||
|
||||
let scroll_handle = self.scroll_handle.0.borrow();
|
||||
dbg!(scroll_handle.last_item_size);
|
||||
// let longest_item_width = dbg!(scroll_handle.last_item_size)
|
||||
// .filter(|size| dbg!(px(10.) * size.contents.width > size.item.width))?
|
||||
// .contents
|
||||
// .width
|
||||
// .0 as f64;
|
||||
|
||||
// println!("Longest item width: {}", longest_item_width);
|
||||
// if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
|
||||
// return None;
|
||||
// }
|
||||
|
||||
Some(
|
||||
div()
|
||||
.id("git-panel-horizontal-scroll")
|
||||
.occlude()
|
||||
.flex_none()
|
||||
.w_full()
|
||||
.cursor_default()
|
||||
.absolute()
|
||||
.bottom_1()
|
||||
.left_1()
|
||||
.right_1()
|
||||
.h(px(32.))
|
||||
// .when(show_container, |this| this.pt_1().pb_1p5())
|
||||
// .when(!show_container, |this| {
|
||||
// this.bottom_1().left_1().right_1().h(px(32.))
|
||||
// })
|
||||
.on_mouse_move(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
if !this.horizontal_scrollbar_state.is_dragging()
|
||||
&& !this.focus_handle.contains_focused(window, cx)
|
||||
{
|
||||
this.hide_scrollbar(window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.children(Scrollbar::horizontal(
|
||||
// percentage as f32..end_offset as f32,
|
||||
self.horizontal_scrollbar_state.clone(),
|
||||
self.scrollbar_state.clone(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
@@ -2890,10 +2821,8 @@ impl GitPanel {
|
||||
let entry_count = self.entries.len();
|
||||
|
||||
h_flex()
|
||||
// .debug_below()
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.relative()
|
||||
.flex_grow()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
uniform_list(cx.entity().clone(), "entries", entry_count, {
|
||||
@@ -2928,10 +2857,8 @@ impl GitPanel {
|
||||
}
|
||||
})
|
||||
.size_full()
|
||||
.flex_grow()
|
||||
.with_sizing_behavior(ListSizingBehavior::Auto)
|
||||
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
|
||||
.with_width_from_item(self.max_width_item_index)
|
||||
.track_scroll(self.scroll_handle.clone()),
|
||||
)
|
||||
.on_mouse_down(
|
||||
@@ -2940,8 +2867,7 @@ impl GitPanel {
|
||||
this.deploy_panel_context_menu(event.position, window, cx)
|
||||
}),
|
||||
)
|
||||
// .children(self.render_vertical_scrollbar(cx))
|
||||
.children(self.render_horizontal_scrollbar(cx))
|
||||
.children(self.render_scrollbar(cx))
|
||||
}
|
||||
|
||||
fn entry_label(&self, label: impl Into<SharedString>, color: Color) -> Label {
|
||||
@@ -3015,6 +2941,7 @@ impl GitPanel {
|
||||
};
|
||||
let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| {
|
||||
context_menu
|
||||
.context(self.focus_handle.clone())
|
||||
.action(stage_title, ToggleStaged.boxed_clone())
|
||||
.action(restore_title, git::RestoreFile.boxed_clone())
|
||||
.separator()
|
||||
@@ -3031,7 +2958,7 @@ impl GitPanel {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let context_menu = git_panel_context_menu(window, cx);
|
||||
let context_menu = git_panel_context_menu(self.focus_handle.clone(), window, cx);
|
||||
self.set_context_menu(context_menu, position, window, cx);
|
||||
}
|
||||
|
||||
@@ -3067,7 +2994,11 @@ impl GitPanel {
|
||||
window: &Window,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let display_name = entry.display_name();
|
||||
let display_name = entry
|
||||
.worktree_path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| entry.worktree_path.to_string_lossy().into_owned());
|
||||
|
||||
let worktree_path = entry.worktree_path.clone();
|
||||
let selected = self.selected_entry == Some(ix);
|
||||
@@ -3160,7 +3091,8 @@ impl GitPanel {
|
||||
el.border_color(cx.theme().colors().border_focused)
|
||||
})
|
||||
.px(rems(0.75)) // ~12px
|
||||
// .flex_none()
|
||||
.overflow_hidden()
|
||||
.flex_none()
|
||||
.gap_1p5()
|
||||
.bg(base_bg)
|
||||
.hover(|this| this.bg(hover_bg))
|
||||
@@ -3260,12 +3192,12 @@ impl GitPanel {
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.flex_1()
|
||||
// .overflow_hidden()
|
||||
.when_some(entry.parent_dir(), |this, parent| {
|
||||
if !parent.is_empty() {
|
||||
.overflow_hidden()
|
||||
.when_some(worktree_path.parent(), |this, parent| {
|
||||
let parent_str = parent.to_string_lossy();
|
||||
if !parent_str.is_empty() {
|
||||
this.child(
|
||||
self.entry_label(format!("{}/", parent), path_color)
|
||||
self.entry_label(format!("{}/", parent_str), path_color)
|
||||
.when(status.is_deleted(), |this| this.strikethrough()),
|
||||
)
|
||||
} else {
|
||||
@@ -3315,10 +3247,16 @@ impl Render for GitPanel {
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
|
||||
.when(has_write_access && !project.is_read_only(cx), |this| {
|
||||
this.on_action(cx.listener(|this, &ToggleStaged, window, cx| {
|
||||
this.toggle_staged_for_selected(&ToggleStaged, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(GitPanel::commit))
|
||||
this.on_action(cx.listener(Self::toggle_staged_for_selected))
|
||||
.on_action(cx.listener(GitPanel::commit))
|
||||
.on_action(cx.listener(Self::stage_all))
|
||||
.on_action(cx.listener(Self::unstage_all))
|
||||
.on_action(cx.listener(Self::stage_selected))
|
||||
.on_action(cx.listener(Self::unstage_selected))
|
||||
.on_action(cx.listener(Self::restore_tracked_files))
|
||||
.on_action(cx.listener(Self::revert_selected))
|
||||
.on_action(cx.listener(Self::clean_all))
|
||||
.on_action(cx.listener(Self::generate_commit_message_action))
|
||||
})
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
@@ -3327,16 +3265,9 @@ impl Render for GitPanel {
|
||||
.on_action(cx.listener(Self::close_panel))
|
||||
.on_action(cx.listener(Self::open_diff))
|
||||
.on_action(cx.listener(Self::open_file))
|
||||
.on_action(cx.listener(Self::revert_selected))
|
||||
.on_action(cx.listener(Self::focus_changes_list))
|
||||
.on_action(cx.listener(Self::focus_editor))
|
||||
.on_action(cx.listener(Self::toggle_staged_for_selected))
|
||||
.on_action(cx.listener(Self::stage_all))
|
||||
.on_action(cx.listener(Self::unstage_all))
|
||||
.on_action(cx.listener(Self::restore_tracked_files))
|
||||
.on_action(cx.listener(Self::clean_all))
|
||||
.on_action(cx.listener(Self::expand_commit_editor))
|
||||
.on_action(cx.listener(Self::generate_commit_message_action))
|
||||
.when(has_write_access && has_co_authors, |git_panel| {
|
||||
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
|
||||
})
|
||||
@@ -3559,23 +3490,11 @@ impl PanelRepoFooter {
|
||||
git_panel: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
|
||||
PopoverMenu::new(id.into())
|
||||
.trigger(
|
||||
IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
)
|
||||
.menu(move |window, cx| Some(git_panel_context_menu(window, cx)))
|
||||
.anchor(Corner::TopRight)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for PanelRepoFooter {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let active_repo = self.active_repository.clone();
|
||||
let overflow_menu_id: SharedString = format!("overflow-menu-{}", active_repo).into();
|
||||
let repo_selector_trigger = Button::new("repo-selector", active_repo)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.size(ButtonSize::None)
|
||||
@@ -3655,6 +3574,7 @@ impl RenderOnce for PanelRepoFooter {
|
||||
.h(px(36.))
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
@@ -3687,7 +3607,6 @@ impl RenderOnce for PanelRepoFooter {
|
||||
.gap_1()
|
||||
.flex_shrink_0()
|
||||
.children(spinner)
|
||||
.child(self.render_overflow_menu(overflow_menu_id))
|
||||
.when_some(branch, |this, branch| {
|
||||
let mut focus_handle = None;
|
||||
if let Some(git_panel) = self.git_panel.as_ref() {
|
||||
|
||||
@@ -29,41 +29,43 @@ pub fn init(cx: &mut App) {
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _, cx| {
|
||||
let project = workspace.project().read(cx);
|
||||
if project.is_via_collab() {
|
||||
if project.is_read_only(cx) {
|
||||
return;
|
||||
}
|
||||
workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.fetch(window, cx);
|
||||
if !project.is_via_collab() {
|
||||
workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.fetch(window, cx);
|
||||
});
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, _: &git::Push, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.push(false, window, cx);
|
||||
workspace.register_action(|workspace, _: &git::Push, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.push(false, window, cx);
|
||||
});
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.push(true, window, cx);
|
||||
workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.push(true, window, cx);
|
||||
});
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, _: &git::Pull, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.pull(window, cx);
|
||||
workspace.register_action(|workspace, _: &git::Pull, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.pull(window, cx);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
workspace.register_action(|workspace, action: &git::StageAll, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
@@ -151,6 +153,7 @@ mod remote_button {
|
||||
0,
|
||||
0,
|
||||
Some(IconName::ArrowCircle),
|
||||
keybinding_target.clone(),
|
||||
move |_, window, cx| {
|
||||
window.dispatch_action(Box::new(git::Fetch), cx);
|
||||
},
|
||||
@@ -178,6 +181,7 @@ mod remote_button {
|
||||
ahead as usize,
|
||||
0,
|
||||
None,
|
||||
keybinding_target.clone(),
|
||||
move |_, window, cx| {
|
||||
window.dispatch_action(Box::new(git::Push), cx);
|
||||
},
|
||||
@@ -206,6 +210,7 @@ mod remote_button {
|
||||
ahead as usize,
|
||||
behind as usize,
|
||||
None,
|
||||
keybinding_target.clone(),
|
||||
move |_, window, cx| {
|
||||
window.dispatch_action(Box::new(git::Pull), cx);
|
||||
},
|
||||
@@ -232,6 +237,7 @@ mod remote_button {
|
||||
0,
|
||||
0,
|
||||
Some(IconName::ArrowUpFromLine),
|
||||
keybinding_target.clone(),
|
||||
move |_, window, cx| {
|
||||
window.dispatch_action(Box::new(git::Push), cx);
|
||||
},
|
||||
@@ -258,6 +264,7 @@ mod remote_button {
|
||||
0,
|
||||
0,
|
||||
Some(IconName::ArrowUpFromLine),
|
||||
keybinding_target.clone(),
|
||||
move |_, window, cx| {
|
||||
window.dispatch_action(Box::new(git::Push), cx);
|
||||
},
|
||||
@@ -299,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())
|
||||
.trigger(
|
||||
ui::ButtonLike::new_rounded_right("split-button-right")
|
||||
@@ -314,6 +324,9 @@ mod remote_button {
|
||||
.menu(move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |context_menu, _, _| {
|
||||
context_menu
|
||||
.when_some(keybinding_target.clone(), |el, keybinding_target| {
|
||||
el.context(keybinding_target.clone())
|
||||
})
|
||||
.action("Fetch", git::Fetch.boxed_clone())
|
||||
.action("Pull", git::Pull.boxed_clone())
|
||||
.separator()
|
||||
@@ -331,12 +344,14 @@ mod remote_button {
|
||||
}
|
||||
|
||||
impl SplitButton {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
id: impl Into<SharedString>,
|
||||
left_label: impl Into<SharedString>,
|
||||
ahead_count: usize,
|
||||
behind_count: usize,
|
||||
left_icon: Option<IconName>,
|
||||
keybinding_target: Option<FocusHandle>,
|
||||
left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
) -> Self {
|
||||
@@ -394,9 +409,10 @@ mod remote_button {
|
||||
.on_click(left_on_click)
|
||||
.tooltip(tooltip);
|
||||
|
||||
let right = render_git_action_menu(ElementId::Name(
|
||||
format!("split-button-right-{}", id).into(),
|
||||
))
|
||||
let right = render_git_action_menu(
|
||||
ElementId::Name(format!("split-button-right-{}", id).into()),
|
||||
keybinding_target,
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
Self { left, right }
|
||||
|
||||
@@ -7,7 +7,6 @@ use editor::{
|
||||
scroll::Autoscroll,
|
||||
Editor, EditorEvent,
|
||||
};
|
||||
use feature_flags::FeatureFlagViewExt;
|
||||
use futures::StreamExt;
|
||||
use git::{
|
||||
repository::Branch, status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged,
|
||||
@@ -64,16 +63,13 @@ const NEW_NAMESPACE: &'static str = "2";
|
||||
|
||||
impl ProjectDiff {
|
||||
pub(crate) fn register(
|
||||
_: &mut Workspace,
|
||||
window: Option<&mut Window>,
|
||||
workspace: &mut Workspace,
|
||||
_window: Option<&mut Window>,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let Some(window) = window else { return };
|
||||
cx.when_flag_enabled::<feature_flags::GitUiFeatureFlag>(window, |workspace, _, _cx| {
|
||||
workspace.register_action(Self::deploy);
|
||||
workspace.register_action(|workspace, _: &Add, window, cx| {
|
||||
Self::deploy(workspace, &Diff, window, cx);
|
||||
});
|
||||
workspace.register_action(Self::deploy);
|
||||
workspace.register_action(|workspace, _: &Add, window, cx| {
|
||||
Self::deploy(workspace, &Diff, window, 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(
|
||||
project: Entity<Project>,
|
||||
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(())
|
||||
|
||||
@@ -152,7 +152,10 @@ impl GoToLine {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::EditorEvent::Blurred => cx.emit(DismissEvent),
|
||||
editor::EditorEvent::Blurred => {
|
||||
self.prev_scroll_position.take();
|
||||
cx.emit(DismissEvent)
|
||||
}
|
||||
editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ blade-macros.workspace = true
|
||||
flume = "0.11"
|
||||
rand.workspace = true
|
||||
windows.workspace = true
|
||||
windows-core = "0.58"
|
||||
windows-core = "0.60"
|
||||
|
||||
[dev-dependencies]
|
||||
backtrace = "0.3"
|
||||
|
||||
@@ -178,6 +178,7 @@ impl EntityMap {
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn double_lease_panic<T>(operation: &str) -> ! {
|
||||
panic!(
|
||||
"cannot {operation} {} while it is already being updated",
|
||||
|
||||
@@ -683,11 +683,11 @@ impl Default for 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 {
|
||||
tag: BackgroundTag::PatternSlash,
|
||||
solid: color,
|
||||
gradient_angle_or_pattern_height: thickness,
|
||||
gradient_angle_or_pattern_height: height,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +76,9 @@ impl Keystroke {
|
||||
}
|
||||
|
||||
/// 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,
|
||||
/// secondary means "cmd" on macOS and "ctrl" on other platforms
|
||||
/// when matching a key with an key_char set will be matched without it.
|
||||
pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
let mut control = false;
|
||||
@@ -95,6 +96,13 @@ impl Keystroke {
|
||||
"alt" => alt = true,
|
||||
"shift" => shift = true,
|
||||
"fn" => function = true,
|
||||
"secondary" => {
|
||||
if cfg!(target_os = "macos") {
|
||||
platform = true
|
||||
} else {
|
||||
control = true
|
||||
};
|
||||
}
|
||||
"cmd" | "super" | "win" => platform = true,
|
||||
_ => {
|
||||
if let Some(next) = components.peek() {
|
||||
|
||||
@@ -115,7 +115,6 @@ pub struct WaylandWindowStatePtr {
|
||||
}
|
||||
|
||||
impl WaylandWindowState {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
handle: AnyWindowHandle,
|
||||
surface: wl_surface::WlSurface,
|
||||
|
||||
@@ -353,7 +353,6 @@ where
|
||||
}
|
||||
|
||||
impl X11WindowState {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
handle: AnyWindowHandle,
|
||||
client: X11ClientStatePtr,
|
||||
@@ -712,7 +711,6 @@ enum WmHintPropertyState {
|
||||
}
|
||||
|
||||
impl X11Window {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
handle: AnyWindowHandle,
|
||||
client: X11ClientStatePtr,
|
||||
|
||||
@@ -347,20 +347,7 @@ impl MacPlatform {
|
||||
msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO];
|
||||
}
|
||||
item.setKeyEquivalentModifierMask_(mask);
|
||||
}
|
||||
// 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(']');
|
||||
|
||||
} else {
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(&name),
|
||||
|
||||
@@ -268,6 +268,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
|
||||
sel!(windowWillEnterFullScreen:),
|
||||
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(
|
||||
sel!(windowDidMove:),
|
||||
window_did_move as extern "C" fn(&Object, Sel, id),
|
||||
@@ -334,6 +338,7 @@ struct MacWindowState {
|
||||
last_key_equivalent: Option<KeyDownEvent>,
|
||||
synthetic_drag_counter: usize,
|
||||
traffic_light_position: Option<Point<Pixels>>,
|
||||
transparent_titlebar: bool,
|
||||
previous_modifiers_changed_event: Option<PlatformInput>,
|
||||
keystroke_for_do_command: Option<Keystroke>,
|
||||
do_command_handled: Option<bool>,
|
||||
@@ -613,6 +618,9 @@ impl MacWindow {
|
||||
traffic_light_position: titlebar
|
||||
.as_ref()
|
||||
.and_then(|titlebar| titlebar.traffic_light_position),
|
||||
transparent_titlebar: titlebar
|
||||
.as_ref()
|
||||
.map_or(true, |titlebar| titlebar.appears_transparent),
|
||||
previous_modifiers_changed_event: None,
|
||||
keystroke_for_do_command: 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 mut lock = window_state.as_ref().lock();
|
||||
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) {
|
||||
|
||||
@@ -170,7 +170,7 @@ fn set_data_to_clipboard<T>(data: &[T], format: u32) -> Result<()> {
|
||||
let handle = GlobalLock(global);
|
||||
std::ptr::copy_nonoverlapping(data.as_ptr(), handle as _, data.len());
|
||||
let _ = GlobalUnlock(global);
|
||||
SetClipboardData(format, HANDLE(global.0))?;
|
||||
SetClipboardData(format, Some(HANDLE(global.0)))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1049,7 +1049,7 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
|
||||
_measuringmode: DWRITE_MEASURING_MODE,
|
||||
glyphrun: *const DWRITE_GLYPH_RUN,
|
||||
glyphrundescription: *const DWRITE_GLYPH_RUN_DESCRIPTION,
|
||||
_clientdrawingeffect: Option<&windows::core::IUnknown>,
|
||||
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
|
||||
) -> windows::core::Result<()> {
|
||||
unsafe {
|
||||
let glyphrun = &*glyphrun;
|
||||
@@ -1113,7 +1113,7 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
|
||||
_baselineoriginx: f32,
|
||||
_baselineoriginy: f32,
|
||||
_underline: *const DWRITE_UNDERLINE,
|
||||
_clientdrawingeffect: Option<&windows::core::IUnknown>,
|
||||
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
|
||||
) -> windows::core::Result<()> {
|
||||
Err(windows::core::Error::new(
|
||||
E_NOTIMPL,
|
||||
@@ -1127,7 +1127,7 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
|
||||
_baselineoriginx: f32,
|
||||
_baselineoriginy: f32,
|
||||
_strikethrough: *const DWRITE_STRIKETHROUGH,
|
||||
_clientdrawingeffect: Option<&windows::core::IUnknown>,
|
||||
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
|
||||
) -> windows::core::Result<()> {
|
||||
Err(windows::core::Error::new(
|
||||
E_NOTIMPL,
|
||||
@@ -1140,10 +1140,10 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
|
||||
_clientdrawingcontext: *const ::core::ffi::c_void,
|
||||
_originx: f32,
|
||||
_originy: f32,
|
||||
_inlineobject: Option<&IDWriteInlineObject>,
|
||||
_inlineobject: windows::core::Ref<IDWriteInlineObject>,
|
||||
_issideways: BOOL,
|
||||
_isrighttoleft: BOOL,
|
||||
_clientdrawingeffect: Option<&windows::core::IUnknown>,
|
||||
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
|
||||
) -> windows::core::Result<()> {
|
||||
Err(windows::core::Error::new(
|
||||
E_NOTIMPL,
|
||||
|
||||
@@ -215,7 +215,7 @@ fn available_monitors() -> SmallVec<[HMONITOR; 4]> {
|
||||
let mut monitors: SmallVec<[HMONITOR; 4]> = SmallVec::new();
|
||||
unsafe {
|
||||
EnumDisplayMonitors(
|
||||
HDC::default(),
|
||||
None,
|
||||
None,
|
||||
Some(monitor_enum_proc),
|
||||
LPARAM(&mut monitors as *mut _ as _),
|
||||
|
||||
@@ -177,7 +177,12 @@ fn handle_size_msg(
|
||||
|
||||
fn handle_size_move_loop(handle: HWND) -> Option<isize> {
|
||||
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 {
|
||||
log::error!(
|
||||
"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> {
|
||||
unsafe {
|
||||
KillTimer(handle, SIZE_MOVE_LOOP_TIMER_ID).log_err();
|
||||
KillTimer(Some(handle), SIZE_MOVE_LOOP_TIMER_ID).log_err();
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -217,7 +222,7 @@ fn handle_paint_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Optio
|
||||
request_frame(Default::default());
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -776,7 +781,7 @@ fn handle_activate_msg(
|
||||
if state_ptr.hide_title_bar {
|
||||
if let Some(titlebar_rect) = state_ptr.state.borrow().get_titlebar_rect().log_err() {
|
||||
unsafe {
|
||||
InvalidateRect(handle, Some(&titlebar_rect), FALSE)
|
||||
InvalidateRect(Some(handle), Some(&titlebar_rect), false)
|
||||
.ok()
|
||||
.log_err()
|
||||
};
|
||||
@@ -1105,7 +1110,7 @@ fn handle_nc_mouse_up_msg(
|
||||
HTCLOSE => {
|
||||
if last_button == HTCLOSE {
|
||||
unsafe {
|
||||
PostMessageW(handle, WM_CLOSE, WPARAM::default(), LPARAM::default())
|
||||
PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default())
|
||||
.log_err()
|
||||
};
|
||||
handled = true;
|
||||
@@ -1133,7 +1138,7 @@ fn handle_set_cursor(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Op
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
unsafe { SetCursor(state_ptr.state.borrow().current_cursor) };
|
||||
unsafe { SetCursor(Some(state_ptr.state.borrow().current_cursor)) };
|
||||
Some(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -130,14 +130,9 @@ impl WindowsPlatform {
|
||||
fn redraw_all(&self) {
|
||||
for handle in self.raw_window_handles.read().iter() {
|
||||
unsafe {
|
||||
RedrawWindow(
|
||||
*handle,
|
||||
None,
|
||||
HRGN::default(),
|
||||
RDW_INVALIDATE | RDW_UPDATENOW,
|
||||
)
|
||||
.ok()
|
||||
.log_err();
|
||||
RedrawWindow(Some(*handle), None, None, RDW_INVALIDATE | RDW_UPDATENOW)
|
||||
.ok()
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,7 +151,7 @@ impl WindowsPlatform {
|
||||
.read()
|
||||
.iter()
|
||||
.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(
|
||||
PCWSTR::from_raw(target_name.as_ptr()),
|
||||
CRED_TYPE_GENERIC,
|
||||
0,
|
||||
None,
|
||||
&mut credentials,
|
||||
)?
|
||||
};
|
||||
@@ -648,7 +643,13 @@ impl Platform for WindowsPlatform {
|
||||
.chain(Some(0))
|
||||
.collect_vec();
|
||||
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(())
|
||||
})
|
||||
}
|
||||
@@ -805,7 +806,7 @@ fn load_icon() -> Result<HICON> {
|
||||
let module = unsafe { GetModuleHandleW(None).context("unable to get module handle")? };
|
||||
let handle = unsafe {
|
||||
LoadImageW(
|
||||
module,
|
||||
Some(module.into()),
|
||||
windows::core::PCWSTR(1 as _),
|
||||
IMAGE_ICON,
|
||||
0,
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::sync::OnceLock;
|
||||
|
||||
use ::util::ResultExt;
|
||||
use windows::{
|
||||
core::BOOL,
|
||||
Wdk::System::SystemServices::RtlGetVersion,
|
||||
Win32::{Foundation::*, Graphics::Dwm::*, UI::WindowsAndMessaging::*},
|
||||
UI::{
|
||||
|
||||
@@ -296,7 +296,7 @@ impl WindowsWindowStatePtr {
|
||||
unsafe {
|
||||
SetWindowPos(
|
||||
state_ptr.hwnd,
|
||||
HWND::default(),
|
||||
None,
|
||||
x,
|
||||
y,
|
||||
cx,
|
||||
@@ -433,7 +433,7 @@ impl WindowsWindow {
|
||||
CW_USEDEFAULT,
|
||||
None,
|
||||
None,
|
||||
hinstance,
|
||||
Some(hinstance.into()),
|
||||
lpparam,
|
||||
)
|
||||
};
|
||||
@@ -650,7 +650,7 @@ impl PlatformWindow for WindowsWindow {
|
||||
.spawn(async move {
|
||||
this.set_window_placement().log_err();
|
||||
unsafe { SetActiveWindow(hwnd).log_err() };
|
||||
unsafe { SetFocus(hwnd).log_err() };
|
||||
unsafe { SetFocus(Some(hwnd)).log_err() };
|
||||
// todo(windows)
|
||||
// crate `windows 0.56` reports true as Err
|
||||
unsafe { SetForegroundWindow(hwnd).as_bool() };
|
||||
@@ -817,16 +817,13 @@ impl WindowsDragDropHandler {
|
||||
impl IDropTarget_Impl for WindowsDragDropHandler_Impl {
|
||||
fn DragEnter(
|
||||
&self,
|
||||
pdataobj: Option<&IDataObject>,
|
||||
pdataobj: windows::core::Ref<IDataObject>,
|
||||
_grfkeystate: MODIFIERKEYS_FLAGS,
|
||||
pt: &POINTL,
|
||||
pdweffect: *mut DROPEFFECT,
|
||||
) -> windows::core::Result<()> {
|
||||
unsafe {
|
||||
let Some(idata_obj) = pdataobj else {
|
||||
log::info!("no dragging file or directory detected");
|
||||
return Ok(());
|
||||
};
|
||||
let idata_obj = pdataobj.ok()?;
|
||||
let config = FORMATETC {
|
||||
cfFormat: CF_HDROP.0,
|
||||
ptd: std::ptr::null_mut() as _,
|
||||
@@ -905,7 +902,7 @@ impl IDropTarget_Impl for WindowsDragDropHandler_Impl {
|
||||
|
||||
fn Drop(
|
||||
&self,
|
||||
_pdataobj: Option<&IDataObject>,
|
||||
_pdataobj: windows::core::Ref<IDataObject>,
|
||||
_grfkeystate: MODIFIERKEYS_FLAGS,
|
||||
pt: &POINTL,
|
||||
_pdweffect: *mut DROPEFFECT,
|
||||
|
||||
@@ -81,6 +81,29 @@ impl ShapedLine {
|
||||
|
||||
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.
|
||||
@@ -132,7 +155,6 @@ impl WrappedLine {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn paint_line(
|
||||
origin: Point<Pixels>,
|
||||
layout: &LineLayout,
|
||||
@@ -160,7 +182,6 @@ fn paint_line(
|
||||
let mut color = black();
|
||||
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = 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 mut glyph_origin = point(
|
||||
aligned_origin_x(
|
||||
@@ -183,21 +204,6 @@ fn paint_line(
|
||||
|
||||
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;
|
||||
}
|
||||
if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
|
||||
if glyph_origin.x == underline_origin.x {
|
||||
underline_origin.x -= max_glyph_size.width.half();
|
||||
@@ -237,7 +243,6 @@ fn paint_line(
|
||||
}
|
||||
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_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
|
||||
if glyph.index >= run_end {
|
||||
@@ -253,18 +258,6 @@ fn paint_line(
|
||||
}
|
||||
|
||||
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 style_run.underline.as_ref() != Some(underline_style) {
|
||||
finished_underline = current_underline.take();
|
||||
@@ -306,26 +299,11 @@ fn paint_line(
|
||||
color = style_run.color;
|
||||
} else {
|
||||
run_end = layout.len;
|
||||
finished_background = current_background.take();
|
||||
finished_underline = current_underline.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 underline_origin.x == glyph_origin.x {
|
||||
underline_origin.x -= max_glyph_size.width.half();
|
||||
@@ -384,19 +362,6 @@ fn paint_line(
|
||||
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 last_line_end_x == underline_start.x {
|
||||
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(
|
||||
origin: Point<Pixels>,
|
||||
align_width: Pixels,
|
||||
|
||||
@@ -1250,7 +1250,6 @@ fn parse_text(
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn get_injections(
|
||||
config: &InjectionConfig,
|
||||
text: &BufferSnapshot,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name = "C++"
|
||||
grammar = "cpp"
|
||||
path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "cu", "cuh", "C", "H"]
|
||||
path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "ixx", "cu", "cuh", "C", "H"]
|
||||
line_comments = ["// ", "/// ", "//! "]
|
||||
autoclose_before = ";:.,=}])>"
|
||||
brackets = [
|
||||
|
||||
@@ -301,7 +301,6 @@ pub struct AdapterServerCapabilities {
|
||||
|
||||
impl LanguageServer {
|
||||
/// Starts a language server process.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
stderr_capture: Arc<Mutex<Option<String>>>,
|
||||
server_id: LanguageServerId,
|
||||
@@ -372,7 +371,6 @@ impl LanguageServer {
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new_internal<Stdin, Stdout, Stderr, F>(
|
||||
server_id: LanguageServerId,
|
||||
server_name: LanguageServerName,
|
||||
|
||||
@@ -2982,7 +2982,6 @@ impl MultiBuffer {
|
||||
snapshot.check_invariants();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn recompute_diff_transforms_for_edit(
|
||||
&self,
|
||||
edit: &Edit<TypedOffset<Excerpt>>,
|
||||
@@ -4176,6 +4175,9 @@ impl MultiBufferSnapshot {
|
||||
let region = cursor.region()?;
|
||||
let overshoot = offset - region.range.start;
|
||||
let buffer_offset = region.buffer_range.start + overshoot;
|
||||
if buffer_offset > region.buffer.len() {
|
||||
return None;
|
||||
}
|
||||
Some((region.buffer, buffer_offset))
|
||||
}
|
||||
|
||||
@@ -4184,8 +4186,11 @@ impl MultiBufferSnapshot {
|
||||
cursor.seek(&point);
|
||||
let region = cursor.region()?;
|
||||
let overshoot = point - region.range.start;
|
||||
let buffer_offset = region.buffer_range.start + overshoot;
|
||||
Some((region.buffer, buffer_offset, region.is_main_buffer))
|
||||
let buffer_point = region.buffer_range.start + overshoot;
|
||||
if buffer_point > region.buffer.max_point() {
|
||||
return None;
|
||||
}
|
||||
Some((region.buffer, buffer_point, region.is_main_buffer))
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -2360,7 +2360,6 @@ impl OutlinePanel {
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_search_match(
|
||||
&mut self,
|
||||
multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
|
||||
@@ -2452,7 +2451,6 @@ impl OutlinePanel {
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn entry_element(
|
||||
&self,
|
||||
rendered_entry: PanelEntry,
|
||||
@@ -3836,7 +3834,6 @@ impl OutlinePanel {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn push_entry(
|
||||
&self,
|
||||
state: &mut GenerationState,
|
||||
@@ -4054,7 +4051,6 @@ impl OutlinePanel {
|
||||
update_cached_entries
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn add_excerpt_entries(
|
||||
&self,
|
||||
state: &mut GenerationState,
|
||||
@@ -4113,7 +4109,6 @@ impl OutlinePanel {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn add_search_entries(
|
||||
&mut self,
|
||||
state: &mut GenerationState,
|
||||
|
||||
@@ -96,8 +96,8 @@ pub trait PickerDelegate: Sized + 'static {
|
||||
None
|
||||
}
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str>;
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
|
||||
"No matches".into()
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
Some("No matches".into())
|
||||
}
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
@@ -844,18 +844,17 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||
)
|
||||
})
|
||||
.when(self.delegate.match_count() == 0, |el| {
|
||||
el.child(
|
||||
v_flex().flex_grow().py_2().child(
|
||||
ListItem::new("empty_state")
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.disabled(true)
|
||||
.child(
|
||||
Label::new(self.delegate.no_matches_text(window, cx))
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
)
|
||||
el.when_some(self.delegate.no_matches_text(window, cx), |el, text| {
|
||||
el.child(
|
||||
v_flex().flex_grow().py_2().child(
|
||||
ListItem::new("empty_state")
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.disabled(true)
|
||||
.child(Label::new(text).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
.children(self.delegate.render_footer(window, cx))
|
||||
.children(match &self.head {
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
};
|
||||
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use buffer_diff::{BufferDiff, BufferDiffEvent};
|
||||
use buffer_diff::BufferDiff;
|
||||
use client::Client;
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use fs::Fs;
|
||||
@@ -15,7 +15,6 @@ use git::{blame::Blame, repository::RepoPath};
|
||||
use gpui::{
|
||||
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use http_client::Url;
|
||||
use language::{
|
||||
proto::{
|
||||
deserialize_line_ending, deserialize_version, serialize_line_ending, serialize_version,
|
||||
@@ -34,7 +33,6 @@ use std::{
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
pin::pin,
|
||||
str::FromStr as _,
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
};
|
||||
@@ -217,39 +215,29 @@ impl BufferDiffState {
|
||||
_ => false,
|
||||
};
|
||||
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 {
|
||||
unstaged_changed_range = BufferDiff::update_diff(
|
||||
unstaged_diff.clone(),
|
||||
buffer.clone(),
|
||||
index,
|
||||
index_changed,
|
||||
language_changed,
|
||||
language.clone(),
|
||||
language_registry.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.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),
|
||||
})
|
||||
}
|
||||
})?;
|
||||
new_unstaged_diff = Some(
|
||||
BufferDiff::update_diff(
|
||||
unstaged_diff.clone(),
|
||||
buffer.clone(),
|
||||
index,
|
||||
index_changed,
|
||||
language_changed,
|
||||
language.clone(),
|
||||
language_registry.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
let mut new_uncommitted_diff = None;
|
||||
if let Some(uncommitted_diff) = &uncommitted_diff {
|
||||
let uncommitted_changed_range =
|
||||
if let (Some(unstaged_diff), true) = (&unstaged_diff, index_matches_head) {
|
||||
uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| {
|
||||
uncommitted_diff.update_diff_from(&buffer, unstaged_diff, cx)
|
||||
})?
|
||||
} else {
|
||||
new_uncommitted_diff = if index_matches_head {
|
||||
new_unstaged_diff.clone()
|
||||
} else {
|
||||
Some(
|
||||
BufferDiff::update_diff(
|
||||
uncommitted_diff.clone(),
|
||||
buffer.clone(),
|
||||
@@ -260,32 +248,32 @@ impl BufferDiffState {
|
||||
language_registry.clone(),
|
||||
&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| {
|
||||
if language_changed {
|
||||
cx.emit(BufferDiffEvent::LanguageChanged);
|
||||
}
|
||||
let changed_range = match (unstaged_changed_range, uncommitted_changed_range) {
|
||||
(None, None) => None,
|
||||
(Some(unstaged_range), None) => {
|
||||
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 });
|
||||
uncommitted_diff.set_snapshot(
|
||||
&buffer,
|
||||
new_uncommitted_diff,
|
||||
language_changed,
|
||||
unstaged_changed_range,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -813,8 +801,7 @@ impl LocalBufferStore {
|
||||
let Some(buffer) = buffer.upgrade() else {
|
||||
continue;
|
||||
};
|
||||
let buffer = buffer.read(cx);
|
||||
let Some(file) = File::from_dyn(buffer.file()) else {
|
||||
let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
|
||||
continue;
|
||||
};
|
||||
if file.worktree != worktree_handle {
|
||||
@@ -825,7 +812,6 @@ impl LocalBufferStore {
|
||||
.iter()
|
||||
.any(|(work_dir, _)| file.path.starts_with(work_dir))
|
||||
{
|
||||
let snapshot = buffer.text_snapshot();
|
||||
let has_unstaged_diff = diff_state
|
||||
.unstaged_diff
|
||||
.as_ref()
|
||||
@@ -835,7 +821,7 @@ impl LocalBufferStore {
|
||||
.as_ref()
|
||||
.is_some_and(|set| set.is_upgradable());
|
||||
diff_state_updates.push((
|
||||
snapshot.clone(),
|
||||
buffer,
|
||||
file.path.clone(),
|
||||
has_unstaged_diff.then(|| diff_state.index_text.clone()),
|
||||
has_uncommitted_diff.then(|| diff_state.head_text.clone()),
|
||||
@@ -854,36 +840,33 @@ impl LocalBufferStore {
|
||||
.background_spawn(async move {
|
||||
diff_state_updates
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|(buffer_snapshot, path, current_index_text, current_head_text)| {
|
||||
let local_repo = snapshot.local_repo_for_path(&path)?;
|
||||
let relative_path = local_repo.relativize(&path).ok()?;
|
||||
let index_text = if current_index_text.is_some() {
|
||||
local_repo.repo().load_index_text(&relative_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let head_text = if current_head_text.is_some() {
|
||||
local_repo.repo().load_committed_text(&relative_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
.filter_map(|(buffer, path, current_index_text, current_head_text)| {
|
||||
let local_repo = snapshot.local_repo_for_path(&path)?;
|
||||
let relative_path = local_repo.relativize(&path).ok()?;
|
||||
let index_text = if current_index_text.is_some() {
|
||||
local_repo.repo().load_index_text(&relative_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let head_text = if current_head_text.is_some() {
|
||||
local_repo.repo().load_committed_text(&relative_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Avoid triggering a diff update if the base text has not changed.
|
||||
if let Some((current_index, current_head)) =
|
||||
current_index_text.as_ref().zip(current_head_text.as_ref())
|
||||
// Avoid triggering a diff update if the base text has not changed.
|
||||
if let Some((current_index, current_head)) =
|
||||
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()
|
||||
&& current_head.as_deref() == head_text.as_ref()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let diff_bases_change = match (
|
||||
current_index_text.is_some(),
|
||||
current_head_text.is_some(),
|
||||
) {
|
||||
let diff_bases_change =
|
||||
match (current_index_text.is_some(), current_head_text.is_some()) {
|
||||
(true, true) => Some(if index_text == head_text {
|
||||
DiffBasesChange::SetBoth(head_text)
|
||||
} else {
|
||||
@@ -896,17 +879,17 @@ impl LocalBufferStore {
|
||||
(false, true) => Some(DiffBasesChange::SetHead(head_text)),
|
||||
(false, false) => None,
|
||||
};
|
||||
Some((buffer_snapshot, diff_bases_change))
|
||||
},
|
||||
)
|
||||
|
||||
Some((buffer, diff_bases_change))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.await;
|
||||
|
||||
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, .. }) =
|
||||
this.opened_buffers.get_mut(&buffer_snapshot.remote_id())
|
||||
this.opened_buffers.get_mut(&buffer.read(cx).remote_id())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
@@ -917,8 +900,9 @@ impl LocalBufferStore {
|
||||
diff_state.update(cx, |diff_state, cx| {
|
||||
use proto::update_diff_bases::Mode;
|
||||
|
||||
let buffer = buffer.read(cx);
|
||||
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
|
||||
.clone()
|
||||
{
|
||||
@@ -942,8 +926,11 @@ impl LocalBufferStore {
|
||||
client.send(message).log_err();
|
||||
}
|
||||
|
||||
let _ =
|
||||
diff_state.diff_bases_changed(buffer_snapshot, diff_bases_change, cx);
|
||||
let _ = diff_state.diff_bases_changed(
|
||||
buffer.text_snapshot(),
|
||||
diff_bases_change,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -1705,11 +1692,17 @@ impl BufferStore {
|
||||
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 {
|
||||
const REMOTE_NAME: &str = "origin";
|
||||
let origin_url = repo
|
||||
.remote_url(REMOTE_NAME)
|
||||
.ok_or_else(|| anyhow!("remote \"{REMOTE_NAME}\" not found"))?;
|
||||
.remote_url(&remote)
|
||||
.ok_or_else(|| anyhow!("remote \"{remote}\" not found"))?;
|
||||
|
||||
let sha = repo
|
||||
.head_sha()
|
||||
@@ -2787,20 +2780,10 @@ fn serialize_blame_buffer_response(blame: Option<git::blame::Blame>) -> proto::B
|
||||
})
|
||||
.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 {
|
||||
blame_response: Some(proto::blame_buffer_response::BlameResponse {
|
||||
entries,
|
||||
messages,
|
||||
permalinks,
|
||||
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)))
|
||||
.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 {
|
||||
entries,
|
||||
permalinks,
|
||||
messages,
|
||||
remote_url: response.remote_url,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
buffer_store::{BufferStore, BufferStoreEvent},
|
||||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
Project, ProjectItem, ProjectPath,
|
||||
Project, ProjectEnvironment, ProjectItem, ProjectPath,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use askpass::{AskPassDelegate, AskPassSession};
|
||||
@@ -10,6 +10,7 @@ use client::ProjectId;
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
future::OptionFuture,
|
||||
StreamExt as _,
|
||||
};
|
||||
use git::repository::DiffType;
|
||||
@@ -43,6 +44,7 @@ use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory};
|
||||
|
||||
pub struct GitStore {
|
||||
buffer_store: Entity<BufferStore>,
|
||||
environment: Option<Entity<ProjectEnvironment>>,
|
||||
pub(super) project_id: Option<ProjectId>,
|
||||
pub(super) client: AnyProtoClient,
|
||||
repositories: Vec<Entity<Repository>>,
|
||||
@@ -54,6 +56,7 @@ pub struct GitStore {
|
||||
pub struct Repository {
|
||||
commit_message_buffer: Option<Entity<Buffer>>,
|
||||
git_store: WeakEntity<GitStore>,
|
||||
project_environment: Option<WeakEntity<ProjectEnvironment>>,
|
||||
pub worktree_id: WorktreeId,
|
||||
pub repository_entry: RepositoryEntry,
|
||||
pub dot_git_abs_path: PathBuf,
|
||||
@@ -101,6 +104,7 @@ impl GitStore {
|
||||
pub fn new(
|
||||
worktree_store: &Entity<WorktreeStore>,
|
||||
buffer_store: Entity<BufferStore>,
|
||||
environment: Option<Entity<ProjectEnvironment>>,
|
||||
client: AnyProtoClient,
|
||||
project_id: Option<ProjectId>,
|
||||
cx: &mut Context<'_, Self>,
|
||||
@@ -115,6 +119,7 @@ impl GitStore {
|
||||
project_id,
|
||||
client,
|
||||
buffer_store,
|
||||
environment,
|
||||
repositories: Vec::new(),
|
||||
active_index: None,
|
||||
update_sender,
|
||||
@@ -225,6 +230,10 @@ impl GitStore {
|
||||
existing_handle
|
||||
} else {
|
||||
cx.new(|_| Repository {
|
||||
project_environment: self
|
||||
.environment
|
||||
.as_ref()
|
||||
.map(|env| env.downgrade()),
|
||||
git_store: this.clone(),
|
||||
worktree_id,
|
||||
askpass_delegates: Default::default(),
|
||||
@@ -282,9 +291,13 @@ impl GitStore {
|
||||
if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event {
|
||||
let buffer_id = diff.read(cx).buffer_id;
|
||||
if let Some((repo, path)) = this.repository_and_path_for_buffer_id(buffer_id, cx) {
|
||||
let recv = repo
|
||||
.read(cx)
|
||||
.set_index_text(&path, new_index_text.as_ref().map(|rope| rope.to_string()));
|
||||
let recv = repo.update(cx, |repo, cx| {
|
||||
repo.set_index_text(
|
||||
&path,
|
||||
new_index_text.as_ref().map(|rope| rope.to_string()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let diff = diff.downgrade();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some(result) = cx.background_spawn(async move { recv.await.ok() }).await
|
||||
@@ -542,10 +555,11 @@ impl GitStore {
|
||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||
|
||||
repository_handle
|
||||
.update(&mut cx, |repository_handle, _| {
|
||||
.update(&mut cx, |repository_handle, cx| {
|
||||
repository_handle.set_index_text(
|
||||
&RepoPath::from_str(&envelope.payload.path),
|
||||
envelope.payload.text,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await??;
|
||||
@@ -567,8 +581,8 @@ impl GitStore {
|
||||
let email = envelope.payload.email.map(SharedString::from);
|
||||
|
||||
repository_handle
|
||||
.update(&mut cx, |repository_handle, _| {
|
||||
repository_handle.commit(message, name.zip(email))
|
||||
.update(&mut cx, |repository_handle, cx| {
|
||||
repository_handle.commit(message, name.zip(email), cx)
|
||||
})?
|
||||
.await??;
|
||||
Ok(proto::Ack {})
|
||||
@@ -636,7 +650,7 @@ impl GitStore {
|
||||
|
||||
repository_handle
|
||||
.update(&mut cx, |repository_handle, _| {
|
||||
repository_handle.create_branch(branch_name)
|
||||
repository_handle.create_branch(&branch_name)
|
||||
})?
|
||||
.await??;
|
||||
|
||||
@@ -656,7 +670,7 @@ impl GitStore {
|
||||
|
||||
repository_handle
|
||||
.update(&mut cx, |repository_handle, _| {
|
||||
repository_handle.change_branch(branch_name)
|
||||
repository_handle.change_branch(&branch_name)
|
||||
})?
|
||||
.await??;
|
||||
|
||||
@@ -703,8 +717,8 @@ impl GitStore {
|
||||
};
|
||||
|
||||
repository_handle
|
||||
.update(&mut cx, |repository_handle, _| {
|
||||
repository_handle.reset(&envelope.payload.commit, mode)
|
||||
.update(&mut cx, |repository_handle, cx| {
|
||||
repository_handle.reset(&envelope.payload.commit, mode, cx)
|
||||
})?
|
||||
.await??;
|
||||
Ok(proto::Ack {})
|
||||
@@ -727,8 +741,8 @@ impl GitStore {
|
||||
.collect();
|
||||
|
||||
repository_handle
|
||||
.update(&mut cx, |repository_handle, _| {
|
||||
repository_handle.checkout_files(&envelope.payload.commit, paths)
|
||||
.update(&mut cx, |repository_handle, cx| {
|
||||
repository_handle.checkout_files(&envelope.payload.commit, paths, cx)
|
||||
})?
|
||||
.await??;
|
||||
Ok(proto::Ack {})
|
||||
@@ -1115,11 +1129,14 @@ impl Repository {
|
||||
&self,
|
||||
commit: &str,
|
||||
paths: Vec<RepoPath>,
|
||||
cx: &mut App,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
let commit = commit.to_string();
|
||||
let env = self.worktree_environment(cx);
|
||||
|
||||
self.send_job(|git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(repo) => repo.checkout_files(&commit, &paths),
|
||||
GitRepo::Local(repo) => repo.checkout_files(&commit, &paths, &env.await),
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
@@ -1145,11 +1162,20 @@ impl Repository {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver<Result<()>> {
|
||||
pub fn reset(
|
||||
&self,
|
||||
commit: &str,
|
||||
reset_mode: ResetMode,
|
||||
cx: &mut App,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
let commit = commit.to_string();
|
||||
let env = self.worktree_environment(cx);
|
||||
self.send_job(|git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(git_repo) => git_repo.reset(&commit, reset_mode),
|
||||
GitRepo::Local(git_repo) => {
|
||||
let env = env.await;
|
||||
git_repo.reset(&commit, reset_mode, &env)
|
||||
}
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
@@ -1219,6 +1245,7 @@ impl Repository {
|
||||
if entries.is_empty() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
let env = self.worktree_environment(cx);
|
||||
|
||||
let mut save_futures = Vec::new();
|
||||
if let Some(buffer_store) = self.buffer_store(cx) {
|
||||
@@ -1245,11 +1272,12 @@ impl Repository {
|
||||
for save_future in save_futures {
|
||||
save_future.await?;
|
||||
}
|
||||
let env = env.await;
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.send_job(|git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(repo) => repo.stage_paths(&entries),
|
||||
GitRepo::Local(repo) => repo.stage_paths(&entries, &env),
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
@@ -1288,6 +1316,7 @@ impl Repository {
|
||||
if entries.is_empty() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
let env = self.worktree_environment(cx);
|
||||
|
||||
let mut save_futures = Vec::new();
|
||||
if let Some(buffer_store) = self.buffer_store(cx) {
|
||||
@@ -1314,11 +1343,12 @@ impl Repository {
|
||||
for save_future in save_futures {
|
||||
save_future.await?;
|
||||
}
|
||||
let env = env.await;
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.send_job(|git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(repo) => repo.unstage_paths(&entries),
|
||||
GitRepo::Local(repo) => repo.unstage_paths(&entries, &env),
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
@@ -1375,19 +1405,42 @@ impl Repository {
|
||||
self.repository_entry.status_len()
|
||||
}
|
||||
|
||||
fn worktree_environment(
|
||||
&self,
|
||||
cx: &mut App,
|
||||
) -> impl Future<Output = HashMap<String, String>> + 'static {
|
||||
let task = self.project_environment.as_ref().and_then(|env| {
|
||||
env.update(cx, |env, cx| {
|
||||
env.get_environment(
|
||||
Some(self.worktree_id),
|
||||
Some(self.worktree_abs_path.clone()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
async move { OptionFuture::from(task).await.flatten().unwrap_or_default() }
|
||||
}
|
||||
|
||||
pub fn commit(
|
||||
&self,
|
||||
message: SharedString,
|
||||
name_and_email: Option<(SharedString, SharedString)>,
|
||||
cx: &mut App,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
let env = self.worktree_environment(cx);
|
||||
self.send_job(|git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(repo) => repo.commit(
|
||||
message.as_ref(),
|
||||
name_and_email
|
||||
.as_ref()
|
||||
.map(|(name, email)| (name.as_ref(), email.as_ref())),
|
||||
),
|
||||
GitRepo::Local(repo) => {
|
||||
let env = env.await;
|
||||
repo.commit(
|
||||
message.as_ref(),
|
||||
name_and_email
|
||||
.as_ref()
|
||||
.map(|(name, email)| (name.as_ref(), email.as_ref())),
|
||||
&env,
|
||||
)
|
||||
}
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
@@ -1416,17 +1469,19 @@ impl Repository {
|
||||
pub fn fetch(
|
||||
&mut self,
|
||||
askpass: AskPassDelegate,
|
||||
cx: &App,
|
||||
cx: &mut App,
|
||||
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||
let executor = cx.background_executor().clone();
|
||||
let askpass_delegates = self.askpass_delegates.clone();
|
||||
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
|
||||
let env = self.worktree_environment(cx);
|
||||
|
||||
self.send_job(move |git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(git_repository) => {
|
||||
let askpass = AskPassSession::new(&executor, askpass).await?;
|
||||
git_repository.fetch(askpass)
|
||||
let env = env.await;
|
||||
git_repository.fetch(askpass, &env)
|
||||
}
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
@@ -1465,17 +1520,19 @@ impl Repository {
|
||||
remote: SharedString,
|
||||
options: Option<PushOptions>,
|
||||
askpass: AskPassDelegate,
|
||||
cx: &App,
|
||||
cx: &mut App,
|
||||
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||
let executor = cx.background_executor().clone();
|
||||
let askpass_delegates = self.askpass_delegates.clone();
|
||||
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
|
||||
let env = self.worktree_environment(cx);
|
||||
|
||||
self.send_job(move |git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(git_repository) => {
|
||||
let env = env.await;
|
||||
let askpass = AskPassSession::new(&executor, askpass).await?;
|
||||
git_repository.push(&branch, &remote, options, askpass)
|
||||
git_repository.push(&branch, &remote, options, askpass, &env)
|
||||
}
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
@@ -1518,16 +1575,19 @@ impl Repository {
|
||||
branch: SharedString,
|
||||
remote: SharedString,
|
||||
askpass: AskPassDelegate,
|
||||
cx: &App,
|
||||
cx: &mut App,
|
||||
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||
let executor = cx.background_executor().clone();
|
||||
let askpass_delegates = self.askpass_delegates.clone();
|
||||
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
|
||||
let env = self.worktree_environment(cx);
|
||||
|
||||
self.send_job(move |git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(git_repository) => {
|
||||
let askpass = AskPassSession::new(&executor, askpass).await?;
|
||||
git_repository.pull(&branch, &remote, askpass)
|
||||
let env = env.await;
|
||||
git_repository.pull(&branch, &remote, askpass, &env)
|
||||
}
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
@@ -1565,13 +1625,16 @@ impl Repository {
|
||||
&self,
|
||||
path: &RepoPath,
|
||||
content: Option<String>,
|
||||
cx: &mut App,
|
||||
) -> oneshot::Receiver<anyhow::Result<()>> {
|
||||
let path = path.clone();
|
||||
let env = self.worktree_environment(cx);
|
||||
|
||||
self.send_keyed_job(
|
||||
Some(GitJobKey::WriteIndex(path.clone())),
|
||||
|git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(repo) => repo.set_index_text(&path, content),
|
||||
GitRepo::Local(repo) => repo.set_index_text(&path, content, &env.await),
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
@@ -1695,7 +1758,8 @@ impl Repository {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_branch(&self, branch_name: String) -> oneshot::Receiver<Result<()>> {
|
||||
pub fn create_branch(&self, branch_name: &str) -> oneshot::Receiver<Result<()>> {
|
||||
let branch_name = branch_name.to_owned();
|
||||
self.send_job(|repo| async move {
|
||||
match repo {
|
||||
GitRepo::Local(git_repository) => git_repository.create_branch(&branch_name),
|
||||
@@ -1720,7 +1784,8 @@ impl Repository {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn change_branch(&self, branch_name: String) -> oneshot::Receiver<Result<()>> {
|
||||
pub fn change_branch(&self, branch_name: &str) -> oneshot::Receiver<Result<()>> {
|
||||
let branch_name = branch_name.to_owned();
|
||||
self.send_job(|repo| async move {
|
||||
match repo {
|
||||
GitRepo::Local(git_repository) => git_repository.change_branch(&branch_name),
|
||||
|
||||
@@ -1245,7 +1245,6 @@ impl LocalLspStore {
|
||||
Ok(project_transaction)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn execute_formatters(
|
||||
lsp_store: WeakEntity<LspStore>,
|
||||
formatters: &[Formatter],
|
||||
@@ -1495,7 +1494,6 @@ impl LocalLspStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn format_via_lsp(
|
||||
this: &WeakEntity<LspStore>,
|
||||
buffer: &Entity<Buffer>,
|
||||
@@ -2999,7 +2997,6 @@ impl LspStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_local(
|
||||
buffer_store: Entity<BufferStore>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
@@ -3093,7 +3090,6 @@ impl LspStore {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn new_remote(
|
||||
buffer_store: Entity<BufferStore>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
@@ -4619,7 +4615,6 @@ impl LspStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn resolve_completion_remote(
|
||||
project_id: u64,
|
||||
server_id: LanguageServerId,
|
||||
@@ -7736,7 +7731,6 @@ impl LspStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn insert_newly_running_language_server(
|
||||
&mut self,
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
|
||||
@@ -844,6 +844,7 @@ impl Project {
|
||||
GitStore::new(
|
||||
&worktree_store,
|
||||
buffer_store.clone(),
|
||||
Some(environment.clone()),
|
||||
client.clone().into(),
|
||||
None,
|
||||
cx,
|
||||
@@ -972,6 +973,7 @@ impl Project {
|
||||
GitStore::new(
|
||||
&worktree_store,
|
||||
buffer_store.clone(),
|
||||
Some(environment.clone()),
|
||||
ssh_proto.clone(),
|
||||
Some(ProjectId(SSH_PROJECT_ID)),
|
||||
cx,
|
||||
@@ -1116,7 +1118,6 @@ impl Project {
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn from_join_project_response(
|
||||
response: TypedEnvelope<proto::JoinProjectResponse>,
|
||||
subscriptions: [EntitySubscription; 5],
|
||||
@@ -1179,6 +1180,7 @@ impl Project {
|
||||
GitStore::new(
|
||||
&worktree_store,
|
||||
buffer_store.clone(),
|
||||
None,
|
||||
client.clone().into(),
|
||||
Some(ProjectId(remote_id)),
|
||||
cx,
|
||||
|
||||
@@ -179,12 +179,13 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
|
||||
if self.store.prompt_count() == 0 {
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
let text = if self.store.prompt_count() == 0 {
|
||||
"No prompts.".into()
|
||||
} else {
|
||||
"No prompts found matching your search.".into()
|
||||
}
|
||||
};
|
||||
Some(text)
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
syntax = "proto3";
|
||||
package zed.messages;
|
||||
import "google/protobuf/wrappers.proto";
|
||||
|
||||
// Looking for a number? Search "// current max"
|
||||
|
||||
@@ -1003,8 +1002,8 @@ message Completion {
|
||||
optional bytes lsp_defaults = 8;
|
||||
|
||||
enum Source {
|
||||
Custom = 0;
|
||||
Lsp = 1;
|
||||
Lsp = 0;
|
||||
Custom = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2322,8 +2321,8 @@ message BlameBufferResponse {
|
||||
message BlameResponse {
|
||||
repeated BlameEntry entries = 1;
|
||||
repeated CommitMessage messages = 2;
|
||||
repeated CommitPermalink permalinks = 3;
|
||||
optional string remote_url = 4;
|
||||
reserved 3;
|
||||
}
|
||||
|
||||
optional BlameResponse blame_response = 5;
|
||||
|
||||
@@ -351,12 +351,13 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
|
||||
fn dismissed(&mut self, _window: &mut Window, _: &mut Context<Picker<Self>>) {}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
|
||||
if self.workspaces.is_empty() {
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
let text = if self.workspaces.is_empty() {
|
||||
"Recently opened projects will show up here".into()
|
||||
} else {
|
||||
"No matches".into()
|
||||
}
|
||||
};
|
||||
Some(text)
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
|
||||
@@ -1280,7 +1280,6 @@ impl From<SshRemoteClient> for AnyProtoClient {
|
||||
|
||||
#[async_trait(?Send)]
|
||||
trait RemoteConnection: Send + Sync {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn start_proxy(
|
||||
&self,
|
||||
unique_identifier: String,
|
||||
|
||||
@@ -87,10 +87,12 @@ impl HeadlessProject {
|
||||
buffer_store
|
||||
});
|
||||
|
||||
let environment = project::ProjectEnvironment::new(&worktree_store, None, cx);
|
||||
let git_store = cx.new(|cx| {
|
||||
GitStore::new(
|
||||
&worktree_store,
|
||||
buffer_store.clone(),
|
||||
Some(environment.clone()),
|
||||
session.clone().into(),
|
||||
None,
|
||||
cx,
|
||||
@@ -105,7 +107,6 @@ impl HeadlessProject {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let environment = project::ProjectEnvironment::new(&worktree_store, None, cx);
|
||||
let toolchain_store = cx.new(|cx| {
|
||||
ToolchainStore::local(
|
||||
languages.clone(),
|
||||
|
||||
@@ -1361,7 +1361,7 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
|
||||
|
||||
assert_eq!(&remote_branches, &branches_set);
|
||||
|
||||
cx.update(|cx| repository.read(cx).change_branch(new_branch.to_string()))
|
||||
cx.update(|cx| repository.read(cx).change_branch(new_branch))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -1383,23 +1383,15 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
|
||||
assert_eq!(server_branch.name, branches[2]);
|
||||
|
||||
// Also try creating a new branch
|
||||
cx.update(|cx| {
|
||||
repository
|
||||
.read(cx)
|
||||
.create_branch("totally-new-branch".to_string())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
cx.update(|cx| repository.read(cx).create_branch("totally-new-branch"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
repository
|
||||
.read(cx)
|
||||
.change_branch("totally-new-branch".to_string())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
cx.update(|cx| repository.read(cx).change_branch("totally-new-branch"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
|
||||
@@ -445,7 +445,7 @@ pub fn execute_run(
|
||||
let extension_host_proxy = ExtensionHostProxy::global(cx);
|
||||
|
||||
let project = cx.new(|cx| {
|
||||
let fs = Arc::new(RealFs::new(Default::default(), None));
|
||||
let fs = Arc::new(RealFs::new(None));
|
||||
let node_settings_rx = initialize_settings(session.clone(), fs.clone(), cx);
|
||||
|
||||
let proxy_url = read_proxy_settings(cx);
|
||||
|
||||
@@ -175,7 +175,6 @@ impl RichText {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_markdown_mut(
|
||||
block: &str,
|
||||
mut mentions: &[Mention],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "assistant_scripting"
|
||||
name = "scripting_tool"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
@@ -9,7 +9,7 @@ license = "GPL-3.0-or-later"
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant_scripting.rs"
|
||||
path = "src/scripting_tool.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
@@ -17,11 +17,13 @@ anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
mlua.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
@@ -30,6 +32,7 @@ util.workspace = true
|
||||
[dev-dependencies]
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
@@ -3,6 +3,16 @@
|
||||
-- Create a sandbox environment
|
||||
local sandbox = {}
|
||||
|
||||
-- For now, add all globals to `sandbox` (so there effectively is no sandbox).
|
||||
-- We still need the logic below so that we can do things like overriding print() to write
|
||||
-- to our in-memory log rather than to stdout, we will delete this loop (and re-enable
|
||||
-- the I/O module being sandboxed below) to have things be sandboxed again.
|
||||
for k, v in pairs(_G) do
|
||||
if sandbox[k] == nil then
|
||||
sandbox[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
-- Allow access to standard libraries (safe subset)
|
||||
sandbox.string = string
|
||||
sandbox.table = table
|
||||
@@ -19,7 +29,10 @@ sandbox.search = search
|
||||
sandbox.outline = outline
|
||||
|
||||
-- Create a sandboxed version of LuaFileIO
|
||||
local io = {}
|
||||
-- local io = {};
|
||||
--
|
||||
-- For now we are using unsandboxed io
|
||||
local io = _G.io;
|
||||
|
||||
-- File functions
|
||||
io.open = sb_io_open
|
||||
@@ -27,17 +40,16 @@ io.open = sb_io_open
|
||||
-- Add the sandboxed io library to the sandbox environment
|
||||
sandbox.io = io
|
||||
|
||||
|
||||
-- Load the script with the sandbox environment
|
||||
local user_script_fn, err = load(user_script, nil, "t", sandbox)
|
||||
|
||||
if not user_script_fn then
|
||||
error("Failed to load user script: " .. tostring(err))
|
||||
error("Failed to load user script: " .. tostring(err))
|
||||
end
|
||||
|
||||
-- Execute the user script within the sandbox
|
||||
local success, result = pcall(user_script_fn)
|
||||
|
||||
if not success then
|
||||
error("Error executing user script: " .. tostring(result))
|
||||
error("Error executing user script: " .. tostring(result))
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user