Compare commits
149 Commits
tool-calli
...
debugger-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdc5999d79 | ||
|
|
74931bd472 | ||
|
|
7f8c28877f | ||
|
|
1ff23477de | ||
|
|
d28950c633 | ||
|
|
6ff5e00740 | ||
|
|
b70acdfa4a | ||
|
|
403ae10087 | ||
|
|
9a8a54109e | ||
|
|
11c740b47a | ||
|
|
1a421db92d | ||
|
|
9c60884771 | ||
|
|
ee3323d12a | ||
|
|
b88fd3e0c5 | ||
|
|
2e10853b34 | ||
|
|
d08e28f4e0 | ||
|
|
ef67321ff2 | ||
|
|
4c777ad140 | ||
|
|
923ae5473a | ||
|
|
a48166e5a0 | ||
|
|
ef098c028b | ||
|
|
7ce1b8dc76 | ||
|
|
e350417a33 | ||
|
|
ea9e0755df | ||
|
|
6aced1b3aa | ||
|
|
99e01fc608 | ||
|
|
fe899c9164 | ||
|
|
f8b9937e51 | ||
|
|
dc5928374e | ||
|
|
0deb3cc606 | ||
|
|
ba4a70d7ae | ||
|
|
65a790e4ca | ||
|
|
b6e677eb06 | ||
|
|
ffa0609f8d | ||
|
|
77a314350f | ||
|
|
737b03c928 | ||
|
|
1b42dd5865 | ||
|
|
c9074b1c25 | ||
|
|
00379280f3 | ||
|
|
4e2d0351cc | ||
|
|
a583efd9b9 | ||
|
|
6237c29a42 | ||
|
|
9ea9b41e73 | ||
|
|
8d99f9b7d2 | ||
|
|
014ffbce2e | ||
|
|
68dd3c90c2 | ||
|
|
4a6f6151f0 | ||
|
|
f108d4c705 | ||
|
|
5ab95f1e1a | ||
|
|
49da08ffa4 | ||
|
|
f287c897a4 | ||
|
|
d15ff2d06f | ||
|
|
648daa3237 | ||
|
|
33e127de09 | ||
|
|
5a9b279039 | ||
|
|
cce58570dc | ||
|
|
361bbec3a0 | ||
|
|
a87409813c | ||
|
|
da84aa1ac2 | ||
|
|
817760688a | ||
|
|
13e56010c1 | ||
|
|
b827a35e44 | ||
|
|
9006e8fdff | ||
|
|
ce8ec033f4 | ||
|
|
9678cc9bc3 | ||
|
|
e87c4ddadc | ||
|
|
3f8581a2fb | ||
|
|
00c5b83384 | ||
|
|
4aedc1cd0b | ||
|
|
d238675c1a | ||
|
|
dcf6f6ca30 | ||
|
|
f4606bd951 | ||
|
|
12bef0830a | ||
|
|
953a2b376c | ||
|
|
ac3b9f7a4c | ||
|
|
ef5990d427 | ||
|
|
23a81d5d70 | ||
|
|
515122c54d | ||
|
|
f4eacca987 | ||
|
|
003fb7c81e | ||
|
|
7d2f63ebbd | ||
|
|
93e0bbb833 | ||
|
|
8c5f6a0be7 | ||
|
|
d6cafb8315 | ||
|
|
a24b76b30b | ||
|
|
11d74ea4ec | ||
|
|
93f4775cf6 | ||
|
|
a93913b9a3 | ||
|
|
08afbc6b58 | ||
|
|
b869465f00 | ||
|
|
2debea8115 | ||
|
|
cae295ff65 | ||
|
|
c51206e980 | ||
|
|
1baa5aea94 | ||
|
|
3e022a5565 | ||
|
|
3dd769be94 | ||
|
|
7936a4bee3 | ||
|
|
09aabe481c | ||
|
|
2ea1e4fa85 | ||
|
|
8699dad0e3 | ||
|
|
47a5f0c620 | ||
|
|
854ff68bac | ||
|
|
01d384e676 | ||
|
|
ddd893a795 | ||
|
|
3d7cd5dac7 | ||
|
|
a6fdfb5191 | ||
|
|
73a68d560f | ||
|
|
b4eeb25f55 | ||
|
|
fc991ab273 | ||
|
|
ab58d14559 | ||
|
|
3a0b311378 | ||
|
|
b81065fe63 | ||
|
|
a67f28dba2 | ||
|
|
99b2472e83 | ||
|
|
be45d5aa73 | ||
|
|
0508df9e7b | ||
|
|
153efab377 | ||
|
|
8015fb70e3 | ||
|
|
79d23aa4fe | ||
|
|
d5dae425fc | ||
|
|
d9e09c4a66 | ||
|
|
331625e876 | ||
|
|
61949fb348 | ||
|
|
c7f4e09496 | ||
|
|
5442e116ce | ||
|
|
11a4fc8b02 | ||
|
|
d303ebd46e | ||
|
|
e1de8dc50e | ||
|
|
5fe110c1dd | ||
|
|
89b203d03a | ||
|
|
0f4f8abbaa | ||
|
|
0d97e9e579 | ||
|
|
14b913fb4b | ||
|
|
9f1cd2bdb5 | ||
|
|
547c40e332 | ||
|
|
9cff6d5aa5 | ||
|
|
7e438bc1f3 | ||
|
|
944a52ce91 | ||
|
|
95a814ed41 | ||
|
|
6b9295b6c4 | ||
|
|
1128fce61a | ||
|
|
7c355fdb0f | ||
|
|
0e2a0b9edc | ||
|
|
c130f9c2f2 | ||
|
|
08300c6e90 | ||
|
|
7b71119094 | ||
|
|
c18db76862 | ||
|
|
c0dd152509 | ||
|
|
f402a4e5ce |
@@ -12,7 +12,3 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
# This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = ["--cfg", "windows_slim_errors"]
|
||||
|
||||
3
.github/workflows/ci.yml
vendored
@@ -147,8 +147,7 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: cargo clippy
|
||||
# Windows can't run shell scripts, so we need to use `cargo xtask`.
|
||||
run: cargo xtask clippy
|
||||
run: ./script/clippy
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
|
||||
1
.gitignore
vendored
@@ -29,3 +29,4 @@ DerivedData/
|
||||
.vscode
|
||||
.wrangler
|
||||
.flatpak-builder
|
||||
.zed/debug.json
|
||||
|
||||
1113
Cargo.lock
generated
69
Cargo.toml
@@ -1,11 +1,11 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/anthropic",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant_slash_command",
|
||||
"crates/assistant_tooling",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
@@ -21,6 +21,8 @@ members = [
|
||||
"crates/command_palette_hooks",
|
||||
"crates/completion",
|
||||
"crates/copilot",
|
||||
"crates/dap",
|
||||
"crates/debugger_ui",
|
||||
"crates/db",
|
||||
"crates/dev_server_projects",
|
||||
"crates/diagnostics",
|
||||
@@ -125,10 +127,6 @@ members = [
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
|
||||
#
|
||||
# Extensions
|
||||
#
|
||||
|
||||
"extensions/astro",
|
||||
"extensions/clojure",
|
||||
"extensions/csharp",
|
||||
@@ -158,25 +156,20 @@ members = [
|
||||
"extensions/vue",
|
||||
"extensions/zig",
|
||||
|
||||
#
|
||||
# Tooling
|
||||
#
|
||||
|
||||
"tooling/xtask",
|
||||
]
|
||||
default-members = ["crates/zed"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
#
|
||||
# Workspace member crates
|
||||
#
|
||||
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
aho-corasick = "1.1"
|
||||
ai = { path = "crates/ai" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_tooling = { path = "crates/assistant_tooling" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
breadcrumbs = { path = "crates/breadcrumbs" }
|
||||
@@ -192,7 +185,9 @@ command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
completion = { path = "crates/completion" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
dap = { path = "crates/dap" }
|
||||
db = { path = "crates/db" }
|
||||
debugger_ui = { path = "crates/debugger_ui" }
|
||||
dev_server_projects = { path = "crates/dev_server_projects" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
editor = { path = "crates/editor" }
|
||||
@@ -249,7 +244,6 @@ project_symbols = { path = "crates/project_symbols" }
|
||||
proto = { path = "crates/proto" }
|
||||
quick_action_bar = { path = "crates/quick_action_bar" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
refineable = { path = "crates/refineable" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
remote = { path = "crates/remote" }
|
||||
remote_server = { path = "crates/remote_server" }
|
||||
@@ -295,44 +289,39 @@ worktree = { path = "crates/worktree" }
|
||||
zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
|
||||
#
|
||||
# External crates
|
||||
#
|
||||
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = "0.23"
|
||||
any_vec = "0.14"
|
||||
anyhow = "1.0.86"
|
||||
ashpd = "0.9.1"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = "0.1"
|
||||
async-dispatcher = { version = "0.1" }
|
||||
async-fs = "1.6"
|
||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
async-tungstenite = "0.23"
|
||||
async-tungstenite = { version = "0.16" }
|
||||
async-watch = "0.3.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
base64 = "0.22"
|
||||
base64 = "0.13"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
|
||||
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
|
||||
blade-util = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
|
||||
cargo_metadata = "0.18"
|
||||
cap-std = "3.0"
|
||||
cargo_toml = "0.20"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clickhouse = "0.11.6"
|
||||
clickhouse = { version = "0.11.6" }
|
||||
cocoa = "0.25"
|
||||
core-foundation = "0.9.3"
|
||||
core-foundation = { version = "0.9.3" }
|
||||
core-foundation-sys = "0.8.6"
|
||||
ctor = "0.2.6"
|
||||
dashmap = "6.0"
|
||||
dashmap = "5.5.3"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
env_logger = "0.10"
|
||||
exec = "0.3.1"
|
||||
fork = "0.1.23"
|
||||
futures = "0.3"
|
||||
@@ -346,13 +335,12 @@ html5ever = "0.27.0"
|
||||
ignore = "0.4.22"
|
||||
image = "0.25.1"
|
||||
indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
indoc = "2"
|
||||
indoc = "1"
|
||||
# We explicitly disable http2 support in isahc.
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
"text-decoding",
|
||||
] }
|
||||
itertools = "0.11.0"
|
||||
jsonwebtoken = "9.3"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2"
|
||||
linkify = "0.10.0"
|
||||
@@ -374,6 +362,7 @@ prost-build = "0.9"
|
||||
prost-types = "0.9"
|
||||
pulldown-cmark = { version = "0.10.0", default-features = false }
|
||||
rand = "0.8.5"
|
||||
refineable = { path = "./crates/refineable" }
|
||||
regex = "1.5"
|
||||
repair_json = "0.1.0"
|
||||
rsa = "0.9.6"
|
||||
@@ -403,7 +392,6 @@ smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
subtle = "2.5.0"
|
||||
sys-locale = "0.3.1"
|
||||
sysinfo = "0.30.7"
|
||||
tempfile = "3.9.0"
|
||||
thiserror = "1.0.29"
|
||||
@@ -427,7 +415,7 @@ tree-sitter-css = "0.21"
|
||||
tree-sitter-elixir = "0.2"
|
||||
tree-sitter-embedded-template = "0.20.0"
|
||||
tree-sitter-go = "0.21"
|
||||
tree-sitter-go-mod = { git = "https://github.com/SomeoneToIgnore/tree-sitter-go-mod", rev = "8c1f54f12bb4c846336b634bc817645d6f35d641", package = "tree-sitter-gomod" }
|
||||
tree-sitter-go-mod = { git = "https://github.com/SomeoneToIgnore/tree-sitter-go-mod", rev = "8c1f54f12bb4c846336b634bc817645d6f35d641", package = "tree-sitter-gomod"}
|
||||
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work", rev = "dcbabff454703c3a4bc98a23cf8778d4be46fd22" }
|
||||
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "6dd0303acf7138dd2b9b432a229e16539581c701" }
|
||||
tree-sitter-html = "0.20"
|
||||
@@ -448,19 +436,20 @@ url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
|
||||
wasmparser = "0.201"
|
||||
wasm-encoder = "0.201"
|
||||
wasmtime = { version = "21.0.1", default-features = false, features = [
|
||||
wasmtime = { version = "19.0.2", default-features = false, features = [
|
||||
"async",
|
||||
"demangle",
|
||||
"runtime",
|
||||
"cranelift",
|
||||
"component-model",
|
||||
] }
|
||||
wasmtime-wasi = "21.0.1"
|
||||
wasmtime-wasi = "19.0.2"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.201"
|
||||
sys-locale = "0.3.1"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.58"
|
||||
version = "0.57"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Numerics",
|
||||
@@ -480,6 +469,7 @@ features = [
|
||||
"Win32_Security",
|
||||
"Win32_Security_Credentials",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_DataExchange",
|
||||
@@ -498,10 +488,6 @@ features = [
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
# Patch Tree-sitter for updated wasmtime.
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7f4a57817d58a2f134fe863674acad6bbf007228" }
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
debug = "limited"
|
||||
@@ -553,6 +539,13 @@ single_range_in_vec_init = "allow"
|
||||
style = { level = "allow", priority = -1 }
|
||||
|
||||
# Individual rules that have violations in the codebase:
|
||||
almost_complete_range = "allow"
|
||||
arc_with_non_send_sync = "allow"
|
||||
borrowed_box = "allow"
|
||||
let_underscore_future = "allow"
|
||||
map_entry = "allow"
|
||||
non_canonical_partial_ord_impl = "allow"
|
||||
reversed_empty_ranges = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.80-bookworm as builder
|
||||
FROM rust:1.79-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
1
assets/icons/debug-continue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 2H4v12H2.5V2zm4.936.39L6.25 3v10l1.186.61 7-5V7.39l-7-5zM12.71 8l-4.96 3.543V4.457L12.71 8z"/></svg>
|
||||
|
After Width: | Height: | Size: 257 B |
1
assets/icons/debug-pause.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M4.5 3H6v10H4.5V3zm7 0v10H10V3h1.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 156 B |
1
assets/icons/debug-restart.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.75 8a4.5 4.5 0 0 1-8.61 1.834l-1.391.565A6.001 6.001 0 0 0 14.25 8 6 6 0 0 0 3.5 4.334V2.5H2v4l.75.75h3.5v-1.5H4.352A4.5 4.5 0 0 1 12.75 8z"/></svg>
|
||||
|
After Width: | Height: | Size: 304 B |
1
assets/icons/debug-step-into.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 9.532h.542l3.905-3.905-1.061-1.06-2.637 2.61V1H7.251v6.177l-2.637-2.61-1.061 1.06 3.905 3.905H8zm1.956 3.481a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 301 B |
1
assets/icons/debug-step-out.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 1h-.542L3.553 4.905l1.061 1.06 2.637-2.61v6.177h1.498V3.355l2.637 2.61 1.061-1.06L8.542 1H8zm1.956 12.013a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 298 B |
1
assets/icons/debug-step-over.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M14.25 5.75v-4h-1.5v2.542c-1.145-1.359-2.911-2.209-4.84-2.209-3.177 0-5.92 2.307-6.16 5.398l-.02.269h1.501l.022-.226c.212-2.195 2.202-3.94 4.656-3.94 1.736 0 3.244.875 4.05 2.166h-2.83v1.5h4.163l.962-.975V5.75h-.004zM8 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg>
|
||||
|
After Width: | Height: | Size: 411 B |
1
assets/icons/debug-stop.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M13 1.99976L14 2.99976V12.9998L13 13.9998H3L2 12.9998L2 2.99976L3 1.99976H13ZM12.7461 3.25057L3.25469 3.25057L3.25469 12.7504H12.7461V3.25057Z"/></svg>
|
||||
|
After Width: | Height: | Size: 303 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
|
Before Width: | Height: | Size: 358 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-code"><path d="M10 12.5 8 15l2 2.5"/><path d="m14 12.5 2 2.5-2 2.5"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/></svg>
|
||||
|
Before Width: | Height: | Size: 388 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-text"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>
|
||||
|
Before Width: | Height: | Size: 384 B |
@@ -1,6 +0,0 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 4H8" stroke="black" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 10L11 10" stroke="black" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="4" cy="10" r="1.875" stroke="black" stroke-width="1.75"/>
|
||||
<circle cx="10" cy="4" r="1.875" stroke="black" stroke-width="1.75"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 450 B |
@@ -40,6 +40,7 @@
|
||||
"backspace": "editor::Backspace",
|
||||
"shift-backspace": "editor::Backspace",
|
||||
"delete": "editor::Delete",
|
||||
"ctrl-d": "editor::Delete",
|
||||
"tab": "editor::Tab",
|
||||
"shift-tab": "editor::TabPrev",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
@@ -268,7 +269,6 @@
|
||||
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }],
|
||||
"ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // Add selection to Next Find Match
|
||||
"ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }],
|
||||
|
||||
@@ -26,9 +26,6 @@
|
||||
},
|
||||
// The name of a font to use for rendering text in the editor
|
||||
"buffer_font_family": "Zed Plex Mono",
|
||||
// Set the buffer text's font fallbacks, this will be merged with
|
||||
// the platform's default fallbacks.
|
||||
"buffer_font_fallbacks": [],
|
||||
// The OpenType features to enable for text in the editor.
|
||||
"buffer_font_features": {
|
||||
// Disable ligatures:
|
||||
@@ -50,11 +47,8 @@
|
||||
// },
|
||||
"buffer_line_height": "comfortable",
|
||||
// The name of a font to use for rendering text in the UI
|
||||
// You can set this to ".SystemUIFont" to use the system font
|
||||
// (On macOS) You can set this to ".SystemUIFont" to use the system font
|
||||
"ui_font_family": "Zed Plex Sans",
|
||||
// Set the UI's font fallbacks, this will be merged with the platform's
|
||||
// default font fallbacks.
|
||||
"ui_font_fallbacks": [],
|
||||
// The OpenType features to enable for text in the UI
|
||||
"ui_font_features": {
|
||||
// Disable ligatures:
|
||||
@@ -318,7 +312,7 @@
|
||||
"auto_reveal_entries": true,
|
||||
// Whether to fold directories automatically and show compact folders
|
||||
// (e.g. "a/b/c" ) when a directory has only one subdirectory inside.
|
||||
"auto_fold_dirs": true,
|
||||
"auto_fold_dirs": false,
|
||||
/// Scrollbar-related settings
|
||||
"scrollbar": {
|
||||
/// When to show the scrollbar in the project panel.
|
||||
@@ -681,10 +675,6 @@
|
||||
// Set the terminal's font family. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font family.
|
||||
// "font_family": "Zed Plex Mono",
|
||||
// Set the terminal's font fallbacks. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font fallbacks.
|
||||
// This will be merged with the platform's default font fallbacks
|
||||
// "font_fallbacks": ["FiraCode Nerd Fonts"],
|
||||
// Sets the maximum number of lines in the terminal's scrollback buffer.
|
||||
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
|
||||
// Existing terminals will not pick up this change until they are recreated.
|
||||
@@ -975,21 +965,5 @@
|
||||
// {
|
||||
// "W": "workspace::Save"
|
||||
// }
|
||||
"command_aliases": {},
|
||||
// ssh_connections is an array of ssh connections.
|
||||
// By default this setting is null, which disables the direct ssh connection support.
|
||||
// You can configure these from `project: Open Remote` in the command palette.
|
||||
// Zed's ssh support will pull configuration from your ~/.ssh too.
|
||||
// Examples:
|
||||
// [
|
||||
// {
|
||||
// "host": "example-box",
|
||||
// "projects": [
|
||||
// {
|
||||
// "paths": ["/home/user/code/zed"]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
"ssh_connections": null
|
||||
"command_aliases": {}
|
||||
}
|
||||
|
||||
@@ -100,13 +100,21 @@ impl From<Role> for String {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Request {
|
||||
pub model: String,
|
||||
#[serde(serialize_with = "serialize_request_model")]
|
||||
pub model: Model,
|
||||
pub messages: Vec<RequestMessage>,
|
||||
pub stream: bool,
|
||||
pub system: String,
|
||||
pub max_tokens: u32,
|
||||
}
|
||||
|
||||
fn serialize_request_model<S>(model: &Model, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&model.id())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct RequestMessage {
|
||||
pub role: Role,
|
||||
|
||||
@@ -38,7 +38,7 @@ Considering these aspects will ensure our conversation view design is optimized
|
||||
|
||||
@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5.
|
||||
|
||||
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multiple people, or between multiple people and multiple bots (you and other bots).
|
||||
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots).
|
||||
|
||||
@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try...
|
||||
|
||||
|
||||
@@ -314,7 +314,7 @@ impl AssistantPanel {
|
||||
workspace.project().clone(),
|
||||
Default::default(),
|
||||
None,
|
||||
NewFile.boxed_clone(),
|
||||
Some(NewFile.boxed_clone()),
|
||||
cx,
|
||||
);
|
||||
pane.set_can_split(false, cx);
|
||||
|
||||
@@ -532,21 +532,7 @@ impl EditOperation {
|
||||
.path_candidates
|
||||
.iter()
|
||||
.find(|item| item.string == symbol)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"symbol {:?} not found in path {:?}.\ncandidates: {:?}.\nparse status: {:?}. text:\n{}",
|
||||
symbol,
|
||||
path,
|
||||
outline
|
||||
.path_candidates
|
||||
.iter()
|
||||
.map(|candidate| &candidate.string)
|
||||
.collect::<Vec<_>>(),
|
||||
*parse_status.borrow(),
|
||||
buffer.read_with(&cx, |buffer, _| buffer.text()).unwrap_or_else(|_| "error".to_string())
|
||||
)
|
||||
})?;
|
||||
|
||||
.context("symbol not found")?;
|
||||
buffer.update(&mut cx, |buffer, _| {
|
||||
let outline_item = &outline.items[candidate.id];
|
||||
let symbol_range = outline_item.range.to_point(buffer);
|
||||
@@ -1137,17 +1123,16 @@ impl Context {
|
||||
.timer(Duration::from_millis(200))
|
||||
.await;
|
||||
|
||||
if let Some(token_count) = cx.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})? {
|
||||
let token_count = token_count.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
cx.notify()
|
||||
})?;
|
||||
}
|
||||
let token_count = cx
|
||||
.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
cx.notify()
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
|
||||
@@ -1420,34 +1420,27 @@ impl Render for PromptEditor {
|
||||
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
ModelSelector::new(
|
||||
self.fs.clone(),
|
||||
IconButton::new("context", IconName::SlidersAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelCompletionProvider::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.with_info_text(
|
||||
"Inline edits use context\n\
|
||||
from the currently selected\n\
|
||||
assistant panel tab.",
|
||||
),
|
||||
)
|
||||
.child(ModelSelector::new(
|
||||
self.fs.clone(),
|
||||
IconButton::new("context", IconName::Settings)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelCompletionProvider::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
))
|
||||
.children(
|
||||
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
|
||||
let error_message = SharedString::from(error.to_string());
|
||||
@@ -1635,18 +1628,15 @@ impl PromptEditor {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
if let Some(token_count) = cx.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})? {
|
||||
let token_count = token_count.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
let token_count = cx
|
||||
.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1835,7 +1825,6 @@ impl PromptEditor {
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(1.3),
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::sync::Arc;
|
||||
|
||||
use crate::{assistant_settings::AssistantSettings, LanguageModelCompletionProvider};
|
||||
use fs::Fs;
|
||||
use gpui::SharedString;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use settings::update_settings_file;
|
||||
use ui::{prelude::*, ContextMenu, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
|
||||
@@ -12,7 +11,6 @@ pub struct ModelSelector<T: PopoverTrigger> {
|
||||
handle: Option<PopoverMenuHandle<ContextMenu>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
trigger: T,
|
||||
info_text: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> ModelSelector<T> {
|
||||
@@ -21,7 +19,6 @@ impl<T: PopoverTrigger> ModelSelector<T> {
|
||||
handle: None,
|
||||
fs,
|
||||
trigger,
|
||||
info_text: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +26,6 @@ impl<T: PopoverTrigger> ModelSelector<T> {
|
||||
self.handle = Some(handle);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_info_text(mut self, text: impl Into<SharedString>) -> Self {
|
||||
self.info_text = Some(text.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
|
||||
@@ -43,20 +35,8 @@ impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
|
||||
menu = menu.with_handle(handle);
|
||||
}
|
||||
|
||||
let info_text = self.info_text.clone();
|
||||
|
||||
menu.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
if let Some(info_text) = info_text.clone() {
|
||||
menu = menu
|
||||
.custom_row(move |_cx| {
|
||||
Label::new(info_text.clone())
|
||||
.color(Color::Muted)
|
||||
.into_any_element()
|
||||
})
|
||||
.separator();
|
||||
}
|
||||
|
||||
for (index, provider) in LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
|
||||
@@ -734,29 +734,26 @@ impl PromptLibrary {
|
||||
const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
|
||||
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
|
||||
if let Some(token_count) = cx.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(
|
||||
LanguageModelRequest {
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: body.to_string(),
|
||||
}],
|
||||
stop: Vec::new(),
|
||||
temperature: 1.,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})? {
|
||||
let token_count = token_count.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let prompt_editor = this.prompt_editors.get_mut(&prompt_id).unwrap();
|
||||
prompt_editor.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
let token_count = cx
|
||||
.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(
|
||||
LanguageModelRequest {
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: body.to_string(),
|
||||
}],
|
||||
stop: Vec::new(),
|
||||
temperature: 1.,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let prompt_editor = this.prompt_editors.get_mut(&prompt_id).unwrap();
|
||||
prompt_editor.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ impl DiagnosticsSlashCommand {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
let path_prefix: Arc<str> = Arc::default();
|
||||
let path_prefix: Arc<str> = "".into();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
|
||||
@@ -219,7 +219,7 @@ impl SlashCommand for DocsSlashCommand {
|
||||
if index {
|
||||
// We don't need to hold onto this task, as the `IndexedDocsStore` will hold it
|
||||
// until it completes.
|
||||
drop(store.clone().index(package.as_str().into()));
|
||||
let _ = store.clone().index(package.as_str().into());
|
||||
}
|
||||
|
||||
let items = store.search(package).await;
|
||||
|
||||
@@ -29,7 +29,7 @@ impl FileSlashCommand {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
let path_prefix: Arc<str> = Arc::default();
|
||||
let path_prefix: Arc<str> = "".into();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
|
||||
@@ -707,18 +707,15 @@ impl PromptEditor {
|
||||
inline_assistant.request_for_inline_assist(assist_id, cx)
|
||||
})??;
|
||||
|
||||
if let Some(token_count) = cx.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})? {
|
||||
let token_count = token_count.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
let token_count = cx
|
||||
.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -909,7 +906,6 @@ impl PromptEditor {
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(1.3),
|
||||
@@ -947,7 +943,7 @@ impl TerminalTransaction {
|
||||
}
|
||||
|
||||
pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
|
||||
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
|
||||
// Ensure that the assistant cannot accidently execute commands that are streamed into the terminal
|
||||
let input = hunk.replace(CARRIAGE_RETURN, " ");
|
||||
self.terminal
|
||||
.update(cx, |terminal, _| terminal.input(input));
|
||||
|
||||
33
crates/assistant_tooling/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "assistant_tooling"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant_tooling.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
project.workspace = true
|
||||
repair_json.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sum_tree.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
1
crates/assistant_tooling/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
85
crates/assistant_tooling/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Assistant Tooling
|
||||
|
||||
Bringing Language Model tool calling to GPUI.
|
||||
|
||||
This unlocks:
|
||||
|
||||
- **Structured Extraction** of model responses
|
||||
- **Validation** of model inputs
|
||||
- **Execution** of chosen tools
|
||||
|
||||
## Overview
|
||||
|
||||
Language Models can produce structured outputs that are perfect for calling functions. The most famous of these is OpenAI's tool calling. When making a chat completion you can pass a list of tools available to the model. The model will choose `0..n` tools to help them complete a user's task. It's up to _you_ to create the tools that the model can call.
|
||||
|
||||
> **User**: "Hey I need help with implementing a collapsible panel in GPUI"
|
||||
>
|
||||
> **Assistant**: "Sure, I can help with that. Let me see what I can find."
|
||||
>
|
||||
> `tool_calls: ["name": "query_codebase", arguments: "{ 'query': 'GPUI collapsible panel' }"]`
|
||||
>
|
||||
> `result: "['crates/gpui/src/panel.rs:12: impl Panel { ... }', 'crates/gpui/src/panel.rs:20: impl Panel { ... }']"`
|
||||
>
|
||||
> **Assistant**: "Here are some excerpts from the GPUI codebase that might help you."
|
||||
|
||||
This library is designed to facilitate this interaction mode by allowing you to go from `struct` to `tool` with two simple traits, `LanguageModelTool` and `ToolView`.
|
||||
|
||||
## Using the Tool Registry
|
||||
|
||||
```rust
|
||||
let mut tool_registry = ToolRegistry::new();
|
||||
tool_registry
|
||||
.register(WeatherTool { api_client },
|
||||
})
|
||||
.unwrap(); // You can only register one tool per name
|
||||
|
||||
let completion = cx.update(|cx| {
|
||||
CompletionProvider::get(cx).complete(
|
||||
model_name,
|
||||
messages,
|
||||
Vec::new(),
|
||||
1.0,
|
||||
// The definitions get passed directly to OpenAI when you want
|
||||
// the model to be able to call your tool
|
||||
tool_registry.definitions(),
|
||||
)
|
||||
});
|
||||
|
||||
let mut stream = completion?.await?;
|
||||
|
||||
let mut message = AssistantMessage::new();
|
||||
|
||||
while let Some(delta) = stream.next().await {
|
||||
// As messages stream in, you'll get both assistant content
|
||||
if let Some(content) = &delta.content {
|
||||
message
|
||||
.body
|
||||
.update(cx, |message, cx| message.append(&content, cx));
|
||||
}
|
||||
|
||||
// And tool calls!
|
||||
for tool_call_delta in delta.tool_calls {
|
||||
let index = tool_call_delta.index as usize;
|
||||
if index >= message.tool_calls.len() {
|
||||
message.tool_calls.resize_with(index + 1, Default::default);
|
||||
}
|
||||
let tool_call = &mut message.tool_calls[index];
|
||||
|
||||
// Build up an ID
|
||||
if let Some(id) = &tool_call_delta.id {
|
||||
tool_call.id.push_str(id);
|
||||
}
|
||||
|
||||
tool_registry.update_tool_call(
|
||||
tool_call,
|
||||
tool_call_delta.name.as_deref(),
|
||||
tool_call_delta.arguments.as_deref(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once the stream of tokens is complete, you can exexute the tool call by calling `tool_registry.execute_tool_call(tool_call, cx)`, which returns a `Task<Result<()>>`.
|
||||
|
||||
As the tokens stream in and tool calls are executed, your `ToolView` will get updates. Render each tool call by passing that `tool_call` in to `tool_registry.render_tool_call(tool_call, cx)`. The final message for the model can be pulled by calling `self.tool_registry.content_for_tool_call( tool_call, &mut project_context, cx, )`.
|
||||
13
crates/assistant_tooling/src/assistant_tooling.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
mod attachment_registry;
|
||||
mod project_context;
|
||||
mod tool_registry;
|
||||
|
||||
pub use attachment_registry::{
|
||||
AttachmentOutput, AttachmentRegistry, LanguageModelAttachment, SavedUserAttachment,
|
||||
UserAttachment,
|
||||
};
|
||||
pub use project_context::ProjectContext;
|
||||
pub use tool_registry::{
|
||||
LanguageModelTool, SavedToolFunctionCall, ToolFunctionCall, ToolFunctionDefinition,
|
||||
ToolRegistry, ToolView,
|
||||
};
|
||||
234
crates/assistant_tooling/src/attachment_registry.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
use crate::ProjectContext;
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use futures::future::join_all;
|
||||
use gpui::{AnyView, Render, Task, View, WindowContext};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub struct AttachmentRegistry {
|
||||
registered_attachments: HashMap<TypeId, RegisteredAttachment>,
|
||||
}
|
||||
|
||||
pub trait AttachmentOutput {
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
|
||||
}
|
||||
|
||||
pub trait LanguageModelAttachment {
|
||||
type Output: DeserializeOwned + Serialize + 'static;
|
||||
type View: Render + AttachmentOutput;
|
||||
|
||||
fn name(&self) -> Arc<str>;
|
||||
fn run(&self, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
|
||||
fn view(&self, output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
|
||||
}
|
||||
|
||||
/// A collected attachment from running an attachment tool
|
||||
pub struct UserAttachment {
|
||||
pub view: AnyView,
|
||||
name: Arc<str>,
|
||||
serialized_output: Result<Box<RawValue>, String>,
|
||||
generate_fn: fn(AnyView, &mut ProjectContext, cx: &mut WindowContext) -> String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedUserAttachment {
|
||||
name: Arc<str>,
|
||||
serialized_output: Result<Box<RawValue>, String>,
|
||||
}
|
||||
|
||||
/// Internal representation of an attachment tool to allow us to treat them dynamically
|
||||
struct RegisteredAttachment {
|
||||
name: Arc<str>,
|
||||
enabled: AtomicBool,
|
||||
call: Box<dyn Fn(&mut WindowContext) -> Task<Result<UserAttachment>>>,
|
||||
deserialize: Box<dyn Fn(&SavedUserAttachment, &mut WindowContext) -> Result<UserAttachment>>,
|
||||
}
|
||||
|
||||
impl AttachmentRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
registered_attachments: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register<A: LanguageModelAttachment + 'static>(&mut self, attachment: A) {
|
||||
let attachment = Arc::new(attachment);
|
||||
|
||||
let call = Box::new({
|
||||
let attachment = attachment.clone();
|
||||
move |cx: &mut WindowContext| {
|
||||
let result = attachment.run(cx);
|
||||
let attachment = attachment.clone();
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let result: Result<A::Output> = result.await;
|
||||
let serialized_output =
|
||||
result
|
||||
.as_ref()
|
||||
.map_err(ToString::to_string)
|
||||
.and_then(|output| {
|
||||
Ok(RawValue::from_string(
|
||||
serde_json::to_string(output).map_err(|e| e.to_string())?,
|
||||
)
|
||||
.unwrap())
|
||||
});
|
||||
|
||||
let view = cx.update(|cx| attachment.view(result, cx))?;
|
||||
|
||||
Ok(UserAttachment {
|
||||
name: attachment.name(),
|
||||
view: view.into(),
|
||||
generate_fn: generate::<A>,
|
||||
serialized_output,
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let deserialize = Box::new({
|
||||
let attachment = attachment.clone();
|
||||
move |saved_attachment: &SavedUserAttachment, cx: &mut WindowContext| {
|
||||
let serialized_output = saved_attachment.serialized_output.clone();
|
||||
let output = match &serialized_output {
|
||||
Ok(serialized_output) => {
|
||||
Ok(serde_json::from_str::<A::Output>(serialized_output.get())?)
|
||||
}
|
||||
Err(error) => Err(anyhow!("{error}")),
|
||||
};
|
||||
let view = attachment.view(output, cx).into();
|
||||
|
||||
Ok(UserAttachment {
|
||||
name: saved_attachment.name.clone(),
|
||||
view,
|
||||
serialized_output,
|
||||
generate_fn: generate::<A>,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
self.registered_attachments.insert(
|
||||
TypeId::of::<A>(),
|
||||
RegisteredAttachment {
|
||||
name: attachment.name(),
|
||||
call,
|
||||
deserialize,
|
||||
enabled: AtomicBool::new(true),
|
||||
},
|
||||
);
|
||||
return;
|
||||
|
||||
fn generate<T: LanguageModelAttachment>(
|
||||
view: AnyView,
|
||||
project: &mut ProjectContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> String {
|
||||
view.downcast::<T::View>()
|
||||
.unwrap()
|
||||
.update(cx, |view, cx| T::View::generate(view, project, cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_attachment_tool_enabled<A: LanguageModelAttachment + 'static>(
|
||||
&self,
|
||||
is_enabled: bool,
|
||||
) {
|
||||
if let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) {
|
||||
attachment.enabled.store(is_enabled, SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_attachment_tool_enabled<A: LanguageModelAttachment + 'static>(&self) -> bool {
|
||||
if let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) {
|
||||
attachment.enabled.load(SeqCst)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call<A: LanguageModelAttachment + 'static>(
|
||||
&self,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<UserAttachment>> {
|
||||
let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) else {
|
||||
return Task::ready(Err(anyhow!("no attachment tool")));
|
||||
};
|
||||
|
||||
(attachment.call)(cx)
|
||||
}
|
||||
|
||||
pub fn call_all_attachment_tools(
|
||||
self: Arc<Self>,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Task<Result<Vec<UserAttachment>>> {
|
||||
let this = self.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let attachment_tasks = cx.update(|cx| {
|
||||
let mut tasks = Vec::new();
|
||||
for attachment in this
|
||||
.registered_attachments
|
||||
.values()
|
||||
.filter(|attachment| attachment.enabled.load(SeqCst))
|
||||
{
|
||||
tasks.push((attachment.call)(cx))
|
||||
}
|
||||
|
||||
tasks
|
||||
})?;
|
||||
|
||||
let attachments = join_all(attachment_tasks.into_iter()).await;
|
||||
|
||||
Ok(attachments
|
||||
.into_iter()
|
||||
.filter_map(|attachment| attachment.log_err())
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize_user_attachment(
|
||||
&self,
|
||||
user_attachment: &UserAttachment,
|
||||
) -> SavedUserAttachment {
|
||||
SavedUserAttachment {
|
||||
name: user_attachment.name.clone(),
|
||||
serialized_output: user_attachment.serialized_output.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_user_attachment(
|
||||
&self,
|
||||
saved_user_attachment: SavedUserAttachment,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<UserAttachment> {
|
||||
if let Some(registered_attachment) = self
|
||||
.registered_attachments
|
||||
.values()
|
||||
.find(|attachment| attachment.name == saved_user_attachment.name)
|
||||
{
|
||||
(registered_attachment.deserialize)(&saved_user_attachment, cx)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"no attachment tool for name {}",
|
||||
saved_user_attachment.name
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserAttachment {
|
||||
pub fn generate(&self, output: &mut ProjectContext, cx: &mut WindowContext) -> Option<String> {
|
||||
let result = (self.generate_fn)(self.view.clone(), output, cx);
|
||||
if result.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
296
crates/assistant_tooling/src/project_context.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{AppContext, Model, Task, WeakModel};
|
||||
use project::{Fs, Project, ProjectPath, Worktree};
|
||||
use std::{cmp::Ordering, fmt::Write as _, ops::Range, sync::Arc};
|
||||
use sum_tree::TreeMap;
|
||||
|
||||
pub struct ProjectContext {
|
||||
files: TreeMap<ProjectPath, PathState>,
|
||||
project: WeakModel<Project>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum PathState {
|
||||
PathOnly,
|
||||
EntireFile,
|
||||
Excerpts { ranges: Vec<Range<usize>> },
|
||||
}
|
||||
|
||||
impl ProjectContext {
|
||||
pub fn new(project: WeakModel<Project>, fs: Arc<dyn Fs>) -> Self {
|
||||
Self {
|
||||
files: TreeMap::default(),
|
||||
fs,
|
||||
project,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_path(&mut self, project_path: ProjectPath) {
|
||||
if self.files.get(&project_path).is_none() {
|
||||
self.files.insert(project_path, PathState::PathOnly);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_excerpts(&mut self, project_path: ProjectPath, new_ranges: &[Range<usize>]) {
|
||||
let previous_state = self
|
||||
.files
|
||||
.get(&project_path)
|
||||
.unwrap_or(&PathState::PathOnly);
|
||||
|
||||
let mut ranges = match previous_state {
|
||||
PathState::EntireFile => return,
|
||||
PathState::PathOnly => Vec::new(),
|
||||
PathState::Excerpts { ranges } => ranges.to_vec(),
|
||||
};
|
||||
|
||||
for new_range in new_ranges {
|
||||
let ix = ranges.binary_search_by(|probe| {
|
||||
if probe.end < new_range.start {
|
||||
Ordering::Less
|
||||
} else if probe.start > new_range.end {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
|
||||
match ix {
|
||||
Ok(mut ix) => {
|
||||
let existing = &mut ranges[ix];
|
||||
existing.start = existing.start.min(new_range.start);
|
||||
existing.end = existing.end.max(new_range.end);
|
||||
while ix + 1 < ranges.len() && ranges[ix + 1].start <= ranges[ix].end {
|
||||
ranges[ix].end = ranges[ix].end.max(ranges[ix + 1].end);
|
||||
ranges.remove(ix + 1);
|
||||
}
|
||||
while ix > 0 && ranges[ix - 1].end >= ranges[ix].start {
|
||||
ranges[ix].start = ranges[ix].start.min(ranges[ix - 1].start);
|
||||
ranges.remove(ix - 1);
|
||||
ix -= 1;
|
||||
}
|
||||
}
|
||||
Err(ix) => {
|
||||
ranges.insert(ix, new_range.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.files
|
||||
.insert(project_path, PathState::Excerpts { ranges });
|
||||
}
|
||||
|
||||
pub fn add_file(&mut self, project_path: ProjectPath) {
|
||||
self.files.insert(project_path, PathState::EntireFile);
|
||||
}
|
||||
|
||||
pub fn generate_system_message(&self, cx: &mut AppContext) -> Task<Result<String>> {
|
||||
let project = self
|
||||
.project
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("project dropped"));
|
||||
let files = self.files.clone();
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
let project = project?;
|
||||
let mut result = "project structure:\n".to_string();
|
||||
|
||||
let mut last_worktree: Option<Model<Worktree>> = None;
|
||||
for (project_path, path_state) in files.iter() {
|
||||
if let Some(worktree) = &last_worktree {
|
||||
if worktree.read_with(&cx, |tree, _| tree.id())? != project_path.worktree_id {
|
||||
last_worktree = None;
|
||||
}
|
||||
}
|
||||
|
||||
let worktree;
|
||||
if let Some(last_worktree) = &last_worktree {
|
||||
worktree = last_worktree.clone();
|
||||
} else if let Some(tree) = project.read_with(&cx, |project, cx| {
|
||||
project.worktree_for_id(project_path.worktree_id, cx)
|
||||
})? {
|
||||
worktree = tree;
|
||||
last_worktree = Some(worktree.clone());
|
||||
let worktree_name =
|
||||
worktree.read_with(&cx, |tree, _cx| tree.root_name().to_string())?;
|
||||
writeln!(&mut result, "# {}", worktree_name).unwrap();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
let worktree_abs_path = worktree.read_with(&cx, |tree, _cx| tree.abs_path())?;
|
||||
let path = &project_path.path;
|
||||
writeln!(&mut result, "## {}", path.display()).unwrap();
|
||||
|
||||
match path_state {
|
||||
PathState::PathOnly => {}
|
||||
PathState::EntireFile => {
|
||||
let text = fs.load(&worktree_abs_path.join(&path)).await?;
|
||||
writeln!(&mut result, "~~~\n{text}\n~~~").unwrap();
|
||||
}
|
||||
PathState::Excerpts { ranges } => {
|
||||
let text = fs.load(&worktree_abs_path.join(&path)).await?;
|
||||
|
||||
writeln!(&mut result, "~~~").unwrap();
|
||||
|
||||
// Assumption: ranges are in order, not overlapping
|
||||
let mut prev_range_end = 0;
|
||||
for range in ranges {
|
||||
if range.start > prev_range_end {
|
||||
writeln!(&mut result, "...").unwrap();
|
||||
prev_range_end = range.end;
|
||||
}
|
||||
|
||||
let mut start = range.start;
|
||||
let mut end = range.end.min(text.len());
|
||||
while !text.is_char_boundary(start) {
|
||||
start += 1;
|
||||
}
|
||||
while !text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
result.push_str(&text[start..end]);
|
||||
if !result.ends_with('\n') {
|
||||
result.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if prev_range_end < text.len() {
|
||||
writeln!(&mut result, "...").unwrap();
|
||||
}
|
||||
|
||||
writeln!(&mut result, "~~~").unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use project::FakeFs;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
|
||||
use unindent::Unindent as _;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_system_message_generation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let file_3_contents = r#"
|
||||
fn test1() {}
|
||||
fn test2() {}
|
||||
fn test3() {}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/code",
|
||||
json!({
|
||||
"root1": {
|
||||
"lib": {
|
||||
"file1.rs": "mod example;",
|
||||
"file2.rs": "",
|
||||
},
|
||||
"test": {
|
||||
"file3.rs": file_3_contents,
|
||||
}
|
||||
},
|
||||
"root2": {
|
||||
"src": {
|
||||
"main.rs": ""
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
fs.clone(),
|
||||
["/code/root1".as_ref(), "/code/root2".as_ref()],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let worktree_ids = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let mut ax = ProjectContext::new(project.downgrade(), fs);
|
||||
|
||||
ax.add_file(ProjectPath {
|
||||
worktree_id: worktree_ids[0],
|
||||
path: Path::new("lib/file1.rs").into(),
|
||||
});
|
||||
|
||||
let message = cx
|
||||
.update(|cx| ax.generate_system_message(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
r#"
|
||||
project structure:
|
||||
# root1
|
||||
## lib/file1.rs
|
||||
~~~
|
||||
mod example;
|
||||
~~~
|
||||
"#
|
||||
.unindent(),
|
||||
message
|
||||
);
|
||||
|
||||
ax.add_excerpts(
|
||||
ProjectPath {
|
||||
worktree_id: worktree_ids[0],
|
||||
path: Path::new("test/file3.rs").into(),
|
||||
},
|
||||
&[
|
||||
file_3_contents.find("fn test2").unwrap()
|
||||
..file_3_contents.find("fn test3").unwrap(),
|
||||
],
|
||||
);
|
||||
|
||||
let message = cx
|
||||
.update(|cx| ax.generate_system_message(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
r#"
|
||||
project structure:
|
||||
# root1
|
||||
## lib/file1.rs
|
||||
~~~
|
||||
mod example;
|
||||
~~~
|
||||
## test/file3.rs
|
||||
~~~
|
||||
...
|
||||
fn test2() {}
|
||||
...
|
||||
~~~
|
||||
"#
|
||||
.unindent(),
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
526
crates/assistant_tooling/src/tool_registry.rs
Normal file
@@ -0,0 +1,526 @@
|
||||
use crate::ProjectContext;
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{AnyElement, AnyView, IntoElement, Render, Task, View, WindowContext};
|
||||
use repair_json::repair;
|
||||
use schemars::{schema::RootSchema, schema_for, JsonSchema};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
mem,
|
||||
sync::atomic::{AtomicBool, Ordering::SeqCst},
|
||||
};
|
||||
use ui::ViewContext;
|
||||
|
||||
pub struct ToolRegistry {
|
||||
registered_tools: HashMap<String, RegisteredTool>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ToolFunctionCall {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub arguments: String,
|
||||
state: ToolFunctionCallState,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum ToolFunctionCallState {
|
||||
#[default]
|
||||
Initializing,
|
||||
NoSuchTool,
|
||||
KnownTool(Box<dyn InternalToolView>),
|
||||
ExecutedTool(Box<dyn InternalToolView>),
|
||||
}
|
||||
|
||||
trait InternalToolView {
|
||||
fn view(&self) -> AnyView;
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
|
||||
fn try_set_input(&self, input: &str, cx: &mut WindowContext);
|
||||
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>>;
|
||||
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>>;
|
||||
fn deserialize_output(&self, raw_value: &RawValue, cx: &mut WindowContext) -> Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct SavedToolFunctionCall {
|
||||
id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
state: SavedToolFunctionCallState,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
enum SavedToolFunctionCallState {
|
||||
#[default]
|
||||
Initializing,
|
||||
NoSuchTool,
|
||||
KnownTool,
|
||||
ExecutedTool(Box<RawValue>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ToolFunctionDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: RootSchema,
|
||||
}
|
||||
|
||||
pub trait LanguageModelTool {
|
||||
type View: ToolView;
|
||||
|
||||
/// Returns the name of the tool.
|
||||
///
|
||||
/// This name is exposed to the language model to allow the model to pick
|
||||
/// which tools to use. As this name is used to identify the tool within a
|
||||
/// tool registry, it should be unique.
|
||||
fn name(&self) -> String;
|
||||
|
||||
/// Returns the description of the tool.
|
||||
///
|
||||
/// This can be used to _prompt_ the model as to what the tool does.
|
||||
fn description(&self) -> String;
|
||||
|
||||
/// Returns the OpenAI Function definition for the tool, for direct use with OpenAI's API.
|
||||
fn definition(&self) -> ToolFunctionDefinition {
|
||||
let root_schema = schema_for!(<Self::View as ToolView>::Input);
|
||||
|
||||
ToolFunctionDefinition {
|
||||
name: self.name(),
|
||||
description: self.description(),
|
||||
parameters: root_schema,
|
||||
}
|
||||
}
|
||||
|
||||
/// A view of the output of running the tool, for displaying to the user.
|
||||
fn view(&self, cx: &mut WindowContext) -> View<Self::View>;
|
||||
}
|
||||
|
||||
pub trait ToolView: Render {
|
||||
/// The input type that will be passed in to `execute` when the tool is called
|
||||
/// by the language model.
|
||||
type Input: DeserializeOwned + JsonSchema;
|
||||
|
||||
/// The output returned by executing the tool.
|
||||
type SerializedState: DeserializeOwned + Serialize;
|
||||
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut ViewContext<Self>) -> String;
|
||||
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>);
|
||||
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>>;
|
||||
|
||||
fn serialize(&self, cx: &mut ViewContext<Self>) -> Self::SerializedState;
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
output: Self::SerializedState,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
||||
struct RegisteredTool {
|
||||
enabled: AtomicBool,
|
||||
type_id: TypeId,
|
||||
build_view: Box<dyn Fn(&mut WindowContext) -> Box<dyn InternalToolView>>,
|
||||
definition: ToolFunctionDefinition,
|
||||
}
|
||||
|
||||
impl ToolRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
registered_tools: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_tool_enabled<T: 'static + LanguageModelTool>(&self, is_enabled: bool) {
|
||||
for tool in self.registered_tools.values() {
|
||||
if tool.type_id == TypeId::of::<T>() {
|
||||
tool.enabled.store(is_enabled, SeqCst);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_tool_enabled<T: 'static + LanguageModelTool>(&self) -> bool {
|
||||
for tool in self.registered_tools.values() {
|
||||
if tool.type_id == TypeId::of::<T>() {
|
||||
return tool.enabled.load(SeqCst);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn definitions(&self) -> Vec<ToolFunctionDefinition> {
|
||||
self.registered_tools
|
||||
.values()
|
||||
.filter(|tool| tool.enabled.load(SeqCst))
|
||||
.map(|tool| tool.definition.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn update_tool_call(
|
||||
&self,
|
||||
call: &mut ToolFunctionCall,
|
||||
name: Option<&str>,
|
||||
arguments: Option<&str>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if let Some(name) = name {
|
||||
call.name.push_str(name);
|
||||
}
|
||||
if let Some(arguments) = arguments {
|
||||
if call.arguments.is_empty() {
|
||||
if let Some(tool) = self.registered_tools.get(&call.name) {
|
||||
let view = (tool.build_view)(cx);
|
||||
call.state = ToolFunctionCallState::KnownTool(view);
|
||||
} else {
|
||||
call.state = ToolFunctionCallState::NoSuchTool;
|
||||
}
|
||||
}
|
||||
call.arguments.push_str(arguments);
|
||||
|
||||
if let ToolFunctionCallState::KnownTool(view) = &call.state {
|
||||
if let Ok(repaired_arguments) = repair(call.arguments.clone()) {
|
||||
view.try_set_input(&repaired_arguments, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_tool_call(
|
||||
&self,
|
||||
tool_call: &mut ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if let ToolFunctionCallState::KnownTool(view) = mem::take(&mut tool_call.state) {
|
||||
let task = view.execute(cx);
|
||||
tool_call.state = ToolFunctionCallState::ExecutedTool(view);
|
||||
Some(task)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_tool_call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
match &tool_call.state {
|
||||
ToolFunctionCallState::NoSuchTool => {
|
||||
Some(ui::Label::new("No such tool").into_any_element())
|
||||
}
|
||||
ToolFunctionCallState::Initializing => None,
|
||||
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
|
||||
Some(view.view().into_any_element())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content_for_tool_call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
project_context: &mut ProjectContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> String {
|
||||
match &tool_call.state {
|
||||
ToolFunctionCallState::Initializing => String::new(),
|
||||
ToolFunctionCallState::NoSuchTool => {
|
||||
format!("No such tool: {}", tool_call.name)
|
||||
}
|
||||
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
|
||||
view.generate(project_context, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_tool_call(
|
||||
&self,
|
||||
call: &ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<SavedToolFunctionCall> {
|
||||
Ok(SavedToolFunctionCall {
|
||||
id: call.id.clone(),
|
||||
name: call.name.clone(),
|
||||
arguments: call.arguments.clone(),
|
||||
state: match &call.state {
|
||||
ToolFunctionCallState::Initializing => SavedToolFunctionCallState::Initializing,
|
||||
ToolFunctionCallState::NoSuchTool => SavedToolFunctionCallState::NoSuchTool,
|
||||
ToolFunctionCallState::KnownTool(_) => SavedToolFunctionCallState::KnownTool,
|
||||
ToolFunctionCallState::ExecutedTool(view) => {
|
||||
SavedToolFunctionCallState::ExecutedTool(view.serialize_output(cx)?)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deserialize_tool_call(
|
||||
&self,
|
||||
call: &SavedToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<ToolFunctionCall> {
|
||||
let Some(tool) = self.registered_tools.get(&call.name) else {
|
||||
return Err(anyhow!("no such tool {}", call.name));
|
||||
};
|
||||
|
||||
Ok(ToolFunctionCall {
|
||||
id: call.id.clone(),
|
||||
name: call.name.clone(),
|
||||
arguments: call.arguments.clone(),
|
||||
state: match &call.state {
|
||||
SavedToolFunctionCallState::Initializing => ToolFunctionCallState::Initializing,
|
||||
SavedToolFunctionCallState::NoSuchTool => ToolFunctionCallState::NoSuchTool,
|
||||
SavedToolFunctionCallState::KnownTool => {
|
||||
log::error!("Deserialized tool that had not executed");
|
||||
let view = (tool.build_view)(cx);
|
||||
view.try_set_input(&call.arguments, cx);
|
||||
ToolFunctionCallState::KnownTool(view)
|
||||
}
|
||||
SavedToolFunctionCallState::ExecutedTool(output) => {
|
||||
let view = (tool.build_view)(cx);
|
||||
view.try_set_input(&call.arguments, cx);
|
||||
view.deserialize_output(output, cx)?;
|
||||
ToolFunctionCallState::ExecutedTool(view)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register<T: 'static + LanguageModelTool>(&mut self, tool: T) -> Result<()> {
|
||||
let name = tool.name();
|
||||
let registered_tool = RegisteredTool {
|
||||
type_id: TypeId::of::<T>(),
|
||||
definition: tool.definition(),
|
||||
enabled: AtomicBool::new(true),
|
||||
build_view: Box::new(move |cx: &mut WindowContext| Box::new(tool.view(cx))),
|
||||
};
|
||||
|
||||
let previous = self.registered_tools.insert(name.clone(), registered_tool);
|
||||
if previous.is_some() {
|
||||
return Err(anyhow!("already registered a tool with name {}", name));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToolView> InternalToolView for View<T> {
|
||||
fn view(&self) -> AnyView {
|
||||
self.clone().into()
|
||||
}
|
||||
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
|
||||
self.update(cx, |view, cx| view.generate(project, cx))
|
||||
}
|
||||
|
||||
fn try_set_input(&self, input: &str, cx: &mut WindowContext) {
|
||||
if let Ok(input) = serde_json::from_str::<T::Input>(input) {
|
||||
self.update(cx, |view, cx| {
|
||||
view.set_input(input, cx);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>> {
|
||||
self.update(cx, |view, cx| view.execute(cx))
|
||||
}
|
||||
|
||||
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>> {
|
||||
let output = self.update(cx, |view, cx| view.serialize(cx));
|
||||
Ok(RawValue::from_string(serde_json::to_string(&output)?)?)
|
||||
}
|
||||
|
||||
fn deserialize_output(&self, output: &RawValue, cx: &mut WindowContext) -> Result<()> {
|
||||
let state = serde_json::from_str::<T::SerializedState>(output.get())?;
|
||||
self.update(cx, |view, cx| view.deserialize(state, cx))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ToolFunctionDefinition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let schema = serde_json::to_string(&self.parameters).ok();
|
||||
let schema = schema.unwrap_or("None".to_string());
|
||||
write!(f, "Name: {}:\n", self.name)?;
|
||||
write!(f, "Description: {}\n", self.description)?;
|
||||
write!(f, "Parameters: {}", schema)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::{div, prelude::*, Render, TestAppContext};
|
||||
use gpui::{EmptyView, View};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||
struct WeatherQuery {
|
||||
location: String,
|
||||
unit: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct WeatherResult {
|
||||
location: String,
|
||||
temperature: f64,
|
||||
unit: String,
|
||||
}
|
||||
|
||||
struct WeatherView {
|
||||
input: Option<WeatherQuery>,
|
||||
result: Option<WeatherResult>,
|
||||
|
||||
// Fake API call
|
||||
current_weather: WeatherResult,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct WeatherTool {
|
||||
current_weather: WeatherResult,
|
||||
}
|
||||
|
||||
impl WeatherView {
|
||||
fn new(current_weather: WeatherResult) -> Self {
|
||||
Self {
|
||||
input: None,
|
||||
result: None,
|
||||
current_weather,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for WeatherView {
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
match self.result {
|
||||
Some(ref result) => div()
|
||||
.child(format!("temperature: {}", result.temperature))
|
||||
.into_any_element(),
|
||||
None => div().child("Calculating weather...").into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolView for WeatherView {
|
||||
type Input = WeatherQuery;
|
||||
|
||||
type SerializedState = WeatherResult;
|
||||
|
||||
fn generate(&self, _output: &mut ProjectContext, _cx: &mut ViewContext<Self>) -> String {
|
||||
serde_json::to_string(&self.result).unwrap()
|
||||
}
|
||||
|
||||
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
|
||||
self.input = Some(input);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn execute(&mut self, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
let input = self.input.as_ref().unwrap();
|
||||
|
||||
let _location = input.location.clone();
|
||||
let _unit = input.unit.clone();
|
||||
|
||||
let weather = self.current_weather.clone();
|
||||
|
||||
self.result = Some(weather);
|
||||
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
|
||||
self.current_weather.clone()
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
output: Self::SerializedState,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
self.current_weather = output;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for WeatherTool {
|
||||
type View = WeatherView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"get_current_weather".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Fetches the current weather for a given location.".to_string()
|
||||
}
|
||||
|
||||
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
|
||||
cx.new_view(|_cx| WeatherView::new(self.current_weather.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_openai_weather_example(cx: &mut TestAppContext) {
|
||||
let (_, cx) = cx.add_window_view(|_cx| EmptyView);
|
||||
|
||||
let mut registry = ToolRegistry::new();
|
||||
registry
|
||||
.register(WeatherTool {
|
||||
current_weather: WeatherResult {
|
||||
location: "San Francisco".to_string(),
|
||||
temperature: 21.0,
|
||||
unit: "Celsius".to_string(),
|
||||
},
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let definitions = registry.definitions();
|
||||
assert_eq!(
|
||||
definitions,
|
||||
[ToolFunctionDefinition {
|
||||
name: "get_current_weather".to_string(),
|
||||
description: "Fetches the current weather for a given location.".to_string(),
|
||||
parameters: serde_json::from_value(json!({
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "WeatherQuery",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["location", "unit"]
|
||||
}))
|
||||
.unwrap(),
|
||||
}]
|
||||
);
|
||||
|
||||
let mut call = ToolFunctionCall {
|
||||
id: "the-id".to_string(),
|
||||
name: "get_cur".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let task = cx.update(|cx| {
|
||||
registry.update_tool_call(
|
||||
&mut call,
|
||||
Some("rent_weather"),
|
||||
Some(r#"{"location": "San Francisco","#),
|
||||
cx,
|
||||
);
|
||||
registry.update_tool_call(&mut call, None, Some(r#" "unit": "Celsius"}"#), cx);
|
||||
registry.execute_tool_call(&mut call, cx).unwrap()
|
||||
});
|
||||
task.await.unwrap();
|
||||
|
||||
match &call.state {
|
||||
ToolFunctionCallState::ExecutedTool(_view) => {}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -493,7 +493,7 @@ impl Room {
|
||||
// we leave the room and return an error.
|
||||
if let Some(this) = this.upgrade() {
|
||||
log::info!("reconnection failed, leaving room");
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx))?.await?;
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx))?;
|
||||
}
|
||||
Err(anyhow!(
|
||||
"can't reconnect to room: client failed to re-establish connection"
|
||||
@@ -942,7 +942,7 @@ impl Room {
|
||||
this.pending_room_update.take();
|
||||
if this.should_leave() {
|
||||
log::info!("room is empty, leaving");
|
||||
let _ = this.leave(cx).detach();
|
||||
let _ = this.leave(cx);
|
||||
}
|
||||
|
||||
this.user_store.update(cx, |user_store, cx| {
|
||||
|
||||
@@ -7,9 +7,8 @@ pub mod user;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use async_tungstenite::tungstenite::{
|
||||
client::IntoClientRequest,
|
||||
error::Error as WebsocketError,
|
||||
http::{HeaderValue, Request, StatusCode},
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use clock::SystemClock;
|
||||
use collections::HashMap;
|
||||
@@ -236,8 +235,6 @@ pub enum EstablishConnectionError {
|
||||
#[error("{0}")]
|
||||
Http(#[from] http_client::Error),
|
||||
#[error("{0}")]
|
||||
InvalidHeaderValue(#[from] async_tungstenite::tungstenite::http::header::InvalidHeaderValue),
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("{0}")]
|
||||
Websocket(#[from] async_tungstenite::tungstenite::http::Error),
|
||||
@@ -1162,24 +1159,19 @@ impl Client {
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
let request = Request::builder()
|
||||
.header("Authorization", credentials.authorization_header())
|
||||
.header("x-zed-protocol-version", rpc::PROTOCOL_VERSION)
|
||||
.header("x-zed-app-version", app_version)
|
||||
.header(
|
||||
"x-zed-release-channel",
|
||||
release_channel.map(|r| r.dev_name()).unwrap_or("unknown"),
|
||||
);
|
||||
|
||||
let http = self.http.clone();
|
||||
let credentials = credentials.clone();
|
||||
let rpc_url = self.rpc_url(http, release_channel);
|
||||
cx.background_executor().spawn(async move {
|
||||
use HttpOrHttps::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum HttpOrHttps {
|
||||
Http,
|
||||
Https,
|
||||
}
|
||||
|
||||
let mut rpc_url = rpc_url.await?;
|
||||
let url_scheme = match rpc_url.scheme() {
|
||||
"https" => Https,
|
||||
"http" => Http,
|
||||
_ => Err(anyhow!("invalid rpc url: {}", rpc_url))?,
|
||||
};
|
||||
let rpc_host = rpc_url
|
||||
.host_str()
|
||||
.zip(rpc_url.port_or_known_default())
|
||||
@@ -1188,37 +1180,10 @@ impl Client {
|
||||
|
||||
log::info!("connected to rpc endpoint {}", rpc_url);
|
||||
|
||||
rpc_url
|
||||
.set_scheme(match url_scheme {
|
||||
Https => "wss",
|
||||
Http => "ws",
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// We call `into_client_request` to let `tungstenite` construct the WebSocket request
|
||||
// for us from the RPC URL.
|
||||
//
|
||||
// Among other things, it will generate and set a `Sec-WebSocket-Key` header for us.
|
||||
let mut request = rpc_url.into_client_request()?;
|
||||
|
||||
// We then modify the request to add our desired headers.
|
||||
let request_headers = request.headers_mut();
|
||||
request_headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&credentials.authorization_header())?,
|
||||
);
|
||||
request_headers.insert(
|
||||
"x-zed-protocol-version",
|
||||
HeaderValue::from_str(&rpc::PROTOCOL_VERSION.to_string())?,
|
||||
);
|
||||
request_headers.insert("x-zed-app-version", HeaderValue::from_str(&app_version)?);
|
||||
request_headers.insert(
|
||||
"x-zed-release-channel",
|
||||
HeaderValue::from_str(&release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?,
|
||||
);
|
||||
|
||||
match url_scheme {
|
||||
Https => {
|
||||
match rpc_url.scheme() {
|
||||
"https" => {
|
||||
rpc_url.set_scheme("wss").unwrap();
|
||||
let request = request.uri(rpc_url.as_str()).body(())?;
|
||||
let (stream, _) =
|
||||
async_tungstenite::async_std::client_async_tls(request, stream).await?;
|
||||
Ok(Connection::new(
|
||||
@@ -1227,7 +1192,9 @@ impl Client {
|
||||
.sink_map_err(|error| anyhow!(error)),
|
||||
))
|
||||
}
|
||||
Http => {
|
||||
"http" => {
|
||||
rpc_url.set_scheme("ws").unwrap();
|
||||
let request = request.uri(rpc_url.as_str()).body(())?;
|
||||
let (stream, _) = async_tungstenite::client_async(request, stream).await?;
|
||||
Ok(Connection::new(
|
||||
stream
|
||||
@@ -1235,6 +1202,7 @@ impl Client {
|
||||
.sink_map_err(|error| anyhow!(error)),
|
||||
))
|
||||
}
|
||||
_ => Err(anyhow!("invalid rpc url: {}", rpc_url))?,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, AssistantKind, CallEvent, CpuEvent, EditEvent,
|
||||
EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent,
|
||||
MemoryEvent, ReplEvent, SettingEvent,
|
||||
MemoryEvent, SettingEvent,
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -531,21 +531,6 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_repl_event(
|
||||
self: &Arc<Self>,
|
||||
kernel_language: String,
|
||||
kernel_status: String,
|
||||
repl_session_id: String,
|
||||
) {
|
||||
let event = Event::Repl(ReplEvent {
|
||||
kernel_language,
|
||||
kernel_status,
|
||||
repl_session_id,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
fn report_event(self: &Arc<Self>, event: Event) {
|
||||
let mut state = self.state.lock();
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ chrono.workspace = true
|
||||
clock.workspace = true
|
||||
clickhouse.workspace = true
|
||||
collections.workspace = true
|
||||
dashmap.workspace = true
|
||||
dashmap = "5.4"
|
||||
envy = "0.4.2"
|
||||
futures.workspace = true
|
||||
google_ai.workspace = true
|
||||
@@ -47,7 +47,7 @@ prost.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
rpc.workspace = true
|
||||
scrypt = "0.11"
|
||||
scrypt = "0.7"
|
||||
sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
||||
semantic_version.workspace = true
|
||||
semver.workspace = true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use rpc::proto;
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub fn language_model_request_to_open_ai(
|
||||
request: proto::CompleteWithLanguageModel,
|
||||
@@ -20,7 +21,25 @@ pub fn language_model_request_to_open_ai(
|
||||
proto::LanguageModelRole::LanguageModelAssistant => {
|
||||
open_ai::RequestMessage::Assistant {
|
||||
content: Some(message.content),
|
||||
tool_calls: Vec::new(),
|
||||
tool_calls: message
|
||||
.tool_calls
|
||||
.into_iter()
|
||||
.filter_map(|call| {
|
||||
Some(open_ai::ToolCall {
|
||||
id: call.id,
|
||||
content: match call.variant? {
|
||||
proto::tool_call::Variant::Function(f) => {
|
||||
open_ai::ToolCallContent::Function {
|
||||
function: open_ai::FunctionContent {
|
||||
name: f.name,
|
||||
arguments: f.arguments,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
proto::LanguageModelRole::LanguageModelSystem => {
|
||||
@@ -28,6 +47,12 @@ pub fn language_model_request_to_open_ai(
|
||||
content: message.content,
|
||||
}
|
||||
}
|
||||
proto::LanguageModelRole::LanguageModelTool => open_ai::RequestMessage::Tool {
|
||||
tool_call_id: message
|
||||
.tool_call_id
|
||||
.ok_or_else(|| anyhow!("tool message is missing tool call id"))?,
|
||||
content: message.content,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(openai_message)
|
||||
@@ -36,8 +61,32 @@ pub fn language_model_request_to_open_ai(
|
||||
stream: true,
|
||||
stop: request.stop,
|
||||
temperature: request.temperature,
|
||||
tool_choice: None,
|
||||
tools: Vec::new(),
|
||||
tools: request
|
||||
.tools
|
||||
.into_iter()
|
||||
.filter_map(|tool| {
|
||||
Some(match tool.variant? {
|
||||
proto::chat_completion_tool::Variant::Function(f) => {
|
||||
open_ai::ToolDefinition::Function {
|
||||
function: open_ai::FunctionDefinition {
|
||||
name: f.name,
|
||||
description: f.description,
|
||||
parameters: if let Some(params) = &f.parameters {
|
||||
Some(
|
||||
serde_json::from_str(params)
|
||||
.context("failed to deserialize tool parameters")
|
||||
.log_err()?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
tool_choice: request.tool_choice,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,6 +118,9 @@ pub fn language_model_request_message_to_google_ai(
|
||||
proto::LanguageModelRole::LanguageModelUser => google_ai::Role::User,
|
||||
proto::LanguageModelRole::LanguageModelAssistant => google_ai::Role::Model,
|
||||
proto::LanguageModelRole::LanguageModelSystem => google_ai::Role::User,
|
||||
proto::LanguageModelRole::LanguageModelTool => {
|
||||
Err(anyhow!("we don't handle tool calls with google ai yet"))?
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod contributors;
|
||||
pub mod events;
|
||||
pub mod extensions;
|
||||
pub mod ips_file;
|
||||
@@ -6,13 +5,13 @@ pub mod slack;
|
||||
|
||||
use crate::{
|
||||
auth,
|
||||
db::{User, UserId},
|
||||
db::{ContributorSelector, User, UserId},
|
||||
rpc, AppState, Error, Result,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Path, Query},
|
||||
extract::{self, Path, Query},
|
||||
http::{self, Request, StatusCode},
|
||||
middleware::{self, Next},
|
||||
response::IntoResponse,
|
||||
@@ -20,6 +19,7 @@ use axum::{
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use axum_extra::response::ErasedJson;
|
||||
use chrono::SecondsFormat;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tower::ServiceBuilder;
|
||||
@@ -31,7 +31,8 @@ pub fn routes(rpc_server: Option<Arc<rpc::Server>>, state: Arc<AppState>) -> Rou
|
||||
.route("/user", get(get_authenticated_user))
|
||||
.route("/users/:id/access_tokens", post(create_access_token))
|
||||
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
|
||||
.merge(contributors::router())
|
||||
.route("/contributors", get(get_contributors).post(add_contributor))
|
||||
.route("/contributor", get(check_is_contributor))
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(Extension(state))
|
||||
@@ -125,6 +126,66 @@ async fn get_rpc_server_snapshot(
|
||||
Ok(ErasedJson::pretty(rpc_server.snapshot().await))
|
||||
}
|
||||
|
||||
async fn get_contributors(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<String>>> {
|
||||
Ok(Json(app.db.get_contributors().await?))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CheckIsContributorParams {
|
||||
github_user_id: Option<i32>,
|
||||
github_login: Option<String>,
|
||||
}
|
||||
|
||||
impl CheckIsContributorParams {
|
||||
fn as_contributor_selector(self) -> Result<ContributorSelector> {
|
||||
if let Some(github_user_id) = self.github_user_id {
|
||||
return Ok(ContributorSelector::GitHubUserId { github_user_id });
|
||||
}
|
||||
|
||||
if let Some(github_login) = self.github_login {
|
||||
return Ok(ContributorSelector::GitHubLogin { github_login });
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"must be one of `github_user_id` or `github_login`."
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CheckIsContributorResponse {
|
||||
signed_at: Option<String>,
|
||||
}
|
||||
|
||||
async fn check_is_contributor(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Query(params): Query<CheckIsContributorParams>,
|
||||
) -> Result<Json<CheckIsContributorResponse>> {
|
||||
let params = params.as_contributor_selector()?;
|
||||
Ok(Json(CheckIsContributorResponse {
|
||||
signed_at: app
|
||||
.db
|
||||
.get_contributor_sign_timestamp(¶ms)
|
||||
.await?
|
||||
.map(|ts| ts.and_utc().to_rfc3339_opts(SecondsFormat::Millis, true)),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn add_contributor(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
extract::Json(params): extract::Json<AuthenticatedUserParams>,
|
||||
) -> Result<()> {
|
||||
let initial_channel_id = app.config.auto_join_channel_id;
|
||||
app.db
|
||||
.add_contributor(
|
||||
¶ms.github_login,
|
||||
params.github_user_id,
|
||||
params.github_email.as_deref(),
|
||||
initial_channel_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateAccessTokenQueryParams {
|
||||
public_key: String,
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
extract::{self, Query},
|
||||
routing::get,
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use chrono::{NaiveDateTime, SecondsFormat};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::AuthenticatedUserParams;
|
||||
use crate::db::ContributorSelector;
|
||||
use crate::{AppState, Result};
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/contributors", get(get_contributors).post(add_contributor))
|
||||
.route("/contributor", get(check_is_contributor))
|
||||
}
|
||||
|
||||
async fn get_contributors(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<String>>> {
|
||||
Ok(Json(app.db.get_contributors().await?))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CheckIsContributorParams {
|
||||
github_user_id: Option<i32>,
|
||||
github_login: Option<String>,
|
||||
}
|
||||
|
||||
impl CheckIsContributorParams {
|
||||
fn as_contributor_selector(self) -> Result<ContributorSelector> {
|
||||
if let Some(github_user_id) = self.github_user_id {
|
||||
return Ok(ContributorSelector::GitHubUserId { github_user_id });
|
||||
}
|
||||
|
||||
if let Some(github_login) = self.github_login {
|
||||
return Ok(ContributorSelector::GitHubLogin { github_login });
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"must be one of `github_user_id` or `github_login`."
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CheckIsContributorResponse {
|
||||
signed_at: Option<String>,
|
||||
}
|
||||
|
||||
async fn check_is_contributor(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Query(params): Query<CheckIsContributorParams>,
|
||||
) -> Result<Json<CheckIsContributorResponse>> {
|
||||
let params = params.as_contributor_selector()?;
|
||||
|
||||
if RenovateBot::is_renovate_bot(¶ms) {
|
||||
return Ok(Json(CheckIsContributorResponse {
|
||||
signed_at: Some(
|
||||
RenovateBot::created_at()
|
||||
.and_utc()
|
||||
.to_rfc3339_opts(SecondsFormat::Millis, true),
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(Json(CheckIsContributorResponse {
|
||||
signed_at: app
|
||||
.db
|
||||
.get_contributor_sign_timestamp(¶ms)
|
||||
.await?
|
||||
.map(|ts| ts.and_utc().to_rfc3339_opts(SecondsFormat::Millis, true)),
|
||||
}))
|
||||
}
|
||||
|
||||
/// The Renovate bot GitHub user (`renovate[bot]`).
|
||||
///
|
||||
/// https://api.github.com/users/renovate[bot]
|
||||
struct RenovateBot;
|
||||
|
||||
impl RenovateBot {
|
||||
const LOGIN: &'static str = "renovate[bot]";
|
||||
const USER_ID: i32 = 29139614;
|
||||
|
||||
/// Returns the `created_at` timestamp for the Renovate bot user.
|
||||
fn created_at() -> &'static NaiveDateTime {
|
||||
static CREATED_AT: OnceLock<NaiveDateTime> = OnceLock::new();
|
||||
CREATED_AT.get_or_init(|| {
|
||||
chrono::DateTime::parse_from_rfc3339("2017-06-02T07:04:12Z")
|
||||
.expect("failed to parse 'created_at' for 'renovate[bot]'")
|
||||
.naive_utc()
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether the given contributor selector corresponds to the Renovate bot user.
|
||||
fn is_renovate_bot(contributor: &ContributorSelector) -> bool {
|
||||
match contributor {
|
||||
ContributorSelector::GitHubLogin { github_login } => github_login == Self::LOGIN,
|
||||
ContributorSelector::GitHubUserId { github_user_id } => {
|
||||
github_user_id == &Self::USER_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_contributor(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
extract::Json(params): extract::Json<AuthenticatedUserParams>,
|
||||
) -> Result<()> {
|
||||
let initial_channel_id = app.config.auto_join_channel_id;
|
||||
app.db
|
||||
.add_contributor(
|
||||
¶ms.github_login,
|
||||
params.github_user_id,
|
||||
params.github_email.as_deref(),
|
||||
initial_channel_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -16,7 +16,7 @@ use sha2::{Digest, Sha256};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, ReplEvent,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent,
|
||||
SettingEvent,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
@@ -518,13 +518,6 @@ pub async fn post_events(
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
Event::Repl(event) => to_upload.repl_events.push(ReplEventRow::from_event(
|
||||
event.clone(),
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,7 +542,6 @@ struct ToUpload {
|
||||
extension_events: Vec<ExtensionEventRow>,
|
||||
edit_events: Vec<EditEventRow>,
|
||||
action_events: Vec<ActionEventRow>,
|
||||
repl_events: Vec<ReplEventRow>,
|
||||
}
|
||||
|
||||
impl ToUpload {
|
||||
@@ -625,11 +617,6 @@ impl ToUpload {
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table '{ACTION_EVENTS_TABLE}'"))?;
|
||||
|
||||
const REPL_EVENTS_TABLE: &str = "repl_events";
|
||||
Self::upload_to_table(REPL_EVENTS_TABLE, &self.repl_events, clickhouse_client)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table '{REPL_EVENTS_TABLE}'"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -638,24 +625,22 @@ impl ToUpload {
|
||||
rows: &[T],
|
||||
clickhouse_client: &clickhouse::Client,
|
||||
) -> anyhow::Result<()> {
|
||||
if rows.is_empty() {
|
||||
return Ok(());
|
||||
if !rows.is_empty() {
|
||||
let mut insert = clickhouse_client.insert(table)?;
|
||||
|
||||
for event in rows {
|
||||
insert.write(event).await?;
|
||||
}
|
||||
|
||||
insert.end().await?;
|
||||
|
||||
let event_count = rows.len();
|
||||
log::info!(
|
||||
"wrote {event_count} {event_specifier} to '{table}'",
|
||||
event_specifier = if event_count == 1 { "event" } else { "events" }
|
||||
);
|
||||
}
|
||||
|
||||
let mut insert = clickhouse_client.insert(table)?;
|
||||
|
||||
for event in rows {
|
||||
insert.write(event).await?;
|
||||
}
|
||||
|
||||
insert.end().await?;
|
||||
|
||||
let event_count = rows.len();
|
||||
log::info!(
|
||||
"wrote {event_count} {event_specifier} to '{table}'",
|
||||
event_specifier = if event_count == 1 { "event" } else { "events" }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1204,62 +1189,6 @@ impl ExtensionEventRow {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, clickhouse::Row)]
|
||||
pub struct ReplEventRow {
|
||||
// AppInfoBase
|
||||
app_version: String,
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
session_id: Option<String>,
|
||||
is_staff: Option<bool>,
|
||||
time: i64,
|
||||
|
||||
// ReplEventRow
|
||||
kernel_language: String,
|
||||
kernel_status: String,
|
||||
repl_session_id: String,
|
||||
}
|
||||
|
||||
impl ReplEventRow {
|
||||
fn from_event(
|
||||
event: ReplEvent,
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
|
||||
|
||||
Self {
|
||||
app_version: body.app_version.clone(),
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
time: time.timestamp_millis(),
|
||||
kernel_language: event.kernel_language,
|
||||
kernel_status: event.kernel_status,
|
||||
repl_session_id: event.repl_session_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, clickhouse::Row)]
|
||||
pub struct EditEventRow {
|
||||
// AppInfoBase
|
||||
|
||||
@@ -9,7 +9,6 @@ use axum::{
|
||||
middleware::Next,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use base64::prelude::*;
|
||||
use prometheus::{exponential_buckets, register_histogram, Histogram};
|
||||
pub use rpc::auth::random_token;
|
||||
use scrypt::{
|
||||
@@ -156,7 +155,10 @@ pub async fn create_access_token(
|
||||
/// protection.
|
||||
pub fn hash_access_token(token: &str) -> String {
|
||||
let digest = sha2::Sha256::digest(token);
|
||||
format!("$sha256${}", BASE64_URL_SAFE.encode(digest))
|
||||
format!(
|
||||
"$sha256${}",
|
||||
base64::encode_config(digest, base64::URL_SAFE)
|
||||
)
|
||||
}
|
||||
|
||||
/// Encrypts the given access token with the given public key to avoid leaking it on the way
|
||||
@@ -400,16 +402,15 @@ mod test {
|
||||
fn previous_hash_access_token(token: &str) -> Result<String> {
|
||||
// Avoid slow hashing in debug mode.
|
||||
let params = if cfg!(debug_assertions) {
|
||||
scrypt::Params::new(1, 1, 1, scrypt::Params::RECOMMENDED_LEN).unwrap()
|
||||
scrypt::Params::new(1, 1, 1).unwrap()
|
||||
} else {
|
||||
scrypt::Params::new(14, 8, 1, scrypt::Params::RECOMMENDED_LEN).unwrap()
|
||||
scrypt::Params::new(14, 8, 1).unwrap()
|
||||
};
|
||||
|
||||
Ok(Scrypt
|
||||
.hash_password_customized(
|
||||
.hash_password(
|
||||
token.as_bytes(),
|
||||
None,
|
||||
None,
|
||||
params,
|
||||
&SaltString::generate(thread_rng()),
|
||||
)
|
||||
|
||||
@@ -153,7 +153,7 @@ async fn main() -> Result<()> {
|
||||
let signal = async move {
|
||||
// todo(windows):
|
||||
// `ctrl_close` does not work well, because tokio's signal handler always returns soon,
|
||||
// but system terminates the application soon after returning CTRL+CLOSE handler.
|
||||
// but system termiates the application soon after returning CTRL+CLOSE handler.
|
||||
// So we should implement blocking handler to treat CTRL+CLOSE signal.
|
||||
let mut ctrl_break = tokio::signal::windows::ctrl_break()
|
||||
.expect("failed to listen for interrupt signal");
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
executor::Executor,
|
||||
AppState, Error, RateLimit, RateLimiter, Result,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Context as _};
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use async_tungstenite::tungstenite::{
|
||||
protocol::CloseFrame as TungsteniteCloseFrame, Message as TungsteniteMessage,
|
||||
};
|
||||
@@ -1392,7 +1392,7 @@ pub async fn handle_websocket_request(
|
||||
let socket = socket
|
||||
.map_ok(to_tungstenite_message)
|
||||
.err_into()
|
||||
.with(|message| async move { to_axum_message(message) });
|
||||
.with(|message| async move { Ok(to_axum_message(message)) });
|
||||
let connection = Connection::new(Box::pin(socket));
|
||||
async move {
|
||||
server
|
||||
@@ -4597,19 +4597,37 @@ async fn complete_with_open_ai(
|
||||
.map(|choice| proto::LanguageModelChoiceDelta {
|
||||
index: choice.index,
|
||||
delta: Some(proto::LanguageModelResponseMessage {
|
||||
role: choice.delta.role.and_then(|role| match role {
|
||||
open_ai::Role::User => {
|
||||
Some(LanguageModelRole::LanguageModelUser as i32)
|
||||
}
|
||||
open_ai::Role::Assistant => {
|
||||
Some(LanguageModelRole::LanguageModelAssistant as i32)
|
||||
}
|
||||
open_ai::Role::System => {
|
||||
Some(LanguageModelRole::LanguageModelSystem as i32)
|
||||
}
|
||||
_ => None,
|
||||
}),
|
||||
role: choice.delta.role.map(|role| match role {
|
||||
open_ai::Role::User => LanguageModelRole::LanguageModelUser,
|
||||
open_ai::Role::Assistant => LanguageModelRole::LanguageModelAssistant,
|
||||
open_ai::Role::System => LanguageModelRole::LanguageModelSystem,
|
||||
open_ai::Role::Tool => LanguageModelRole::LanguageModelTool,
|
||||
} as i32),
|
||||
content: choice.delta.content,
|
||||
tool_calls: choice
|
||||
.delta
|
||||
.tool_calls
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|delta| proto::ToolCallDelta {
|
||||
index: delta.index as u32,
|
||||
id: delta.id,
|
||||
variant: match delta.function {
|
||||
Some(function) => {
|
||||
let name = function.name;
|
||||
let arguments = function.arguments;
|
||||
|
||||
Some(proto::tool_call_delta::Variant::Function(
|
||||
proto::tool_call_delta::FunctionCallDelta {
|
||||
name,
|
||||
arguments,
|
||||
},
|
||||
))
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
finish_reason: choice.finish_reason,
|
||||
})
|
||||
@@ -4661,6 +4679,8 @@ async fn complete_with_google_ai(
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
// Tool calls are not supported for Google
|
||||
tool_calls: Vec::new(),
|
||||
}),
|
||||
finish_reason: candidate.finish_reason.map(|reason| reason.to_string()),
|
||||
})
|
||||
@@ -4677,6 +4697,8 @@ async fn complete_with_anthropic(
|
||||
session: UserSession,
|
||||
api_key: Arc<str>,
|
||||
) -> Result<()> {
|
||||
let model = anthropic::Model::from_id(&request.model)?;
|
||||
|
||||
let mut system_message = String::new();
|
||||
let messages = request
|
||||
.messages
|
||||
@@ -4701,6 +4723,8 @@ async fn complete_with_anthropic(
|
||||
|
||||
None
|
||||
}
|
||||
// We don't yet support tool calls for Anthropic
|
||||
LanguageModelRole::LanguageModelTool => None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -4710,7 +4734,7 @@ async fn complete_with_anthropic(
|
||||
anthropic::ANTHROPIC_API_URL,
|
||||
&api_key,
|
||||
anthropic::Request {
|
||||
model: request.model,
|
||||
model,
|
||||
messages,
|
||||
stream: true,
|
||||
system: system_message,
|
||||
@@ -4745,6 +4769,7 @@ async fn complete_with_anthropic(
|
||||
delta: Some(proto::LanguageModelResponseMessage {
|
||||
role: Some(current_role as i32),
|
||||
content: Some(text),
|
||||
tool_calls: Vec::new(),
|
||||
}),
|
||||
finish_reason: None,
|
||||
}],
|
||||
@@ -4761,6 +4786,7 @@ async fn complete_with_anthropic(
|
||||
delta: Some(proto::LanguageModelResponseMessage {
|
||||
role: Some(current_role as i32),
|
||||
content: Some(text),
|
||||
tool_calls: Vec::new(),
|
||||
}),
|
||||
finish_reason: None,
|
||||
}],
|
||||
@@ -5128,8 +5154,8 @@ async fn get_private_user_info(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn to_axum_message(message: TungsteniteMessage) -> anyhow::Result<AxumMessage> {
|
||||
let message = match message {
|
||||
fn to_axum_message(message: TungsteniteMessage) -> AxumMessage {
|
||||
match message {
|
||||
TungsteniteMessage::Text(payload) => AxumMessage::Text(payload),
|
||||
TungsteniteMessage::Binary(payload) => AxumMessage::Binary(payload),
|
||||
TungsteniteMessage::Ping(payload) => AxumMessage::Ping(payload),
|
||||
@@ -5138,20 +5164,7 @@ fn to_axum_message(message: TungsteniteMessage) -> anyhow::Result<AxumMessage> {
|
||||
code: frame.code.into(),
|
||||
reason: frame.reason,
|
||||
})),
|
||||
// We should never receive a frame while reading the message, according
|
||||
// to the `tungstenite` maintainers:
|
||||
//
|
||||
// > It cannot occur when you read messages from the WebSocket, but it
|
||||
// > can be used when you want to send the raw frames (e.g. you want to
|
||||
// > send the frames to the WebSocket without composing the full message first).
|
||||
// >
|
||||
// > — https://github.com/snapview/tungstenite-rs/issues/268
|
||||
TungsteniteMessage::Frame(_) => {
|
||||
bail!("received an unexpected frame while reading the message")
|
||||
}
|
||||
};
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_tungstenite_message(message: AxumMessage) -> TungsteniteMessage {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::reversed_empty_ranges)]
|
||||
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
|
||||
use call::{ActiveCall, ParticipantLocation};
|
||||
use client::ChannelId;
|
||||
|
||||
@@ -533,7 +533,6 @@ impl Render for MessageEditor {
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: TextSize::Small.rems(cx).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
|
||||
@@ -2190,7 +2190,6 @@ impl CollabPanel {
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, StreamExt};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::{AppContext, Global, Model, ModelContext, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
@@ -27,7 +27,7 @@ pub struct LanguageModelCompletionProvider {
|
||||
const MAX_CONCURRENT_COMPLETION_REQUESTS: usize = 4;
|
||||
|
||||
pub struct LanguageModelCompletionResponse {
|
||||
inner: BoxStream<'static, Result<String>>,
|
||||
pub inner: BoxStream<'static, Result<String>>,
|
||||
_lock: SemaphoreGuardArc,
|
||||
}
|
||||
|
||||
@@ -143,11 +143,11 @@ impl LanguageModelCompletionProvider {
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> Option<BoxFuture<'static, Result<usize>>> {
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
if let Some(model) = self.active_model() {
|
||||
Some(model.count_tokens(request, cx))
|
||||
model.count_tokens(request, cx)
|
||||
} else {
|
||||
None
|
||||
std::future::ready(Err(anyhow!("No active model set"))).boxed()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -691,7 +691,7 @@ impl Copilot {
|
||||
{
|
||||
match event {
|
||||
language::Event::Edited => {
|
||||
drop(registered_buffer.report_changes(&buffer, cx));
|
||||
let _ = registered_buffer.report_changes(&buffer, cx);
|
||||
}
|
||||
language::Event::Saved => {
|
||||
server
|
||||
|
||||
@@ -333,7 +333,7 @@ mod tests {
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
drop(handle_completion_request(
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
@@ -341,7 +341,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec!["completion_a", "completion_b"],
|
||||
));
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
@@ -375,7 +375,7 @@ mod tests {
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
drop(handle_completion_request(
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
@@ -383,7 +383,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec![],
|
||||
));
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
@@ -408,7 +408,7 @@ mod tests {
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
drop(handle_completion_request(
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
@@ -416,7 +416,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec!["completion_a", "completion_b"],
|
||||
));
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
@@ -590,7 +590,7 @@ mod tests {
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
drop(handle_completion_request(
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
@@ -598,7 +598,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec![],
|
||||
));
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
@@ -632,7 +632,7 @@ mod tests {
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
drop(handle_completion_request(
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
@@ -640,7 +640,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec![],
|
||||
));
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
@@ -889,7 +889,7 @@ mod tests {
|
||||
three
|
||||
"});
|
||||
|
||||
drop(handle_completion_request(
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one
|
||||
@@ -897,7 +897,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec!["completion_a", "completion_b"],
|
||||
));
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
@@ -917,7 +917,7 @@ mod tests {
|
||||
});
|
||||
|
||||
cx.simulate_keystroke("o");
|
||||
drop(handle_completion_request(
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one
|
||||
@@ -925,7 +925,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec!["completion_a_2", "completion_b_2"],
|
||||
));
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
@@ -944,7 +944,7 @@ mod tests {
|
||||
});
|
||||
|
||||
cx.simulate_keystroke(".");
|
||||
drop(handle_completion_request(
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one
|
||||
@@ -952,7 +952,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec!["something_else()"],
|
||||
));
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
|
||||
27
crates/dap/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "dap"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-std = "1.12.0"
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types" }
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
smol.workspace = true
|
||||
task.workspace = true
|
||||
util.workspace = true
|
||||
646
crates/dap/src/client.rs
Normal file
@@ -0,0 +1,646 @@
|
||||
use crate::transport::{Events, Payload, Response, Transport};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
use dap_types::{
|
||||
requests::{
|
||||
Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Request,
|
||||
Restart, RunInTerminal, SetBreakpoints, StartDebugging, StepBack, StepIn, StepOut,
|
||||
},
|
||||
AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, ContinueResponse,
|
||||
DisconnectArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments,
|
||||
NextArguments, PauseArguments, RestartArguments, RunInTerminalRequestArguments,
|
||||
RunInTerminalResponse, Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source,
|
||||
SourceBreakpoint, StackFrame, StartDebuggingRequestArguments, StepBackArguments,
|
||||
StepInArguments, StepOutArguments, SteppingGranularity, Variable,
|
||||
};
|
||||
use futures::{AsyncBufRead, AsyncReadExt, AsyncWrite};
|
||||
use gpui::{AppContext, AsyncAppContext};
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use serde_json::Value;
|
||||
use smol::{
|
||||
channel::{bounded, unbounded, Receiver, Sender},
|
||||
io::BufReader,
|
||||
net::{TcpListener, TcpStream},
|
||||
process::{self, Child},
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{Ipv4Addr, SocketAddrV4},
|
||||
path::PathBuf,
|
||||
process::Stdio,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use task::{DebugAdapterConfig, DebugConnectionType, DebugRequestType, TCPHost};
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ThreadStatus {
|
||||
#[default]
|
||||
Running,
|
||||
Stopped,
|
||||
Exited,
|
||||
Ended,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[repr(transparent)]
|
||||
pub struct DebugAdapterClientId(pub usize);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ThreadState {
|
||||
pub status: ThreadStatus,
|
||||
pub stack_frames: Vec<StackFrame>,
|
||||
pub scopes: HashMap<u64, Vec<Scope>>, // stack_frame_id -> scopes
|
||||
pub variables: HashMap<u64, Vec<Variable>>, // scope.variable_reference -> variables
|
||||
pub current_stack_frame_id: Option<u64>,
|
||||
}
|
||||
|
||||
pub struct DebugAdapterClient {
|
||||
id: DebugAdapterClientId,
|
||||
_process: Option<Child>,
|
||||
server_tx: Sender<Payload>,
|
||||
sequence_count: AtomicU64,
|
||||
capabilities: Arc<Mutex<Option<dap_types::Capabilities>>>,
|
||||
config: DebugAdapterConfig,
|
||||
thread_states: Arc<Mutex<HashMap<u64, ThreadState>>>, // thread_id -> thread_state
|
||||
sub_client: Arc<Mutex<Option<Arc<Self>>>>,
|
||||
}
|
||||
|
||||
impl DebugAdapterClient {
|
||||
/// Creates & returns a new debug adapter client
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients
|
||||
/// - `config`: The adapter specific configurations from debugger task that is starting
|
||||
/// - `command`: The command that starts the debugger
|
||||
/// - `args`: Arguments of the command that starts the debugger
|
||||
/// - `project_path`: The absolute path of the project that is being debugged
|
||||
/// - `cx`: The context that the new client belongs too
|
||||
pub async fn new<F>(
|
||||
id: DebugAdapterClientId,
|
||||
config: DebugAdapterConfig,
|
||||
command: &str,
|
||||
args: Vec<&str>,
|
||||
project_path: PathBuf,
|
||||
event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Arc<Self>>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
match config.connection.clone() {
|
||||
DebugConnectionType::TCP(host) => {
|
||||
Self::create_tcp_client(
|
||||
id,
|
||||
config,
|
||||
host,
|
||||
command,
|
||||
args,
|
||||
project_path,
|
||||
event_handler,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
DebugConnectionType::STDIO => {
|
||||
Self::create_stdio_client(
|
||||
id,
|
||||
config,
|
||||
command,
|
||||
args,
|
||||
project_path,
|
||||
event_handler,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a debug client that connects to an adapter through tcp
|
||||
///
|
||||
/// TCP clients don't have an error communication stream with an adapter
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients
|
||||
/// - `config`: The adapter specific configurations from debugger task that is starting
|
||||
/// - `command`: The command that starts the debugger
|
||||
/// - `args`: Arguments of the command that starts the debugger
|
||||
/// - `project_path`: The absolute path of the project that is being debugged
|
||||
/// - `cx`: The context that the new client belongs too
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn create_tcp_client<F>(
|
||||
id: DebugAdapterClientId,
|
||||
config: DebugAdapterConfig,
|
||||
host: TCPHost,
|
||||
command: &str,
|
||||
args: Vec<&str>,
|
||||
project_path: PathBuf,
|
||||
event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Arc<Self>>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
let mut port = host.port;
|
||||
if port.is_none() {
|
||||
port = Self::get_port().await;
|
||||
}
|
||||
|
||||
let mut command = process::Command::new(command);
|
||||
command
|
||||
.current_dir(project_path)
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let process = command
|
||||
.spawn()
|
||||
.with_context(|| "failed to start debug adapter.")?;
|
||||
|
||||
if let Some(delay) = host.delay {
|
||||
// some debug adapters need some time to start the TCP server
|
||||
// so we have to wait few milliseconds before we can connect to it
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(delay))
|
||||
.await;
|
||||
}
|
||||
|
||||
let address = SocketAddrV4::new(
|
||||
host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)),
|
||||
port.unwrap(),
|
||||
);
|
||||
|
||||
let (rx, tx) = TcpStream::connect(address).await?.split();
|
||||
|
||||
Self::handle_transport(
|
||||
id,
|
||||
config,
|
||||
Box::new(BufReader::new(rx)),
|
||||
Box::new(tx),
|
||||
None,
|
||||
Some(process),
|
||||
event_handler,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get an open port to use with the tcp client when not supplied by debug config
|
||||
async fn get_port() -> Option<u16> {
|
||||
Some(
|
||||
TcpListener::bind(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 0))
|
||||
.await
|
||||
.ok()?
|
||||
.local_addr()
|
||||
.ok()?
|
||||
.port(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a debug client that connects to an adapter through std input/output
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients
|
||||
/// - `config`: The adapter specific configurations from debugger task that is starting
|
||||
/// - `command`: The command that starts the debugger
|
||||
/// - `args`: Arguments of the command that starts the debugger
|
||||
/// - `project_path`: The absolute path of the project that is being debugged
|
||||
/// - `cx`: The context that the new client belongs too
|
||||
async fn create_stdio_client<F>(
|
||||
id: DebugAdapterClientId,
|
||||
config: DebugAdapterConfig,
|
||||
command: &str,
|
||||
args: Vec<&str>,
|
||||
project_path: PathBuf,
|
||||
event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Arc<Self>>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
let mut command = process::Command::new(command);
|
||||
command
|
||||
.current_dir(project_path)
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let mut process = command
|
||||
.spawn()
|
||||
.with_context(|| "failed to spawn command.")?;
|
||||
|
||||
let stdin = process
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("Failed to open stdin"))?;
|
||||
let stdout = process
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("Failed to open stdout"))?;
|
||||
let stderr = process
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("Failed to open stderr"))?;
|
||||
|
||||
let stdin = Box::new(stdin);
|
||||
let stdout = Box::new(BufReader::new(stdout));
|
||||
let stderr = Box::new(BufReader::new(stderr));
|
||||
|
||||
Self::handle_transport(
|
||||
id,
|
||||
config,
|
||||
stdout,
|
||||
stdin,
|
||||
Some(stderr),
|
||||
Some(process),
|
||||
event_handler,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn handle_transport<F>(
|
||||
id: DebugAdapterClientId,
|
||||
config: DebugAdapterConfig,
|
||||
rx: Box<dyn AsyncBufRead + Unpin + Send>,
|
||||
tx: Box<dyn AsyncWrite + Unpin + Send>,
|
||||
err: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
|
||||
process: Option<Child>,
|
||||
event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Arc<Self>>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
let (server_rx, server_tx) = Transport::start(rx, tx, err, cx);
|
||||
let (client_tx, client_rx) = unbounded::<Payload>();
|
||||
|
||||
let client = Arc::new(Self {
|
||||
id,
|
||||
config,
|
||||
_process: process,
|
||||
sub_client: Default::default(),
|
||||
server_tx: server_tx.clone(),
|
||||
capabilities: Default::default(),
|
||||
thread_states: Default::default(),
|
||||
sequence_count: AtomicU64::new(1),
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_recv(server_rx, client_tx))
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
cx.spawn({
|
||||
let client = client.clone();
|
||||
|mut cx| async move {
|
||||
Self::handle_events(client, client_rx, server_tx, event_handler, &mut cx).await
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Set's up a client's event handler.
|
||||
///
|
||||
/// This function should only be called once or else errors will arise
|
||||
/// # Parameters
|
||||
/// `client`: A pointer to the client to pass the event handler too
|
||||
/// `event_handler`: The function that is called to handle events
|
||||
/// should be DebugPanel::handle_debug_client_events
|
||||
/// `cx`: The context that this task will run in
|
||||
pub async fn handle_events<F>(
|
||||
this: Arc<Self>,
|
||||
client_rx: Receiver<Payload>,
|
||||
server_tx: Sender<Payload>,
|
||||
mut event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
while let Ok(payload) = client_rx.recv().await {
|
||||
match payload {
|
||||
Payload::Event(event) => cx.update(|cx| event_handler(*event, cx))?,
|
||||
Payload::Request(request) => {
|
||||
if RunInTerminal::COMMAND == request.command {
|
||||
Self::handle_run_in_terminal_request(request, &server_tx).await?;
|
||||
} else if StartDebugging::COMMAND == request.command {
|
||||
Self::handle_start_debugging_request(&this, request, cx).await?;
|
||||
} else {
|
||||
unreachable!("Unknown reverse request {}", request.command);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn handle_run_in_terminal_request(
|
||||
request: crate::transport::Request,
|
||||
server_tx: &Sender<Payload>,
|
||||
) -> Result<()> {
|
||||
let arguments: RunInTerminalRequestArguments =
|
||||
serde_json::from_value(request.arguments.unwrap_or_default())?;
|
||||
|
||||
let mut args = arguments.args.clone();
|
||||
let mut command = process::Command::new(args.remove(0));
|
||||
|
||||
let envs = arguments.env.as_ref().and_then(|e| e.as_object()).map(|e| {
|
||||
e.iter()
|
||||
.map(|(key, value)| (key.clone(), value.clone().to_string()))
|
||||
.collect::<HashMap<String, String>>()
|
||||
});
|
||||
|
||||
if let Some(envs) = envs {
|
||||
command.envs(envs);
|
||||
}
|
||||
|
||||
let process = command
|
||||
.current_dir(arguments.cwd)
|
||||
.args(args)
|
||||
.spawn()
|
||||
.with_context(|| "failed to spawn run in terminal command.")?;
|
||||
|
||||
server_tx
|
||||
.send(Payload::Response(Response {
|
||||
request_seq: request.seq,
|
||||
success: true,
|
||||
command: RunInTerminal::COMMAND.into(),
|
||||
message: None,
|
||||
body: Some(serde_json::to_value(RunInTerminalResponse {
|
||||
process_id: Some(process.id() as u64),
|
||||
shell_process_id: None,
|
||||
})?),
|
||||
}))
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn handle_start_debugging_request(
|
||||
this: &Arc<Self>,
|
||||
request: crate::transport::Request,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
dbg!(&request);
|
||||
let arguments: StartDebuggingRequestArguments =
|
||||
serde_json::from_value(request.arguments.clone().unwrap_or_default())?;
|
||||
|
||||
let sub_client = DebugAdapterClient::new(
|
||||
DebugAdapterClientId(1),
|
||||
this.config.clone(),
|
||||
"node",
|
||||
vec![
|
||||
"/Users/remcosmits/Downloads/js-debug/src/dapDebugServer.js",
|
||||
"8134",
|
||||
"127.0.0.1",
|
||||
],
|
||||
PathBuf::from("/Users/remcosmits/Documents/code/prettier-test"),
|
||||
|event, _cx| {
|
||||
dbg!(event);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
dbg!(&arguments);
|
||||
|
||||
let res = sub_client.launch(request.arguments).await?;
|
||||
dbg!(res);
|
||||
|
||||
*this.sub_client.lock() = Some(sub_client);
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn handle_recv(server_rx: Receiver<Payload>, client_tx: Sender<Payload>) -> Result<()> {
|
||||
while let Ok(payload) = server_rx.recv().await {
|
||||
match payload {
|
||||
Payload::Event(ev) => client_tx.send(Payload::Event(ev)).await?,
|
||||
Payload::Response(_) => unreachable!(),
|
||||
Payload::Request(req) => client_tx.send(Payload::Request(req)).await?,
|
||||
};
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
/// Send a request to an adapter and get a response back
|
||||
/// Note: This function will block until a response is sent back from the adapter
|
||||
pub async fn request<R: Request>(&self, arguments: R::Arguments) -> Result<R::Response> {
|
||||
let serialized_arguments = serde_json::to_value(arguments)?;
|
||||
|
||||
let (callback_tx, callback_rx) = bounded::<Result<Response>>(1);
|
||||
|
||||
let request = crate::transport::Request {
|
||||
back_ch: Some(callback_tx),
|
||||
seq: self.next_sequence_id(),
|
||||
command: R::COMMAND.to_string(),
|
||||
arguments: Some(serialized_arguments),
|
||||
};
|
||||
|
||||
self.server_tx.send(Payload::Request(request)).await?;
|
||||
|
||||
let response = callback_rx.recv().await??;
|
||||
|
||||
match response.success {
|
||||
true => Ok(serde_json::from_value(response.body.unwrap_or_default())?),
|
||||
false => Err(anyhow!("Request failed")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> DebugAdapterClientId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn config(&self) -> DebugAdapterConfig {
|
||||
self.config.clone()
|
||||
}
|
||||
|
||||
pub fn request_type(&self) -> DebugRequestType {
|
||||
self.config.request.clone()
|
||||
}
|
||||
|
||||
pub fn capabilities(&self) -> dap_types::Capabilities {
|
||||
self.capabilities.lock().clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the next sequence id to be used in a request
|
||||
/// # Side Effect
|
||||
/// This function also increment's client's sequence count by one
|
||||
pub fn next_sequence_id(&self) -> u64 {
|
||||
self.sequence_count.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn update_thread_state_status(&self, thread_id: u64, status: ThreadStatus) {
|
||||
if let Some(thread_state) = self.thread_states().get_mut(&thread_id) {
|
||||
thread_state.status = status;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn thread_states(&self) -> MutexGuard<HashMap<u64, ThreadState>> {
|
||||
self.thread_states.lock()
|
||||
}
|
||||
|
||||
pub fn thread_state_by_id(&self, thread_id: u64) -> ThreadState {
|
||||
self.thread_states.lock().get(&thread_id).cloned().unwrap()
|
||||
}
|
||||
|
||||
pub async fn initialize(&self) -> Result<dap_types::Capabilities> {
|
||||
let args = dap_types::InitializeRequestArguments {
|
||||
client_id: Some("zed".to_owned()),
|
||||
client_name: Some("Zed".to_owned()),
|
||||
adapter_id: self.config.id.clone(),
|
||||
locale: Some("en-us".to_owned()),
|
||||
path_format: Some(InitializeRequestArgumentsPathFormat::Path),
|
||||
supports_variable_type: Some(true),
|
||||
supports_variable_paging: Some(false),
|
||||
supports_run_in_terminal_request: Some(false), // TODO: we should support this
|
||||
supports_memory_references: Some(true),
|
||||
supports_progress_reporting: Some(true),
|
||||
supports_invalidated_event: Some(false),
|
||||
lines_start_at1: Some(true),
|
||||
columns_start_at1: Some(true),
|
||||
supports_memory_event: Some(true),
|
||||
supports_args_can_be_interpreted_by_shell: None,
|
||||
supports_start_debugging_request: Some(true),
|
||||
};
|
||||
|
||||
let capabilities = self.request::<Initialize>(args).await?;
|
||||
|
||||
*self.capabilities.lock() = Some(capabilities.clone());
|
||||
|
||||
Ok(capabilities)
|
||||
}
|
||||
|
||||
pub async fn launch(&self, args: Option<Value>) -> Result<()> {
|
||||
self.request::<Launch>(LaunchRequestArguments {
|
||||
raw: args.unwrap_or(Value::Null),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn attach(&self, args: Option<Value>) -> Result<()> {
|
||||
self.request::<Attach>(AttachRequestArguments {
|
||||
raw: args.unwrap_or(Value::Null),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn resume(&self, thread_id: u64) -> Result<ContinueResponse> {
|
||||
self.request::<Continue>(ContinueArguments {
|
||||
thread_id,
|
||||
single_thread: Some(true),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_over(&self, thread_id: u64) -> Result<()> {
|
||||
self.request::<Next>(NextArguments {
|
||||
thread_id,
|
||||
granularity: Some(SteppingGranularity::Statement),
|
||||
single_thread: Some(true),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_in(&self, thread_id: u64) -> Result<()> {
|
||||
self.request::<StepIn>(StepInArguments {
|
||||
thread_id,
|
||||
target_id: None,
|
||||
granularity: Some(SteppingGranularity::Statement),
|
||||
single_thread: Some(true),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_out(&self, thread_id: u64) -> Result<()> {
|
||||
self.request::<StepOut>(StepOutArguments {
|
||||
thread_id,
|
||||
granularity: Some(SteppingGranularity::Statement),
|
||||
single_thread: Some(true),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_back(&self, thread_id: u64) -> Result<()> {
|
||||
self.request::<StepBack>(StepBackArguments {
|
||||
thread_id,
|
||||
single_thread: Some(true),
|
||||
granularity: Some(SteppingGranularity::Statement),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn restart(&self) {
|
||||
self.request::<Restart>(RestartArguments {
|
||||
raw: self
|
||||
.config
|
||||
.request_args
|
||||
.as_ref()
|
||||
.map(|v| v.args.clone())
|
||||
.unwrap_or(Value::Null),
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
pub async fn pause(&self, thread_id: u64) {
|
||||
self.request::<Pause>(PauseArguments { thread_id })
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
pub async fn stop(&self) {
|
||||
self.request::<Disconnect>(DisconnectArguments {
|
||||
restart: Some(false),
|
||||
terminate_debuggee: Some(false),
|
||||
suspend_debuggee: Some(false),
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
pub async fn set_breakpoints(
|
||||
&self,
|
||||
path: PathBuf,
|
||||
breakpoints: Option<Vec<SourceBreakpoint>>,
|
||||
) -> Result<SetBreakpointsResponse> {
|
||||
let adapter_data = self.config.request_args.clone().map(|c| c.args);
|
||||
|
||||
self.request::<SetBreakpoints>(SetBreakpointsArguments {
|
||||
source: Source {
|
||||
path: Some(String::from(path.to_string_lossy())),
|
||||
name: None,
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data,
|
||||
checksums: None,
|
||||
},
|
||||
breakpoints,
|
||||
source_modified: None,
|
||||
lines: None,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn configuration_done(&self) -> Result<()> {
|
||||
self.request::<ConfigurationDone>(ConfigurationDoneArguments)
|
||||
.await
|
||||
}
|
||||
}
|
||||
3
crates/dap/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod client;
|
||||
pub mod transport;
|
||||
pub use dap_types::*;
|
||||
283
crates/dap/src/transport.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use dap_types::{
|
||||
BreakpointEvent, Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent,
|
||||
InvalidatedEvent, LoadedSourceEvent, MemoryEvent, ModuleEvent, OutputEvent, ProcessEvent,
|
||||
ProgressEndEvent, ProgressStartEvent, ProgressUpdateEvent, StoppedEvent, TerminatedEvent,
|
||||
ThreadEvent,
|
||||
};
|
||||
use futures::{AsyncBufRead, AsyncWrite};
|
||||
use gpui::AsyncAppContext;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::Value;
|
||||
use smol::{
|
||||
channel::{unbounded, Receiver, Sender},
|
||||
io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt},
|
||||
lock::Mutex,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum Payload {
|
||||
Event(Box<Events>),
|
||||
Response(Response),
|
||||
Request(Request),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(tag = "event", content = "body")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Events {
|
||||
Initialized(Option<Capabilities>),
|
||||
Stopped(StoppedEvent),
|
||||
Continued(ContinuedEvent),
|
||||
Exited(ExitedEvent),
|
||||
Terminated(Option<TerminatedEvent>),
|
||||
Thread(ThreadEvent),
|
||||
Output(OutputEvent),
|
||||
Breakpoint(BreakpointEvent),
|
||||
Module(ModuleEvent),
|
||||
LoadedSource(LoadedSourceEvent),
|
||||
Process(ProcessEvent),
|
||||
Capabilities(CapabilitiesEvent),
|
||||
ProgressStart(ProgressStartEvent),
|
||||
ProgressUpdate(ProgressUpdateEvent),
|
||||
ProgressEnd(ProgressEndEvent),
|
||||
Invalidated(InvalidatedEvent),
|
||||
Memory(MemoryEvent),
|
||||
#[serde(untagged)]
|
||||
Other(HashMap<String, Value>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Request {
|
||||
#[serde(skip)]
|
||||
pub back_ch: Option<Sender<Result<Response>>>,
|
||||
pub seq: u64,
|
||||
pub command: String,
|
||||
pub arguments: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
pub struct Response {
|
||||
pub request_seq: u64,
|
||||
pub success: bool,
|
||||
pub command: String,
|
||||
pub message: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_empty_object")]
|
||||
pub body: Option<Value>,
|
||||
}
|
||||
|
||||
fn deserialize_empty_object<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
if value == Value::Object(serde_json::Map::new()) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Transport {
|
||||
pending_requests: Mutex<HashMap<u64, Sender<Result<Response>>>>,
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
pub fn start(
|
||||
server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
|
||||
server_stdin: Box<dyn AsyncWrite + Unpin + Send>,
|
||||
server_stderr: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> (Receiver<Payload>, Sender<Payload>) {
|
||||
let (client_tx, server_rx) = unbounded::<Payload>();
|
||||
let (server_tx, client_rx) = unbounded::<Payload>();
|
||||
|
||||
let transport = Arc::new(Self {
|
||||
pending_requests: Mutex::new(HashMap::default()),
|
||||
});
|
||||
|
||||
let _ = cx.update(|cx| {
|
||||
let transport = transport.clone();
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(Self::receive(transport.clone(), server_stdout, client_tx))
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(Self::send(transport.clone(), server_stdin, client_rx))
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
if let Some(stderr) = server_stderr {
|
||||
cx.background_executor()
|
||||
.spawn(Self::err(stderr))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
|
||||
(server_rx, server_tx)
|
||||
}
|
||||
|
||||
async fn recv_server_message(
|
||||
reader: &mut Box<dyn AsyncBufRead + Unpin + Send>,
|
||||
buffer: &mut String,
|
||||
) -> Result<Payload> {
|
||||
let mut content_length = None;
|
||||
loop {
|
||||
buffer.truncate(0);
|
||||
|
||||
if reader
|
||||
.read_line(buffer)
|
||||
.await
|
||||
.with_context(|| "reading a message from server")?
|
||||
== 0
|
||||
{
|
||||
return Err(anyhow!("reader stream closed"));
|
||||
};
|
||||
|
||||
if buffer == "\r\n" {
|
||||
break;
|
||||
}
|
||||
|
||||
let parts = buffer.trim().split_once(": ");
|
||||
|
||||
match parts {
|
||||
Some(("Content-Length", value)) => {
|
||||
content_length = Some(value.parse().context("invalid content length")?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let content_length = content_length.context("missing content length")?;
|
||||
|
||||
let mut content = vec![0; content_length];
|
||||
reader
|
||||
.read_exact(&mut content)
|
||||
.await
|
||||
.with_context(|| "reading after a loop")?;
|
||||
|
||||
let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
|
||||
|
||||
Ok(serde_json::from_str::<Payload>(msg)?)
|
||||
}
|
||||
|
||||
async fn recv_server_error(
|
||||
err: &mut (impl AsyncBufRead + Unpin + Send),
|
||||
buffer: &mut String,
|
||||
) -> Result<()> {
|
||||
buffer.truncate(0);
|
||||
if err.read_line(buffer).await? == 0 {
|
||||
return Err(anyhow!("error stream closed"));
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_payload_to_server(
|
||||
&self,
|
||||
server_stdin: &mut Box<dyn AsyncWrite + Unpin + Send>,
|
||||
mut payload: Payload,
|
||||
) -> Result<()> {
|
||||
if let Payload::Request(request) = &mut payload {
|
||||
if let Some(back) = request.back_ch.take() {
|
||||
self.pending_requests.lock().await.insert(request.seq, back);
|
||||
}
|
||||
}
|
||||
self.send_string_to_server(server_stdin, serde_json::to_string(&payload)?)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn send_string_to_server(
|
||||
&self,
|
||||
server_stdin: &mut Box<dyn AsyncWrite + Unpin + Send>,
|
||||
request: String,
|
||||
) -> Result<()> {
|
||||
server_stdin
|
||||
.write_all(format!("Content-Length: {}\r\n\r\n{}", request.len(), request).as_bytes())
|
||||
.await?;
|
||||
|
||||
server_stdin.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_response(response: Response) -> Result<Response> {
|
||||
if response.success {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(anyhow!("Received failed response"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_server_message(
|
||||
&self,
|
||||
client_tx: &Sender<Payload>,
|
||||
payload: Payload,
|
||||
) -> Result<()> {
|
||||
match payload {
|
||||
Payload::Response(res) => {
|
||||
if let Some(tx) = self.pending_requests.lock().await.remove(&res.request_seq) {
|
||||
|
||||
if !tx.is_closed() {
|
||||
tx.send(Self::process_response(res)).await?;
|
||||
} else {
|
||||
log::warn!(
|
||||
"Response stream associated with request seq: {} is closed",
|
||||
&res.request_seq
|
||||
); // TODO: Fix this case so it never happens
|
||||
}
|
||||
} else {
|
||||
client_tx.send(Payload::Response(res)).await?;
|
||||
};
|
||||
}
|
||||
|
||||
Payload::Request(_) => {
|
||||
client_tx.send(payload).await?;
|
||||
}
|
||||
Payload::Event(_) => {
|
||||
client_tx.send(payload).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive(
|
||||
transport: Arc<Self>,
|
||||
mut server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
|
||||
client_tx: Sender<Payload>,
|
||||
) -> Result<()> {
|
||||
let mut recv_buffer = String::new();
|
||||
loop {
|
||||
transport
|
||||
.process_server_message(
|
||||
&client_tx,
|
||||
Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await?,
|
||||
)
|
||||
.await
|
||||
.context("Process server message failed in transport::receive")?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(
|
||||
transport: Arc<Self>,
|
||||
mut server_stdin: Box<dyn AsyncWrite + Unpin + Send>,
|
||||
client_rx: Receiver<Payload>,
|
||||
) -> Result<()> {
|
||||
while let Ok(payload) = client_rx.recv().await {
|
||||
transport
|
||||
.send_payload_to_server(&mut server_stdin, payload)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn err(mut server_stderr: Box<dyn AsyncBufRead + Unpin + Send>) -> Result<()> {
|
||||
let mut recv_buffer = String::new();
|
||||
loop {
|
||||
Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
crates/debugger_ui/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "debugger_ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
dap.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
task.workspace = true
|
||||
tasks_ui.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
602
crates/debugger_ui/src/debugger_panel.rs
Normal file
@@ -0,0 +1,602 @@
|
||||
use anyhow::Result;
|
||||
use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus};
|
||||
use dap::requests::{Disconnect, Scopes, StackTrace, Variables};
|
||||
use dap::{client::DebugAdapterClient, transport::Events};
|
||||
use dap::{
|
||||
Capabilities, ContinuedEvent, DisconnectArguments, ExitedEvent, Scope, ScopesArguments,
|
||||
StackFrame, StackTraceArguments, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason,
|
||||
Variable, VariablesArguments,
|
||||
};
|
||||
use editor::Editor;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView,
|
||||
Subscription, Task, View, ViewContext, WeakView,
|
||||
};
|
||||
use std::path::Path;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use task::DebugRequestType;
|
||||
use ui::prelude::*;
|
||||
use workspace::Pane;
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
use crate::debugger_panel_item::DebugPanelItem;
|
||||
|
||||
enum DebugCurrentRowHighlight {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DebugPanelEvent {
|
||||
Stopped((DebugAdapterClientId, StoppedEvent)),
|
||||
Thread((DebugAdapterClientId, ThreadEvent)),
|
||||
}
|
||||
|
||||
actions!(debug_panel, [ToggleFocus]);
|
||||
|
||||
pub struct DebugPanel {
|
||||
size: Pixels,
|
||||
pane: View<Pane>,
|
||||
focus_handle: FocusHandle,
|
||||
workspace: WeakView<Workspace>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl DebugPanel {
|
||||
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
|
||||
cx.new_view(|cx| {
|
||||
let pane = cx.new_view(|cx| {
|
||||
let mut pane = Pane::new(
|
||||
workspace.weak_handle(),
|
||||
workspace.project().clone(),
|
||||
Default::default(),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
pane.set_can_split(false, cx);
|
||||
pane.set_can_navigate(true, cx);
|
||||
pane.display_nav_history_buttons(None);
|
||||
pane.set_should_display_tab_bar(|_| true);
|
||||
|
||||
pane
|
||||
});
|
||||
|
||||
let project = workspace.project().clone();
|
||||
|
||||
let _subscriptions = vec![cx.subscribe(&project, {
|
||||
move |this: &mut Self, _, event, cx| match event {
|
||||
project::Event::DebugClientEvent { event, client_id } => {
|
||||
Self::handle_debug_client_events(
|
||||
this,
|
||||
this.debug_client_by_id(*client_id, cx),
|
||||
event,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
project::Event::DebugClientStarted(client_id) => {
|
||||
let client = this.debug_client_by_id(*client_id, cx);
|
||||
cx.spawn(|_, _| async move {
|
||||
client.initialize().await?;
|
||||
let request_args = client.config().request_args.map(|a| a.args);
|
||||
|
||||
// send correct request based on adapter config
|
||||
match client.config().request {
|
||||
DebugRequestType::Launch => client.launch(request_args).await,
|
||||
DebugRequestType::Attach => client.attach(request_args).await,
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})];
|
||||
|
||||
Self {
|
||||
pane,
|
||||
size: px(300.),
|
||||
_subscriptions,
|
||||
focus_handle: cx.focus_handle(),
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
workspace.update(&mut cx, |workspace, cx| DebugPanel::new(workspace, cx))
|
||||
})
|
||||
}
|
||||
|
||||
fn debug_client_by_id(
|
||||
&self,
|
||||
client_id: DebugAdapterClientId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Arc<DebugAdapterClient> {
|
||||
self.workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.project()
|
||||
.read(cx)
|
||||
.debug_adapter_by_id(client_id)
|
||||
.unwrap()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn handle_debug_client_events(
|
||||
this: &mut Self,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &Events,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
Events::Initialized(event) => Self::handle_initialized_event(client, event, cx),
|
||||
Events::Stopped(event) => Self::handle_stopped_event(client, event, cx),
|
||||
Events::Continued(event) => Self::handle_continued_event(client, event, cx),
|
||||
Events::Exited(event) => Self::handle_exited_event(client, event, cx),
|
||||
Events::Terminated(event) => Self::handle_terminated_event(this, client, event, cx),
|
||||
Events::Thread(event) => Self::handle_thread_event(client, event, cx),
|
||||
Events::Output(_) => {}
|
||||
Events::Breakpoint(_) => {}
|
||||
Events::Module(_) => {}
|
||||
Events::LoadedSource(_) => {}
|
||||
Events::Capabilities(_) => {}
|
||||
Events::Memory(_) => {}
|
||||
Events::Process(_) => {}
|
||||
Events::ProgressEnd(_) => {}
|
||||
Events::ProgressStart(_) => {}
|
||||
Events::ProgressUpdate(_) => {}
|
||||
Events::Invalidated(_) => {}
|
||||
Events::Other(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn go_to_stack_frame(
|
||||
workspace: WeakView<Workspace>,
|
||||
stack_frame: StackFrame,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
clear_highlights: bool,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let path = stack_frame.clone().source.unwrap().path.unwrap().clone();
|
||||
let row = (stack_frame.line.saturating_sub(1)) as u32;
|
||||
let column = (stack_frame.column.saturating_sub(1)) as u32;
|
||||
|
||||
if clear_highlights {
|
||||
Self::remove_highlights(workspace.clone(), client, cx.clone()).await?;
|
||||
}
|
||||
|
||||
let task = workspace.update(&mut cx, |workspace, cx| {
|
||||
let project_path = workspace.project().read_with(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&Path::new(&path), cx)
|
||||
});
|
||||
|
||||
if let Some(project_path) = project_path {
|
||||
workspace.open_path_preview(project_path, None, false, true, cx)
|
||||
} else {
|
||||
Task::ready(Err(anyhow::anyhow!(
|
||||
"No project path found for path: {}",
|
||||
path
|
||||
)))
|
||||
}
|
||||
})?;
|
||||
|
||||
let editor = task.await?.downcast::<Editor>().unwrap();
|
||||
|
||||
workspace.update(&mut cx, |_, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.go_to_line::<DebugCurrentRowHighlight>(
|
||||
row,
|
||||
column,
|
||||
Some(cx.theme().colors().editor_debugger_active_line_background),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async fn remove_highlights(
|
||||
workspace: WeakView<Workspace>,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let mut tasks = Vec::new();
|
||||
for thread_state in client.thread_states().values() {
|
||||
for stack_frame in thread_state.stack_frames.clone() {
|
||||
tasks.push(Self::remove_editor_highlight(
|
||||
workspace.clone(),
|
||||
stack_frame,
|
||||
cx.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !tasks.is_empty() {
|
||||
try_join_all(tasks).await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn remove_highlights_for_thread(
|
||||
workspace: WeakView<Workspace>,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
thread_id: u64,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let mut tasks = Vec::new();
|
||||
if let Some(thread_state) = client.thread_states().get(&thread_id) {
|
||||
for stack_frame in thread_state.stack_frames.clone() {
|
||||
tasks.push(Self::remove_editor_highlight(
|
||||
workspace.clone(),
|
||||
stack_frame.clone(),
|
||||
cx.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !tasks.is_empty() {
|
||||
try_join_all(tasks).await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn remove_editor_highlight(
|
||||
workspace: WeakView<Workspace>,
|
||||
stack_frame: StackFrame,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let path = stack_frame.clone().source.unwrap().path.unwrap().clone();
|
||||
|
||||
let task = workspace.update(&mut cx, |workspace, cx| {
|
||||
let project_path = workspace.project().read_with(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&Path::new(&path), cx)
|
||||
});
|
||||
|
||||
if let Some(project_path) = project_path {
|
||||
workspace.open_path(project_path, None, false, cx)
|
||||
} else {
|
||||
Task::ready(Err(anyhow::anyhow!(
|
||||
"No project path found for path: {}",
|
||||
path
|
||||
)))
|
||||
}
|
||||
})?;
|
||||
|
||||
let editor = task.await?.downcast::<Editor>().unwrap();
|
||||
|
||||
editor.update(&mut cx, |editor, _| {
|
||||
editor.clear_row_highlights::<DebugCurrentRowHighlight>();
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_initialized_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
_: &Option<Capabilities>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let task = this.update(&mut cx, |this, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
let client = client.clone();
|
||||
|
||||
project.send_breakpoints(client, cx)
|
||||
})
|
||||
})
|
||||
})??;
|
||||
|
||||
task.await?;
|
||||
|
||||
client.configuration_done().await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_continued_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &ContinuedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let all_threads = event.all_threads_continued.unwrap_or(false);
|
||||
|
||||
if all_threads {
|
||||
for thread in client.thread_states().values_mut() {
|
||||
thread.status = ThreadStatus::Running;
|
||||
}
|
||||
} else {
|
||||
client.update_thread_state_status(event.thread_id, ThreadStatus::Running);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_stopped_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &StoppedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let Some(thread_id) = event.thread_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let client_id = client.id();
|
||||
cx.spawn({
|
||||
let event = event.clone();
|
||||
|this, mut cx| async move {
|
||||
let stack_trace_response = client
|
||||
.request::<StackTrace>(StackTraceArguments {
|
||||
thread_id,
|
||||
start_frame: None,
|
||||
levels: None,
|
||||
format: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let current_stack_frame =
|
||||
stack_trace_response.stack_frames.first().unwrap().clone();
|
||||
let mut scope_tasks = Vec::new();
|
||||
for stack_frame in stack_trace_response.stack_frames.clone().into_iter() {
|
||||
let frame_id = stack_frame.id;
|
||||
let client = client.clone();
|
||||
scope_tasks.push(async move {
|
||||
anyhow::Ok((
|
||||
frame_id,
|
||||
client
|
||||
.request::<Scopes>(ScopesArguments { frame_id })
|
||||
.await?,
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
let mut scopes: HashMap<u64, Vec<Scope>> = HashMap::new();
|
||||
let mut variables: HashMap<u64, Vec<Variable>> = HashMap::new();
|
||||
|
||||
let mut variable_tasks = Vec::new();
|
||||
for (thread_id, response) in try_join_all(scope_tasks).await? {
|
||||
scopes.insert(thread_id, response.scopes.clone());
|
||||
|
||||
for scope in response.scopes {
|
||||
let scope_reference = scope.variables_reference;
|
||||
let client = client.clone();
|
||||
variable_tasks.push(async move {
|
||||
anyhow::Ok((
|
||||
scope_reference,
|
||||
client
|
||||
.request::<Variables>(VariablesArguments {
|
||||
variables_reference: scope_reference,
|
||||
filter: None,
|
||||
start: None,
|
||||
count: None,
|
||||
format: None,
|
||||
})
|
||||
.await?,
|
||||
))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (scope_reference, response) in try_join_all(variable_tasks).await? {
|
||||
variables.insert(scope_reference, response.variables.clone());
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let mut thread_state = client.thread_states();
|
||||
let thread_state = thread_state
|
||||
.entry(thread_id)
|
||||
.or_insert(ThreadState::default());
|
||||
|
||||
thread_state.current_stack_frame_id = Some(current_stack_frame.clone().id);
|
||||
thread_state.stack_frames = stack_trace_response.stack_frames;
|
||||
thread_state.scopes = scopes;
|
||||
thread_state.variables = variables;
|
||||
thread_state.status = ThreadStatus::Stopped;
|
||||
|
||||
let existing_item = this
|
||||
.pane
|
||||
.read(cx)
|
||||
.items()
|
||||
.filter_map(|item| item.downcast::<DebugPanelItem>())
|
||||
.any(|item| {
|
||||
let item = item.read(cx);
|
||||
|
||||
item.client().id() == client_id && item.thread_id() == thread_id
|
||||
});
|
||||
|
||||
if !existing_item {
|
||||
let debug_panel = cx.view().clone();
|
||||
this.pane.update(cx, |this, cx| {
|
||||
let tab = cx.new_view(|cx| {
|
||||
DebugPanelItem::new(debug_panel, client.clone(), thread_id, cx)
|
||||
});
|
||||
|
||||
this.add_item(Box::new(tab.clone()), false, false, None, cx)
|
||||
});
|
||||
}
|
||||
|
||||
cx.emit(DebugPanelEvent::Stopped((client_id, event)));
|
||||
|
||||
if let Some(item) = this.pane.read(cx).active_item() {
|
||||
if let Some(pane) = item.downcast::<DebugPanelItem>() {
|
||||
let pane = pane.read(cx);
|
||||
if pane.thread_id() == thread_id && pane.client().id() == client_id {
|
||||
let workspace = this.workspace.clone();
|
||||
let client = client.clone();
|
||||
return cx.spawn(|_, cx| async move {
|
||||
Self::go_to_stack_frame(
|
||||
workspace,
|
||||
current_stack_frame.clone(),
|
||||
client,
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Task::ready(anyhow::Ok(()))
|
||||
})?
|
||||
.await
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &ThreadEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let thread_id = event.thread_id;
|
||||
|
||||
if event.reason == ThreadEventReason::Started {
|
||||
client
|
||||
.thread_states()
|
||||
.insert(thread_id, ThreadState::default());
|
||||
} else {
|
||||
client.update_thread_state_status(thread_id, ThreadStatus::Ended);
|
||||
|
||||
// TODO: we want to figure out for witch clients/threads we should remove the highlights
|
||||
cx.spawn({
|
||||
let client = client.clone();
|
||||
|this, mut cx| async move {
|
||||
let workspace = this.update(&mut cx, |this, _| this.workspace.clone())?;
|
||||
|
||||
Self::remove_highlights_for_thread(workspace, client, thread_id, cx).await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
cx.emit(DebugPanelEvent::Thread((client.id(), event.clone())));
|
||||
}
|
||||
|
||||
fn handle_exited_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
_: &ExitedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
cx.spawn(|_, _| async move {
|
||||
for thread_state in client.thread_states().values_mut() {
|
||||
thread_state.status = ThreadStatus::Exited;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_terminated_event(
|
||||
this: &mut Self,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &Option<TerminatedEvent>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let restart_args = event.clone().and_then(|e| e.restart);
|
||||
let workspace = this.workspace.clone();
|
||||
|
||||
cx.spawn(|_, cx| async move {
|
||||
let should_restart = restart_args.is_some();
|
||||
|
||||
Self::remove_highlights(workspace, client.clone(), cx).await?;
|
||||
|
||||
client
|
||||
.request::<Disconnect>(DisconnectArguments {
|
||||
restart: Some(should_restart),
|
||||
terminate_debuggee: None,
|
||||
suspend_debuggee: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
if should_restart {
|
||||
match client.request_type() {
|
||||
DebugRequestType::Launch => client.launch(restart_args).await,
|
||||
DebugRequestType::Attach => client.attach(restart_args).await,
|
||||
}
|
||||
} else {
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<DebugPanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<project::Event> for DebugPanel {}
|
||||
|
||||
impl FocusableView for DebugPanel {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for DebugPanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"DebugPanel"
|
||||
}
|
||||
|
||||
fn position(&self, _cx: &WindowContext) -> DockPosition {
|
||||
DockPosition::Bottom
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
position == DockPosition::Bottom
|
||||
}
|
||||
|
||||
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn size(&self, _cx: &WindowContext) -> Pixels {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<Pixels>, _cx: &mut ViewContext<Self>) {
|
||||
self.size = size.unwrap();
|
||||
}
|
||||
|
||||
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
|
||||
None
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn toggle_action(&self) -> Box<dyn Action> {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
||||
fn icon_label(&self, _: &WindowContext) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_zoomed(&self, _cx: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn starts_open(&self, _cx: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
|
||||
}
|
||||
|
||||
impl Render for DebugPanel {
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("DebugPanel")
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.child(self.pane.clone())
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
514
crates/debugger_ui/src/debugger_panel_item.rs
Normal file
@@ -0,0 +1,514 @@
|
||||
use crate::debugger_panel::{DebugPanel, DebugPanelEvent};
|
||||
use anyhow::Result;
|
||||
use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus};
|
||||
use dap::{Scope, StackFrame, StoppedEvent, ThreadEvent, Variable};
|
||||
use gpui::{
|
||||
actions, list, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
|
||||
FocusableView, ListState, Subscription, View, WeakView,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use ui::WindowContext;
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use workspace::item::{Item, ItemEvent};
|
||||
|
||||
pub struct DebugPanelItem {
|
||||
thread_id: u64,
|
||||
focus_handle: FocusHandle,
|
||||
stack_frame_list: ListState,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
current_stack_frame_id: Option<u64>,
|
||||
}
|
||||
|
||||
actions!(
|
||||
debug_panel_item,
|
||||
[Continue, StepOver, StepIn, StepOut, Restart, Pause, Stop]
|
||||
);
|
||||
|
||||
impl DebugPanelItem {
|
||||
pub fn new(
|
||||
debug_panel: View<DebugPanel>,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
thread_id: u64,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let weakview = cx.view().downgrade();
|
||||
let stack_frame_list =
|
||||
ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| {
|
||||
if let Some(view) = weakview.upgrade() {
|
||||
view.update(cx, |view, cx| {
|
||||
view.render_stack_frame(ix, cx).into_any_element()
|
||||
})
|
||||
} else {
|
||||
div().into_any()
|
||||
}
|
||||
});
|
||||
|
||||
let _subscriptions = vec![cx.subscribe(&debug_panel, {
|
||||
move |this: &mut Self, _, event: &DebugPanelEvent, cx| {
|
||||
match event {
|
||||
DebugPanelEvent::Stopped((client_id, event)) => {
|
||||
Self::handle_stopped_event(this, client_id, event, cx)
|
||||
}
|
||||
DebugPanelEvent::Thread((client_id, event)) => {
|
||||
Self::handle_thread_event(this, client_id, event, cx)
|
||||
}
|
||||
};
|
||||
}
|
||||
})];
|
||||
|
||||
Self {
|
||||
client,
|
||||
thread_id,
|
||||
focus_handle,
|
||||
_subscriptions,
|
||||
stack_frame_list,
|
||||
current_stack_frame_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_skip_event(
|
||||
this: &mut Self,
|
||||
client_id: &DebugAdapterClientId,
|
||||
thread_id: u64,
|
||||
) -> bool {
|
||||
thread_id != this.thread_id || *client_id != this.client.id()
|
||||
}
|
||||
|
||||
fn handle_stopped_event(
|
||||
this: &mut Self,
|
||||
client_id: &DebugAdapterClientId,
|
||||
event: &StoppedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if Self::should_skip_event(this, client_id, event.thread_id.unwrap_or_default()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stack_frame_list
|
||||
.reset(this.current_thread_state().stack_frames.len());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
this: &mut Self,
|
||||
client_id: &DebugAdapterClientId,
|
||||
event: &ThreadEvent,
|
||||
_: &mut ViewContext<Self>,
|
||||
) {
|
||||
if Self::should_skip_event(this, client_id, event.thread_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: handle thread event
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for DebugPanelItem {}
|
||||
|
||||
impl FocusableView for DebugPanelItem {
|
||||
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for DebugPanelItem {
|
||||
type Event = ItemEvent;
|
||||
|
||||
fn tab_content(
|
||||
&self,
|
||||
params: workspace::item::TabContentParams,
|
||||
_: &WindowContext,
|
||||
) -> AnyElement {
|
||||
Label::new(format!(
|
||||
"{} - Thread {}",
|
||||
self.client.config().id,
|
||||
self.thread_id
|
||||
))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
|
||||
Some(SharedString::from(format!(
|
||||
"{} Thread {} - {:?}",
|
||||
self.client.config().id,
|
||||
self.thread_id,
|
||||
self.current_thread_state().status
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl DebugPanelItem {
|
||||
pub fn client(&self) -> Arc<DebugAdapterClient> {
|
||||
self.client.clone()
|
||||
}
|
||||
|
||||
pub fn thread_id(&self) -> u64 {
|
||||
self.thread_id
|
||||
}
|
||||
|
||||
fn stack_frame_for_index(&self, ix: usize) -> StackFrame {
|
||||
self.client
|
||||
.thread_state_by_id(self.thread_id)
|
||||
.stack_frames
|
||||
.get(ix)
|
||||
.cloned()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn current_thread_state(&self) -> ThreadState {
|
||||
self.client
|
||||
.thread_states()
|
||||
.get(&self.thread_id)
|
||||
.cloned()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn render_stack_frames(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w_1_3()
|
||||
.gap_3()
|
||||
.h_full()
|
||||
.child(list(self.stack_frame_list.clone()).size_full())
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_stack_frame(&self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let stack_frame = self.stack_frame_for_index(ix);
|
||||
|
||||
let source = stack_frame.source.clone();
|
||||
let selected_frame_id = self.current_stack_frame_id;
|
||||
let is_selected_frame = Some(stack_frame.id) == selected_frame_id;
|
||||
|
||||
let formatted_path = format!(
|
||||
"{}:{}",
|
||||
source.clone().and_then(|s| s.name).unwrap_or_default(),
|
||||
stack_frame.line,
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.rounded_md()
|
||||
.group("")
|
||||
.id(("stack-frame", stack_frame.id))
|
||||
.tooltip({
|
||||
let formatted_path = formatted_path.clone();
|
||||
move |cx| Tooltip::text(formatted_path.clone(), cx)
|
||||
})
|
||||
.p_1()
|
||||
.when(is_selected_frame, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
.on_click(cx.listener({
|
||||
let stack_frame = stack_frame.clone();
|
||||
move |this, _, _| {
|
||||
this.current_stack_frame_id = Some(stack_frame.id);
|
||||
|
||||
// let client = this.client();
|
||||
// DebugPanel::go_to_stack_frame(&stack_frame, client, true, cx)
|
||||
// .detach_and_log_err(cx);
|
||||
|
||||
// TODO:
|
||||
// this.go_to_stack_frame(&stack_frame, this.client.clone(), false, cx)
|
||||
// .detach_and_log_err(cx);
|
||||
// cx.notify();
|
||||
}
|
||||
}))
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.text_ui_sm(cx)
|
||||
.child(stack_frame.name.clone())
|
||||
.child(formatted_path),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.text_ui_xs(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.when_some(source.and_then(|s| s.path), |this, path| this.child(path)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_scopes(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let thread_state = self.current_thread_state();
|
||||
let Some(scopes) = thread_state
|
||||
.current_stack_frame_id
|
||||
.and_then(|id| thread_state.scopes.get(&id))
|
||||
else {
|
||||
return div().child("No scopes for this thread yet").into_any();
|
||||
};
|
||||
|
||||
div()
|
||||
.w_3_4()
|
||||
.gap_3()
|
||||
.text_ui_sm(cx)
|
||||
.children(
|
||||
scopes
|
||||
.iter()
|
||||
.map(|scope| self.render_scope(&thread_state, scope, cx)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_scope(
|
||||
&self,
|
||||
thread_state: &ThreadState,
|
||||
scope: &Scope,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
div()
|
||||
.id(("scope", scope.variables_reference))
|
||||
.p_1()
|
||||
.text_ui_sm(cx)
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(scope.name.clone())
|
||||
.child(
|
||||
div()
|
||||
.ml_2()
|
||||
.child(self.render_variables(thread_state, scope, cx)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_variables(
|
||||
&self,
|
||||
thread_state: &ThreadState,
|
||||
scope: &Scope,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let Some(variables) = thread_state.variables.get(&scope.variables_reference) else {
|
||||
return div().child("No variables for this thread yet").into_any();
|
||||
};
|
||||
|
||||
div()
|
||||
.gap_3()
|
||||
.text_ui_sm(cx)
|
||||
.children(
|
||||
variables
|
||||
.iter()
|
||||
.map(|variable| self.render_variable(variable, cx)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_variable(&self, variable: &Variable, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id(("variable", variable.variables_reference))
|
||||
.p_1()
|
||||
.gap_1()
|
||||
.text_ui_sm(cx)
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(variable.name.clone())
|
||||
.child(
|
||||
div()
|
||||
.text_ui_xs(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(variable.value.clone()),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
// if the debug adapter does not send the continued event,
|
||||
// and the status of the thread did not change we have to assume the thread is running
|
||||
// so we have to update the thread state status to running
|
||||
fn update_thread_state(
|
||||
this: WeakView<Self>,
|
||||
previous_status: ThreadStatus,
|
||||
all_threads_continued: Option<bool>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if previous_status == this.current_thread_state().status {
|
||||
if all_threads_continued.unwrap_or(false) {
|
||||
for thread in this.client.thread_states().values_mut() {
|
||||
thread.status = ThreadStatus::Running;
|
||||
}
|
||||
} else {
|
||||
this.client
|
||||
.update_thread_state_status(this.thread_id, ThreadStatus::Running);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_continue_action(&mut self, _: &Continue, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
let previous_status = self.current_thread_state().status;
|
||||
|
||||
cx.spawn(|this, cx| async move {
|
||||
let response = client.resume(thread_id).await?;
|
||||
|
||||
Self::update_thread_state(this, previous_status, response.all_threads_continued, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_step_over_action(&mut self, _: &StepOver, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
let previous_status = self.current_thread_state().status;
|
||||
|
||||
cx.spawn(|this, cx| async move {
|
||||
client.step_over(thread_id).await?;
|
||||
|
||||
Self::update_thread_state(this, previous_status, None, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_step_in_action(&mut self, _: &StepIn, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
let previous_status = self.current_thread_state().status;
|
||||
|
||||
cx.spawn(|this, cx| async move {
|
||||
client.step_in(thread_id).await?;
|
||||
|
||||
Self::update_thread_state(this, previous_status, None, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_step_out_action(&mut self, _: &StepOut, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
let previous_status = self.current_thread_state().status;
|
||||
|
||||
cx.spawn(|this, cx| async move {
|
||||
client.step_out(thread_id).await?;
|
||||
|
||||
Self::update_thread_state(this, previous_status, None, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_restart_action(&mut self, _: &Restart, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move { client.restart().await })
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_pause_action(&mut self, _: &Pause, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
cx.background_executor()
|
||||
.spawn(async move { client.pause(thread_id).await })
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_stop_action(&mut self, _: &Stop, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
cx.background_executor()
|
||||
.spawn(async move { client.stop().await })
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DebugPanelItem {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let thread_status = self.current_thread_state().status;
|
||||
|
||||
v_flex()
|
||||
.key_context("DebugPanelItem")
|
||||
.track_focus(&self.focus_handle)
|
||||
.capture_action(cx.listener(Self::handle_continue_action))
|
||||
.capture_action(cx.listener(Self::handle_step_over_action))
|
||||
.capture_action(cx.listener(Self::handle_step_in_action))
|
||||
.capture_action(cx.listener(Self::handle_step_out_action))
|
||||
.capture_action(cx.listener(Self::handle_restart_action))
|
||||
.capture_action(cx.listener(Self::handle_pause_action))
|
||||
.capture_action(cx.listener(Self::handle_stop_action))
|
||||
.p_2()
|
||||
.size_full()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.map(|this| {
|
||||
if self.current_thread_state().status == ThreadStatus::Running {
|
||||
this.child(
|
||||
IconButton::new("debug-pause", IconName::DebugPause)
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Pause))),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text("Pause program", cx)),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
IconButton::new("debug-continue", IconName::DebugContinue)
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.dispatch_action(Box::new(Continue))
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |cx| Tooltip::text("Continue program", cx)),
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
IconButton::new("debug-step-over", IconName::DebugStepOver)
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepOver))),
|
||||
)
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |cx| Tooltip::text("Step over", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-in", IconName::DebugStepInto)
|
||||
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepIn))))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |cx| Tooltip::text("Step in", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-out", IconName::DebugStepOut)
|
||||
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepOut))))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |cx| Tooltip::text("Step out", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-restart", IconName::DebugRestart)
|
||||
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Restart))))
|
||||
.disabled(
|
||||
!self
|
||||
.client
|
||||
.capabilities()
|
||||
.supports_restart_request
|
||||
.unwrap_or_default()
|
||||
|| thread_status != ThreadStatus::Stopped
|
||||
&& thread_status != ThreadStatus::Running,
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text("Restart", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-stop", IconName::DebugStop)
|
||||
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Stop))))
|
||||
.disabled(
|
||||
thread_status != ThreadStatus::Stopped
|
||||
&& thread_status != ThreadStatus::Running,
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text("Stop", cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.items_start()
|
||||
.p_1()
|
||||
.gap_4()
|
||||
.child(self.render_stack_frames(cx))
|
||||
.child(self.render_scopes(cx)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
18
crates/debugger_ui/src/lib.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use debugger_panel::{DebugPanel, ToggleFocus};
|
||||
use gpui::AppContext;
|
||||
use workspace::{StartDebugger, Workspace};
|
||||
|
||||
pub mod debugger_panel;
|
||||
mod debugger_panel_item;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<DebugPanel>(cx);
|
||||
});
|
||||
workspace.register_action(|workspace: &mut Workspace, _: &StartDebugger, cx| {
|
||||
tasks_ui::toggle_modal(workspace, cx, task::TaskType::Debug).detach();
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -293,6 +293,7 @@ gpui::actions!(
|
||||
SplitSelectionIntoLines,
|
||||
Tab,
|
||||
TabPrev,
|
||||
ToggleBreakpoint,
|
||||
ToggleAutoSignatureHelp,
|
||||
ToggleGitBlame,
|
||||
ToggleGitBlameInline,
|
||||
|
||||
@@ -109,7 +109,6 @@ pub struct DisplayMap {
|
||||
crease_map: CreaseMap,
|
||||
fold_placeholder: FoldPlaceholder,
|
||||
pub clip_at_line_ends: bool,
|
||||
pub(crate) masked: bool,
|
||||
}
|
||||
|
||||
impl DisplayMap {
|
||||
@@ -157,7 +156,6 @@ impl DisplayMap {
|
||||
text_highlights: Default::default(),
|
||||
inlay_highlights: Default::default(),
|
||||
clip_at_line_ends: false,
|
||||
masked: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +182,6 @@ impl DisplayMap {
|
||||
text_highlights: self.text_highlights.clone(),
|
||||
inlay_highlights: self.inlay_highlights.clone(),
|
||||
clip_at_line_ends: self.clip_at_line_ends,
|
||||
masked: self.masked,
|
||||
fold_placeholder: self.fold_placeholder.clone(),
|
||||
}
|
||||
}
|
||||
@@ -502,7 +499,6 @@ pub struct DisplaySnapshot {
|
||||
text_highlights: TextHighlights,
|
||||
inlay_highlights: InlayHighlights,
|
||||
clip_at_line_ends: bool,
|
||||
masked: bool,
|
||||
pub(crate) fold_placeholder: FoldPlaceholder,
|
||||
}
|
||||
|
||||
@@ -654,7 +650,6 @@ impl DisplaySnapshot {
|
||||
.chunks(
|
||||
display_row.0..self.max_point().row().next_row().0,
|
||||
false,
|
||||
self.masked,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|h| h.text)
|
||||
@@ -662,9 +657,9 @@ impl DisplaySnapshot {
|
||||
|
||||
/// Returns text chunks starting at the end of the given display row in reverse until the start of the file
|
||||
pub fn reverse_text_chunks(&self, display_row: DisplayRow) -> impl Iterator<Item = &str> {
|
||||
(0..=display_row.0).rev().flat_map(move |row| {
|
||||
(0..=display_row.0).rev().flat_map(|row| {
|
||||
self.block_snapshot
|
||||
.chunks(row..row + 1, false, self.masked, Highlights::default())
|
||||
.chunks(row..row + 1, false, Highlights::default())
|
||||
.map(|h| h.text)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
@@ -681,7 +676,6 @@ impl DisplaySnapshot {
|
||||
self.block_snapshot.chunks(
|
||||
display_rows.start.0..display_rows.end.0,
|
||||
language_aware,
|
||||
self.masked,
|
||||
Highlights {
|
||||
text_highlights: Some(&self.text_highlights),
|
||||
inlay_highlights: Some(&self.inlay_highlights),
|
||||
|
||||
@@ -23,7 +23,6 @@ use text::Edit;
|
||||
use ui::ElementId;
|
||||
|
||||
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
|
||||
const BULLETS: &str = "********************************************************************************************************************************";
|
||||
|
||||
/// Tracks custom blocks such as diagnostics that should be displayed within buffer.
|
||||
///
|
||||
@@ -286,7 +285,6 @@ pub struct BlockChunks<'a> {
|
||||
input_chunk: Chunk<'a>,
|
||||
output_row: u32,
|
||||
max_output_row: u32,
|
||||
masked: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -895,7 +893,6 @@ impl BlockSnapshot {
|
||||
self.chunks(
|
||||
0..self.transforms.summary().output_rows,
|
||||
false,
|
||||
false,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|chunk| chunk.text)
|
||||
@@ -906,7 +903,6 @@ impl BlockSnapshot {
|
||||
&'a self,
|
||||
rows: Range<u32>,
|
||||
language_aware: bool,
|
||||
masked: bool,
|
||||
highlights: Highlights<'a>,
|
||||
) -> BlockChunks<'a> {
|
||||
let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
|
||||
@@ -945,7 +941,6 @@ impl BlockSnapshot {
|
||||
transforms: cursor,
|
||||
output_row: rows.start,
|
||||
max_output_row,
|
||||
masked,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1234,20 +1229,12 @@ impl<'a> Iterator for BlockChunks<'a> {
|
||||
let (prefix_rows, prefix_bytes) =
|
||||
offset_for_row(self.input_chunk.text, transform_end - self.output_row);
|
||||
self.output_row += prefix_rows;
|
||||
let (mut prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
|
||||
let (prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
|
||||
self.input_chunk.text = suffix;
|
||||
if self.output_row == transform_end {
|
||||
self.transforms.next(&());
|
||||
}
|
||||
|
||||
if self.masked {
|
||||
// Not great for multibyte text because to keep cursor math correct we
|
||||
// need to have the same number of bytes in the input as output.
|
||||
let chars = prefix.chars().count();
|
||||
let bullet_len = chars;
|
||||
prefix = &BULLETS[..bullet_len];
|
||||
}
|
||||
|
||||
Some(Chunk {
|
||||
text: prefix,
|
||||
..self.input_chunk.clone()
|
||||
@@ -2061,7 +2048,6 @@ mod tests {
|
||||
.chunks(
|
||||
start_row as u32..blocks_snapshot.max_point().row + 1,
|
||||
false,
|
||||
false,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|chunk| chunk.text)
|
||||
|
||||
@@ -408,7 +408,6 @@ impl EditorActionId {
|
||||
type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);
|
||||
type GutterHighlight = (fn(&AppContext) -> Hsla, Arc<[Range<Anchor>]>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct ScrollbarMarkerState {
|
||||
scrollbar_size: Size<Pixels>,
|
||||
dirty: bool,
|
||||
@@ -422,6 +421,17 @@ impl ScrollbarMarkerState {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ScrollbarMarkerState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scrollbar_size: Size::default(),
|
||||
dirty: false,
|
||||
markers: Arc::from([]),
|
||||
pending_refresh: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct RunnableTasks {
|
||||
templates: Vec<(TaskSourceKind, TaskTemplate)>,
|
||||
@@ -439,6 +449,13 @@ struct ResolvedTasks {
|
||||
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
|
||||
position: Anchor,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Breakpoint {
|
||||
row: MultiBufferRow,
|
||||
_line: BufferRow,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct MultiBufferOffset(usize);
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
|
||||
@@ -480,6 +497,7 @@ pub struct Editor {
|
||||
mode: EditorMode,
|
||||
show_breadcrumbs: bool,
|
||||
show_gutter: bool,
|
||||
redact_all: bool,
|
||||
show_line_numbers: Option<bool>,
|
||||
show_git_diff_gutter: Option<bool>,
|
||||
show_code_actions: Option<bool>,
|
||||
@@ -557,6 +575,7 @@ pub struct Editor {
|
||||
expect_bounds_change: Option<Bounds<Pixels>>,
|
||||
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
|
||||
tasks_update_task: Option<Task<()>>,
|
||||
breakpoints: BTreeMap<(BufferId, BufferRow), Breakpoint>,
|
||||
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
|
||||
file_header_size: u8,
|
||||
breadcrumb_header: Option<String>,
|
||||
@@ -1802,6 +1821,7 @@ impl Editor {
|
||||
show_code_actions: None,
|
||||
show_runnables: None,
|
||||
show_wrap_guides: None,
|
||||
redact_all: false,
|
||||
show_indent_guides,
|
||||
placeholder_text: None,
|
||||
highlight_order: 0,
|
||||
@@ -1871,6 +1891,7 @@ impl Editor {
|
||||
blame_subscription: None,
|
||||
file_header_size,
|
||||
tasks: Default::default(),
|
||||
breakpoints: Default::default(),
|
||||
_subscriptions: vec![
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||
@@ -5120,6 +5141,17 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_breakpoint(&self, row: DisplayRow, cx: &mut ViewContext<Self>) -> IconButton {
|
||||
IconButton::new(("breakpoint_indicator", row.0 as usize), ui::IconName::Play)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.size(ui::ButtonSize::None)
|
||||
.icon_color(Color::Error)
|
||||
.on_click(cx.listener(move |editor, _e, cx| {
|
||||
editor.focus(cx);
|
||||
editor.toggle_breakpoint_at_row(row.0, cx) //TODO handle folded
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_run_indicator(
|
||||
&self,
|
||||
_style: &EditorStyle,
|
||||
@@ -5718,7 +5750,7 @@ impl Editor {
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
let empty_str: Arc<str> = Arc::default();
|
||||
let empty_str: Arc<str> = "".into();
|
||||
buffer.edit(
|
||||
deletion_ranges
|
||||
.into_iter()
|
||||
@@ -5784,7 +5816,7 @@ impl Editor {
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
let buffer = this.buffer.update(cx, |buffer, cx| {
|
||||
let empty_str: Arc<str> = Arc::default();
|
||||
let empty_str: Arc<str> = "".into();
|
||||
buffer.edit(
|
||||
edit_ranges
|
||||
.into_iter()
|
||||
@@ -5938,6 +5970,38 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_breakpoint(&mut self, _: &ToggleBreakpoint, cx: &mut ViewContext<Self>) {
|
||||
let cursor_position: Point = self.selections.newest(cx).head();
|
||||
self.toggle_breakpoint_at_row(cursor_position.row, cx);
|
||||
}
|
||||
|
||||
pub fn toggle_breakpoint_at_row(&mut self, row: u32, cx: &mut ViewContext<Self>) {
|
||||
let Some(project) = &self.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer) = self.buffer.read(cx).as_singleton() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let key = (buffer_id, row);
|
||||
|
||||
if self.breakpoints.remove(&key).is_none() {
|
||||
self.breakpoints.insert(
|
||||
key,
|
||||
Breakpoint {
|
||||
row: MultiBufferRow(row),
|
||||
_line: row,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.update_breakpoint(buffer, row + 1, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn gather_revert_changes(
|
||||
&mut self,
|
||||
selections: &[Selection<Anchor>],
|
||||
@@ -8085,7 +8149,7 @@ impl Editor {
|
||||
let mut selection_edit_ranges = Vec::new();
|
||||
let mut last_toggled_row = None;
|
||||
let snapshot = this.buffer.read(cx).read(cx);
|
||||
let empty_str: Arc<str> = Arc::default();
|
||||
let empty_str: Arc<str> = "".into();
|
||||
let mut suffixes_inserted = Vec::new();
|
||||
|
||||
fn comment_prefix_range(
|
||||
@@ -8914,6 +8978,31 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go_to_line<T: 'static>(
|
||||
&mut self,
|
||||
row: u32,
|
||||
column: u32,
|
||||
highlight_color: Option<Hsla>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let snapshot = self.snapshot(cx).display_snapshot;
|
||||
let point = snapshot
|
||||
.buffer_snapshot
|
||||
.clip_point(Point::new(row, column), Bias::Left);
|
||||
let anchor = snapshot.buffer_snapshot.anchor_before(point);
|
||||
self.clear_row_highlights::<T>();
|
||||
self.highlight_rows::<T>(
|
||||
anchor..=anchor,
|
||||
Some(
|
||||
highlight_color
|
||||
.unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
|
||||
),
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
self.request_autoscroll(Autoscroll::center(), cx);
|
||||
}
|
||||
|
||||
fn seek_in_direction(
|
||||
&mut self,
|
||||
snapshot: &DisplaySnapshot,
|
||||
@@ -10418,11 +10507,9 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_masked(&mut self, masked: bool, cx: &mut ViewContext<Self>) {
|
||||
if self.display_map.read(cx).masked != masked {
|
||||
self.display_map.update(cx, |map, _| map.masked = masked);
|
||||
}
|
||||
cx.notify()
|
||||
pub fn set_redact_all(&mut self, redact_all: bool, cx: &mut ViewContext<Self>) {
|
||||
self.redact_all = redact_all;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut ViewContext<Self>) {
|
||||
@@ -11108,6 +11195,10 @@ impl Editor {
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
cx: &WindowContext,
|
||||
) -> Vec<Range<DisplayPoint>> {
|
||||
if self.redact_all {
|
||||
return vec![DisplayPoint::zero()..display_snapshot.max_point()];
|
||||
}
|
||||
|
||||
display_snapshot
|
||||
.buffer_snapshot
|
||||
.redacted_ranges(search_range, |file| {
|
||||
@@ -12430,7 +12521,6 @@ impl Render for Editor {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
@@ -12440,7 +12530,6 @@ impl Render for Editor {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
|
||||
@@ -305,7 +305,7 @@ pub struct ScrollbarContent {
|
||||
}
|
||||
|
||||
/// Gutter related settings
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct GutterContent {
|
||||
/// Whether to show line numbers in the gutter.
|
||||
///
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{AppContext, FontFeatures, FontWeight};
|
||||
use gpui::{AppContext, FontWeight};
|
||||
use project::project_settings::{InlineBlameSettings, ProjectSettings};
|
||||
use settings::{EditableSettingControl, Settings};
|
||||
use theme::{FontFamilyCache, ThemeSettings};
|
||||
@@ -9,8 +7,6 @@ use ui::{
|
||||
SettingsGroup,
|
||||
};
|
||||
|
||||
use crate::EditorSettings;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct EditorSettingsControls {}
|
||||
|
||||
@@ -32,19 +28,9 @@ impl RenderOnce for EditorSettingsControls {
|
||||
.child(BufferFontFamilyControl)
|
||||
.child(BufferFontWeightControl),
|
||||
)
|
||||
.child(BufferFontSizeControl)
|
||||
.child(BufferFontLigaturesControl),
|
||||
.child(BufferFontSizeControl),
|
||||
)
|
||||
.child(SettingsGroup::new("Editor").child(InlineGitBlameControl))
|
||||
.child(
|
||||
SettingsGroup::new("Gutter").child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(LineNumbersControl)
|
||||
.child(RelativeLineNumbersControl),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,76 +190,6 @@ impl RenderOnce for BufferFontWeightControl {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct BufferFontLigaturesControl;
|
||||
|
||||
impl EditableSettingControl for BufferFontLigaturesControl {
|
||||
type Value = bool;
|
||||
type Settings = ThemeSettings;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Buffer Font Ligatures".into()
|
||||
}
|
||||
|
||||
fn read(cx: &AppContext) -> Self::Value {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
settings
|
||||
.buffer_font
|
||||
.features
|
||||
.is_calt_enabled()
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn apply(
|
||||
settings: &mut <Self::Settings as Settings>::FileContent,
|
||||
value: Self::Value,
|
||||
_cx: &AppContext,
|
||||
) {
|
||||
let value = if value { 1 } else { 0 };
|
||||
|
||||
let mut features = settings
|
||||
.buffer_font_features
|
||||
.as_ref()
|
||||
.map(|features| {
|
||||
features
|
||||
.tag_value_list()
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
|
||||
features[calt_index].1 = value;
|
||||
} else {
|
||||
features.push(("calt".into(), value));
|
||||
}
|
||||
|
||||
settings.buffer_font_features = Some(FontFeatures(Arc::new(features)));
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for BufferFontLigaturesControl {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let value = Self::read(cx);
|
||||
|
||||
CheckboxWithLabel::new(
|
||||
"buffer-font-ligatures",
|
||||
Label::new(self.name()),
|
||||
value.into(),
|
||||
|selection, cx| {
|
||||
Self::write(
|
||||
match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected | Selection::Indeterminate => false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct InlineGitBlameControl;
|
||||
|
||||
@@ -326,102 +242,3 @@ impl RenderOnce for InlineGitBlameControl {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct LineNumbersControl;
|
||||
|
||||
impl EditableSettingControl for LineNumbersControl {
|
||||
type Value = bool;
|
||||
type Settings = EditorSettings;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Line Numbers".into()
|
||||
}
|
||||
|
||||
fn read(cx: &AppContext) -> Self::Value {
|
||||
let settings = EditorSettings::get_global(cx);
|
||||
settings.gutter.line_numbers
|
||||
}
|
||||
|
||||
fn apply(
|
||||
settings: &mut <Self::Settings as Settings>::FileContent,
|
||||
value: Self::Value,
|
||||
_cx: &AppContext,
|
||||
) {
|
||||
if let Some(gutter) = settings.gutter.as_mut() {
|
||||
gutter.line_numbers = Some(value);
|
||||
} else {
|
||||
settings.gutter = Some(crate::editor_settings::GutterContent {
|
||||
line_numbers: Some(value),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for LineNumbersControl {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let value = Self::read(cx);
|
||||
|
||||
CheckboxWithLabel::new(
|
||||
"line-numbers",
|
||||
Label::new(self.name()),
|
||||
value.into(),
|
||||
|selection, cx| {
|
||||
Self::write(
|
||||
match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected | Selection::Indeterminate => false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct RelativeLineNumbersControl;
|
||||
|
||||
impl EditableSettingControl for RelativeLineNumbersControl {
|
||||
type Value = bool;
|
||||
type Settings = EditorSettings;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Relative Line Numbers".into()
|
||||
}
|
||||
|
||||
fn read(cx: &AppContext) -> Self::Value {
|
||||
let settings = EditorSettings::get_global(cx);
|
||||
settings.relative_line_numbers
|
||||
}
|
||||
|
||||
fn apply(
|
||||
settings: &mut <Self::Settings as Settings>::FileContent,
|
||||
value: Self::Value,
|
||||
_cx: &AppContext,
|
||||
) {
|
||||
settings.relative_line_numbers = Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for RelativeLineNumbersControl {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let value = Self::read(cx);
|
||||
|
||||
DropdownMenu::new(
|
||||
"relative-line-numbers",
|
||||
if value { "Relative" } else { "Ascending" },
|
||||
ContextMenu::build(cx, |menu, _cx| {
|
||||
menu.custom_entry(
|
||||
|_cx| Label::new("Ascending").into_any_element(),
|
||||
move |cx| Self::write(false, cx),
|
||||
)
|
||||
.custom_entry(
|
||||
|_cx| Label::new("Relative").into_any_element(),
|
||||
move |cx| Self::write(true, cx),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10105,7 +10105,7 @@ struct Row8;
|
||||
struct Row9;
|
||||
struct Row10;"#};
|
||||
|
||||
// Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
|
||||
// Deletion hunks trigger with carets on ajacent rows, so carets and selections have to stay farther to avoid the revert
|
||||
assert_hunk_revert(
|
||||
indoc! {r#"struct Row;
|
||||
struct Row2;
|
||||
|
||||
@@ -59,7 +59,6 @@ use std::{
|
||||
fmt::{self, Write},
|
||||
iter, mem,
|
||||
ops::{Deref, Range},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
@@ -401,7 +400,8 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::accept_partial_inline_completion);
|
||||
register_action(view, cx, Editor::accept_inline_completion);
|
||||
register_action(view, cx, Editor::revert_selected_hunks);
|
||||
register_action(view, cx, Editor::open_active_item_in_terminal)
|
||||
register_action(view, cx, Editor::open_active_item_in_terminal);
|
||||
register_action(view, cx, Editor::toggle_breakpoint);
|
||||
}
|
||||
|
||||
fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) {
|
||||
@@ -1555,6 +1555,45 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_breakpoints(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
snapshot: &EditorSnapshot,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<AnyElement> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.breakpoints
|
||||
.iter()
|
||||
.filter_map(|(_, breakpoint)| {
|
||||
if snapshot.is_line_folded(breakpoint.row) {
|
||||
return None;
|
||||
}
|
||||
let display_row = Point::new(breakpoint.row.0, 0)
|
||||
.to_display_point(snapshot)
|
||||
.row();
|
||||
let button = editor.render_breakpoint(display_row, cx);
|
||||
|
||||
let button = prepaint_gutter_button(
|
||||
button,
|
||||
display_row,
|
||||
line_height,
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
rows_with_hunk_bounds,
|
||||
cx,
|
||||
);
|
||||
Some(button)
|
||||
})
|
||||
.collect_vec()
|
||||
})
|
||||
}
|
||||
|
||||
fn layout_run_indicators(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -1970,7 +2009,6 @@ impl EditorElement {
|
||||
max_width: text_hitbox.size.width.max(*scroll_width),
|
||||
editor_style: &self.style,
|
||||
}))
|
||||
.cursor(CursorStyle::Arrow)
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -3254,6 +3292,9 @@ impl EditorElement {
|
||||
}
|
||||
});
|
||||
|
||||
for breakpoint in layout.breakpoints.iter_mut() {
|
||||
breakpoint.paint(cx);
|
||||
}
|
||||
for test_indicator in layout.test_indicators.iter_mut() {
|
||||
test_indicator.paint(cx);
|
||||
}
|
||||
@@ -4107,11 +4148,11 @@ fn prepaint_gutter_button(
|
||||
);
|
||||
let indicator_size = button.layout_as_root(available_space, cx);
|
||||
|
||||
let blame_width = gutter_dimensions.git_blame_entries_width;
|
||||
let gutter_width = rows_with_hunk_bounds
|
||||
let blame_offset = gutter_dimensions.git_blame_entries_width;
|
||||
let gutter_offset = rows_with_hunk_bounds
|
||||
.get(&row)
|
||||
.map(|bounds| bounds.size.width);
|
||||
let left_offset = blame_width.max(gutter_width).unwrap_or_default();
|
||||
.map(|bounds| bounds.origin.x + bounds.size.width);
|
||||
let left_offset = blame_offset.max(gutter_offset).unwrap_or(Pixels::ZERO);
|
||||
|
||||
let mut x = left_offset;
|
||||
let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
|
||||
@@ -5394,6 +5435,16 @@ impl Element for EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let breakpoints = self.layout_breakpoints(
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
&snapshot,
|
||||
cx,
|
||||
);
|
||||
|
||||
let test_indicators = if gutter_settings.runnables {
|
||||
self.layout_run_indicators(
|
||||
line_height,
|
||||
@@ -5494,7 +5545,7 @@ impl Element for EditorElement {
|
||||
|
||||
EditorLayout {
|
||||
mode: snapshot.mode,
|
||||
position_map: Rc::new(PositionMap {
|
||||
position_map: Arc::new(PositionMap {
|
||||
size: bounds.size,
|
||||
scroll_pixel_position,
|
||||
scroll_max,
|
||||
@@ -5529,6 +5580,7 @@ impl Element for EditorElement {
|
||||
selections,
|
||||
mouse_context_menu,
|
||||
test_indicators,
|
||||
breakpoints,
|
||||
close_indicators,
|
||||
code_actions_indicator,
|
||||
gutter_fold_toggles,
|
||||
@@ -5644,7 +5696,7 @@ impl IntoElement for EditorElement {
|
||||
}
|
||||
|
||||
pub struct EditorLayout {
|
||||
position_map: Rc<PositionMap>,
|
||||
position_map: Arc<PositionMap>,
|
||||
hitbox: Hitbox,
|
||||
text_hitbox: Hitbox,
|
||||
gutter_hitbox: Hitbox,
|
||||
@@ -5671,6 +5723,7 @@ pub struct EditorLayout {
|
||||
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||||
code_actions_indicator: Option<AnyElement>,
|
||||
test_indicators: Vec<AnyElement>,
|
||||
breakpoints: Vec<AnyElement>,
|
||||
close_indicators: Vec<AnyElement>,
|
||||
gutter_fold_toggles: Vec<Option<AnyElement>>,
|
||||
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
|
||||
|
||||
@@ -13,8 +13,8 @@ use multi_buffer::{
|
||||
use settings::SettingsStore;
|
||||
use text::{BufferId, Point};
|
||||
use ui::{
|
||||
div, h_flex, rems, v_flex, ActiveTheme, Context as _, ContextMenu, InteractiveElement,
|
||||
IntoElement, ParentElement, Pixels, Styled, ViewContext, VisualContext,
|
||||
div, h_flex, v_flex, ActiveTheme, Context as _, ContextMenu, InteractiveElement, IntoElement,
|
||||
ParentElement, Pixels, Styled, ViewContext, VisualContext,
|
||||
};
|
||||
use util::{debug_panic, RangeExt};
|
||||
|
||||
@@ -484,10 +484,7 @@ impl Editor {
|
||||
.child(
|
||||
h_flex()
|
||||
.id("gutter hunk")
|
||||
.pl(gutter_dimensions.margin
|
||||
+ gutter_dimensions
|
||||
.git_blame_entries_width
|
||||
.unwrap_or_default())
|
||||
.pl(hunk_bounds.origin.x)
|
||||
.max_w(hunk_bounds.size.width)
|
||||
.min_w(hunk_bounds.size.width)
|
||||
.size_full()
|
||||
@@ -515,7 +512,7 @@ impl Editor {
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.pt(rems(0.25))
|
||||
.pt(ui::rems(0.25))
|
||||
.justify_start()
|
||||
.child(close_button),
|
||||
),
|
||||
|
||||
@@ -44,7 +44,7 @@ impl SelectionsCollection {
|
||||
buffer,
|
||||
next_selection_id: 1,
|
||||
line_mode: false,
|
||||
disjoint: Arc::default(),
|
||||
disjoint: Arc::from([]),
|
||||
pending: Some(PendingSelection {
|
||||
selection: Selection {
|
||||
id: 0,
|
||||
@@ -398,7 +398,7 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
}
|
||||
|
||||
pub fn clear_disjoint(&mut self) {
|
||||
self.collection.disjoint = Arc::default();
|
||||
self.collection.disjoint = Arc::from([]);
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, selection_id: usize) {
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::Editor;
|
||||
use gpui::{Task as AsyncTask, WindowContext};
|
||||
use project::Location;
|
||||
use task::{TaskContext, TaskVariables, VariableName};
|
||||
use text::{ToOffset, ToPoint};
|
||||
use text::{Point, ToOffset, ToPoint};
|
||||
use workspace::Workspace;
|
||||
|
||||
fn task_context_with_editor(
|
||||
@@ -14,7 +14,11 @@ fn task_context_with_editor(
|
||||
return AsyncTask::ready(None);
|
||||
};
|
||||
let (selection, buffer, editor_snapshot) = {
|
||||
let selection = editor.selections.newest_adjusted(cx);
|
||||
let mut selection = editor.selections.newest::<Point>(cx);
|
||||
if editor.selections.line_mode {
|
||||
selection.start = Point::new(selection.start.row, 0);
|
||||
selection.end = Point::new(selection.end.row + 1, 0);
|
||||
}
|
||||
let Some((buffer, _, _)) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
|
||||
@@ -27,7 +27,6 @@ pub fn marked_display_snapshot(
|
||||
let font = Font {
|
||||
family: "Zed Plex Mono".into(),
|
||||
features: FontFeatures::default(),
|
||||
fallbacks: None,
|
||||
weight: FontWeight::default(),
|
||||
style: FontStyle::default(),
|
||||
};
|
||||
|
||||
@@ -327,7 +327,7 @@ impl EditorTestContext {
|
||||
.background_highlights
|
||||
.get(&TypeId::of::<Tag>())
|
||||
.map(|h| h.1.clone())
|
||||
.unwrap_or_else(|| Arc::default())
|
||||
.unwrap_or_else(|| Arc::from([]))
|
||||
.into_iter()
|
||||
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
||||
.collect()
|
||||
|
||||
@@ -21,6 +21,7 @@ assistant_slash_command.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
async-trait.workspace = true
|
||||
cap-std.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
fs.workspace = true
|
||||
|
||||
@@ -363,7 +363,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = store.update(cx, |store, cx| store.reload(None, cx));
|
||||
|
||||
cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
|
||||
|
||||
@@ -159,25 +159,29 @@ impl WasmHost {
|
||||
}
|
||||
|
||||
async fn build_wasi_ctx(&self, manifest: &Arc<ExtensionManifest>) -> Result<wasi::WasiCtx> {
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
|
||||
let extension_work_dir = self.work_dir.join(manifest.id.as_ref());
|
||||
self.fs
|
||||
.create_dir(&extension_work_dir)
|
||||
.await
|
||||
.context("failed to create extension work dir")?;
|
||||
|
||||
let file_perms = wasi::FilePerms::all();
|
||||
let work_dir_preopen = Dir::open_ambient_dir(&extension_work_dir, ambient_authority())
|
||||
.context("failed to preopen extension work directory")?;
|
||||
let current_dir_preopen = work_dir_preopen
|
||||
.try_clone()
|
||||
.context("failed to preopen extension current directory")?;
|
||||
let extension_work_dir = extension_work_dir.to_string_lossy();
|
||||
|
||||
let perms = wasi::FilePerms::all();
|
||||
let dir_perms = wasi::DirPerms::all();
|
||||
|
||||
Ok(wasi::WasiCtxBuilder::new()
|
||||
.inherit_stdio()
|
||||
.preopened_dir(&extension_work_dir, ".", dir_perms, file_perms)?
|
||||
.preopened_dir(
|
||||
&extension_work_dir,
|
||||
&extension_work_dir.to_string_lossy(),
|
||||
dir_perms,
|
||||
file_perms,
|
||||
)?
|
||||
.env("PWD", &extension_work_dir.to_string_lossy())
|
||||
.preopened_dir(current_dir_preopen, dir_perms, perms, ".")
|
||||
.preopened_dir(work_dir_preopen, dir_perms, perms, &extension_work_dir)
|
||||
.env("PWD", &extension_work_dir)
|
||||
.env("RUST_BACKTRACE", "full")
|
||||
.build())
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ pub fn new_linker(
|
||||
f: impl Fn(&mut Linker<WasmState>, fn(&mut WasmState) -> &mut WasmState) -> Result<()>,
|
||||
) -> Linker<WasmState> {
|
||||
let mut linker = Linker::new(&wasm_engine());
|
||||
wasmtime_wasi::add_to_linker_async(&mut linker).unwrap();
|
||||
wasmtime_wasi::command::add_to_linker(&mut linker).unwrap();
|
||||
f(&mut linker, wasi_view).unwrap();
|
||||
linker
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 1);
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
trappable_imports: true,
|
||||
path: "../extension_api/wit/since_v0.0.1",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
|
||||
@@ -11,7 +11,6 @@ pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 4);
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
trappable_imports: true,
|
||||
path: "../extension_api/wit/since_v0.0.4",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
|
||||
@@ -12,7 +12,6 @@ pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 6);
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
trappable_imports: true,
|
||||
path: "../extension_api/wit/since_v0.0.6",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
|
||||
@@ -26,7 +26,6 @@ pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 7);
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
trappable_imports: true,
|
||||
path: "../extension_api/wit/since_v0.0.7",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
|
||||
@@ -816,7 +816,6 @@ impl ExtensionsPage {
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(1.3),
|
||||
|
||||
@@ -998,7 +998,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("b0.5")),
|
||||
path_prefix: Arc::default(),
|
||||
path_prefix: Arc::from(""),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1006,7 +1006,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("c1.0")),
|
||||
path_prefix: Arc::default(),
|
||||
path_prefix: Arc::from(""),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1014,7 +1014,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("a1.0")),
|
||||
path_prefix: Arc::default(),
|
||||
path_prefix: Arc::from(""),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1022,7 +1022,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("a0.5")),
|
||||
path_prefix: Arc::default(),
|
||||
path_prefix: Arc::from(""),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1030,7 +1030,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("b1.0")),
|
||||
path_prefix: Arc::default(),
|
||||
path_prefix: Arc::from(""),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
];
|
||||
@@ -1044,7 +1044,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("a1.0")),
|
||||
path_prefix: Arc::default(),
|
||||
path_prefix: Arc::from(""),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1052,7 +1052,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("b1.0")),
|
||||
path_prefix: Arc::default(),
|
||||
path_prefix: Arc::from(""),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1060,7 +1060,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("c1.0")),
|
||||
path_prefix: Arc::default(),
|
||||
path_prefix: Arc::from(""),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1068,7 +1068,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("a0.5")),
|
||||
path_prefix: Arc::default(),
|
||||
path_prefix: Arc::from(""),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1076,7 +1076,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("b0.5")),
|
||||
path_prefix: Arc::default(),
|
||||
path_prefix: Arc::from(""),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -404,12 +404,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_match_multibyte_path_entries() {
|
||||
let paths = vec![
|
||||
"aαbβ/cγdδ",
|
||||
"αβγδ/bcde",
|
||||
"c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f",
|
||||
"/d/🆒/h",
|
||||
];
|
||||
let paths = vec!["aαbβ/cγdδ", "αβγδ/bcde", "c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", "/d/🆒/h"];
|
||||
assert_eq!("1️⃣".len(), 7);
|
||||
assert_eq!(
|
||||
match_single_path_query("bcd", false, &paths),
|
||||
|
||||
@@ -120,7 +120,7 @@ pub fn match_fixed_path_set(
|
||||
worktree_id,
|
||||
positions: Vec::new(),
|
||||
path: Arc::from(candidate.path),
|
||||
path_prefix: Arc::default(),
|
||||
path_prefix: Arc::from(""),
|
||||
distance_to_relative_ancestor: usize::MAX,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -5,9 +5,6 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/google_ai.rs"
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ thiserror.workspace = true
|
||||
time.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
waker-fn = "1.2.0"
|
||||
waker-fn = "1.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
backtrace = "0.3"
|
||||
@@ -93,7 +93,6 @@ cbindgen = { version = "0.26.0", default-features = false }
|
||||
block = "0.1"
|
||||
cocoa.workspace = true
|
||||
core-foundation.workspace = true
|
||||
core-foundation-sys = "0.8"
|
||||
core-graphics = "0.23"
|
||||
core-text = "20.1"
|
||||
foreign-types = "0.5"
|
||||
@@ -151,7 +150,7 @@ x11-clipboard = "0.9.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
windows-core = "0.58"
|
||||
windows-core = "0.57"
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
use gpui::{
|
||||
div, img, prelude::*, App, AppContext, ImageSource, Render, ViewContext, WindowOptions,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
struct GifViewer {
|
||||
gif_path: PathBuf,
|
||||
}
|
||||
|
||||
impl GifViewer {
|
||||
fn new(gif_path: PathBuf) -> Self {
|
||||
Self { gif_path }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GifViewer {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().size_full().child(
|
||||
img(ImageSource::File(self.gif_path.clone().into()))
|
||||
.size_full()
|
||||
.object_fit(gpui::ObjectFit::Contain)
|
||||
.id("gif"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
let cwd = std::env::current_dir().expect("Failed to get current working directory");
|
||||
let gif_path = cwd.join("crates/gpui/examples/image/black-cat-typing.gif");
|
||||
|
||||
if !gif_path.exists() {
|
||||
eprintln!("Image file not found at {:?}", gif_path);
|
||||
eprintln!("Make sure you're running this example from the root of the gpui crate");
|
||||
cx.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
focus: true,
|
||||
..Default::default()
|
||||
},
|
||||
|cx| cx.new_view(|_cx| GifViewer::new(gif_path)),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.3 MiB |
@@ -1,7 +1,6 @@
|
||||
use crate::{size, DevicePixels, Result, SharedString, Size};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use image::{Delay, Frame};
|
||||
use image::RgbaImage;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt,
|
||||
@@ -35,54 +34,43 @@ pub struct ImageId(usize);
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
pub(crate) struct RenderImageParams {
|
||||
pub(crate) image_id: ImageId,
|
||||
pub(crate) frame_index: usize,
|
||||
}
|
||||
|
||||
/// A cached and processed image.
|
||||
pub struct ImageData {
|
||||
/// The ID associated with this image
|
||||
pub id: ImageId,
|
||||
data: SmallVec<[Frame; 1]>,
|
||||
data: RgbaImage,
|
||||
}
|
||||
|
||||
impl ImageData {
|
||||
/// Create a new image from the given data.
|
||||
pub fn new(data: impl Into<SmallVec<[Frame; 1]>>) -> Self {
|
||||
pub fn new(data: RgbaImage) -> Self {
|
||||
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
Self {
|
||||
id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
|
||||
data: data.into(),
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this image into a byte slice.
|
||||
pub fn as_bytes(&self, frame_index: usize) -> &[u8] {
|
||||
&self.data[frame_index].buffer()
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Get the size of this image, in pixels.
|
||||
pub fn size(&self, frame_index: usize) -> Size<DevicePixels> {
|
||||
let (width, height) = self.data[frame_index].buffer().dimensions();
|
||||
/// Get the size of this image, in pixels
|
||||
pub fn size(&self) -> Size<DevicePixels> {
|
||||
let (width, height) = self.data.dimensions();
|
||||
size(width.into(), height.into())
|
||||
}
|
||||
|
||||
/// Get the delay of this frame from the previous
|
||||
pub fn delay(&self, frame_index: usize) -> Delay {
|
||||
self.data[frame_index].delay()
|
||||
}
|
||||
|
||||
/// Get the number of frames for this image.
|
||||
pub fn frame_count(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ImageData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ImageData")
|
||||
.field("id", &self.id)
|
||||
.field("size", &self.size(0))
|
||||
.field("size", &self.data.dimensions())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,14 +323,14 @@ impl Interactivity {
|
||||
pub fn on_boxed_action(
|
||||
&mut self,
|
||||
action: &dyn Action,
|
||||
listener: impl Fn(&dyn Action, &mut WindowContext) + 'static,
|
||||
listener: impl Fn(&Box<dyn Action>, &mut WindowContext) + 'static,
|
||||
) {
|
||||
let action = action.boxed_clone();
|
||||
self.action_listeners.push((
|
||||
(*action).type_id(),
|
||||
Box::new(move |_, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble {
|
||||
(listener)(&*action, cx)
|
||||
(listener)(&action, cx)
|
||||
}
|
||||
}),
|
||||
));
|
||||
@@ -757,7 +757,7 @@ pub trait InteractiveElement: Sized {
|
||||
fn on_boxed_action(
|
||||
mut self,
|
||||
action: &dyn Action,
|
||||
listener: impl Fn(&dyn Action, &mut WindowContext) + 'static,
|
||||
listener: impl Fn(&Box<dyn Action>, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.interactivity().on_boxed_action(action, listener);
|
||||
self
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element,
|
||||
ElementId, GlobalElementId, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement,
|
||||
@@ -5,20 +9,11 @@ use crate::{
|
||||
WindowContext,
|
||||
};
|
||||
use futures::{AsyncReadExt, Future};
|
||||
use http_client;
|
||||
use image::{
|
||||
codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat,
|
||||
};
|
||||
use image::{ImageBuffer, ImageError};
|
||||
#[cfg(target_os = "macos")]
|
||||
use media::core_video::CVImageBuffer;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
fs,
|
||||
io::Cursor,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use http_client;
|
||||
use thiserror::Error;
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -235,14 +230,8 @@ impl Img {
|
||||
}
|
||||
}
|
||||
|
||||
/// The image state between frames
|
||||
struct ImgState {
|
||||
frame_index: usize,
|
||||
last_frame_time: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Element for Img {
|
||||
type RequestLayoutState = usize;
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
@@ -254,65 +243,29 @@ impl Element for Img {
|
||||
global_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
cx.with_optional_element_state(global_id, |state, cx| {
|
||||
let mut state = state.map(|state| {
|
||||
state.unwrap_or(ImgState {
|
||||
frame_index: 0,
|
||||
last_frame_time: None,
|
||||
})
|
||||
});
|
||||
|
||||
let frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
|
||||
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(global_id, cx, |mut style, cx| {
|
||||
if let Some(data) = self.source.data(cx) {
|
||||
if let Some(state) = &mut state {
|
||||
let frame_count = data.frame_count();
|
||||
if frame_count > 1 {
|
||||
let current_time = Instant::now();
|
||||
if let Some(last_frame_time) = state.last_frame_time {
|
||||
let elapsed = current_time - last_frame_time;
|
||||
let frame_duration =
|
||||
Duration::from(data.delay(state.frame_index));
|
||||
|
||||
if elapsed >= frame_duration {
|
||||
state.frame_index = (state.frame_index + 1) % frame_count;
|
||||
state.last_frame_time =
|
||||
Some(current_time - (elapsed - frame_duration));
|
||||
}
|
||||
} else {
|
||||
state.last_frame_time = Some(current_time);
|
||||
}
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(global_id, cx, |mut style, cx| {
|
||||
if let Some(data) = self.source.data(cx) {
|
||||
let image_size = data.size();
|
||||
match (style.size.width, style.size.height) {
|
||||
(Length::Auto, Length::Auto) => {
|
||||
style.size = Size {
|
||||
width: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
|
||||
)),
|
||||
height: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
let image_size = data.size(frame_index);
|
||||
match (style.size.width, style.size.height) {
|
||||
(Length::Auto, Length::Auto) => {
|
||||
style.size = Size {
|
||||
width: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
|
||||
)),
|
||||
height: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
|
||||
)),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if global_id.is_some() && data.frame_count() > 1 {
|
||||
cx.request_animation_frame();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
cx.request_layout(style, [])
|
||||
});
|
||||
|
||||
((layout_id, frame_index), state)
|
||||
})
|
||||
cx.request_layout(style, [])
|
||||
});
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
@@ -330,7 +283,7 @@ impl Element for Img {
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
frame_index: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
@@ -340,15 +293,9 @@ impl Element for Img {
|
||||
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||
|
||||
if let Some(data) = source.data(cx) {
|
||||
let new_bounds = self.object_fit.get_bounds(bounds, data.size(*frame_index));
|
||||
cx.paint_image(
|
||||
new_bounds,
|
||||
corner_radii,
|
||||
data.clone(),
|
||||
*frame_index,
|
||||
self.grayscale,
|
||||
)
|
||||
.log_err();
|
||||
let new_bounds = self.object_fit.get_bounds(bounds, data.size());
|
||||
cx.paint_image(new_bounds, corner_radii, data.clone(), self.grayscale)
|
||||
.log_err();
|
||||
}
|
||||
|
||||
match source {
|
||||
@@ -438,34 +385,12 @@ impl Asset for Image {
|
||||
};
|
||||
|
||||
let data = if let Ok(format) = image::guess_format(&bytes) {
|
||||
let data = match format {
|
||||
ImageFormat::Gif => {
|
||||
let decoder = GifDecoder::new(Cursor::new(&bytes))?;
|
||||
let mut frames = SmallVec::new();
|
||||
let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
|
||||
|
||||
for frame in decoder.into_frames() {
|
||||
let mut frame = frame?;
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
frames.push(frame);
|
||||
}
|
||||
|
||||
frames
|
||||
}
|
||||
_ => {
|
||||
let mut data =
|
||||
image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
|
||||
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in data.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
|
||||
SmallVec::from_elem(Frame::new(data), 1)
|
||||
}
|
||||
};
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in data.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
|
||||
ImageData::new(data)
|
||||
} else {
|
||||
@@ -475,7 +400,7 @@ impl Asset for Image {
|
||||
let buffer =
|
||||
ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
|
||||
|
||||
ImageData::new(SmallVec::from_elem(Frame::new(buffer), 1))
|
||||
ImageData::new(buffer)
|
||||
};
|
||||
|
||||
Ok(Arc::new(data))
|
||||
|
||||
@@ -180,7 +180,7 @@ impl Transformation {
|
||||
}
|
||||
|
||||
fn into_matrix(self, center: Point<Pixels>, scale_factor: f32) -> TransformationMatrix {
|
||||
//Note: if you read this as a sequence of matrix multiplications, start from the bottom
|
||||
//Note: if you read this as a sequence of matrix mulitplications, start from the bottom
|
||||
TransformationMatrix::unit()
|
||||
.translate(center.scale(scale_factor) + self.translate.scale(scale_factor))
|
||||
.rotate(self.rotate)
|
||||
|
||||
@@ -325,9 +325,7 @@ impl UniformList {
|
||||
|
||||
let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
|
||||
let mut items = (self.render_items)(item_ix..item_ix + 1, cx);
|
||||
let Some(mut item_to_measure) = items.pop() else {
|
||||
return Size::default();
|
||||
};
|
||||
let mut item_to_measure = items.pop().unwrap();
|
||||
let available_space = size(
|
||||
list_width.map_or(AvailableSpace::MinContent, |width| {
|
||||
AvailableSpace::Definite(width)
|
||||
|
||||
@@ -940,15 +940,6 @@ where
|
||||
pub fn half_perimeter(&self) -> T {
|
||||
self.size.width.clone() + self.size.height.clone()
|
||||
}
|
||||
|
||||
/// centered_at creates a new bounds centered at the given point.
|
||||
pub fn centered_at(center: Point<T>, size: Size<T>) -> Self {
|
||||
let origin = Point {
|
||||
x: center.x - size.width.half(),
|
||||
y: center.y - size.height.half(),
|
||||
};
|
||||
Self::new(origin, size)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
mod app_menu;
|
||||
mod keystroke;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod cosmic_text;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
|
||||
@@ -48,6 +51,8 @@ use uuid::Uuid;
|
||||
pub use app_menu::*;
|
||||
pub use keystroke::*;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub(crate) use cosmic_text::*;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) use linux::*;
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -100,6 +105,7 @@ pub fn guess_compositor() -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
// todo("windows")
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
|
||||
Rc::new(WindowsPlatform::new())
|
||||
@@ -407,6 +413,8 @@ pub(crate) trait PlatformTextSystem: Send + Sync {
|
||||
raster_bounds: Bounds<DevicePixels>,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)>;
|
||||
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
|
||||
#[cfg(target_os = "windows")]
|
||||
fn destroy(&self);
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
@@ -706,6 +714,7 @@ pub(crate) struct WindowParams {
|
||||
|
||||
pub display_id: Option<DisplayId>,
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub window_min_size: Option<Size<Pixels>>,
|
||||
}
|
||||
|
||||
|
||||
3
crates/gpui/src/platform/cosmic_text.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod text_system;
|
||||
|
||||
pub(crate) use text_system::*;
|
||||
@@ -64,17 +64,13 @@ impl PlatformTextSystem for CosmicTextSystem {
|
||||
}
|
||||
|
||||
fn all_font_names(&self) -> Vec<String> {
|
||||
let mut result = self
|
||||
.0
|
||||
self.0
|
||||
.read()
|
||||
.font_system
|
||||
.db()
|
||||
.faces()
|
||||
.filter_map(|face| face.families.first().map(|family| family.0.clone()))
|
||||
.collect_vec();
|
||||
result.sort();
|
||||
result.dedup();
|
||||
result
|
||||
.map(|face| face.post_script_name.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn all_font_families(&self) -> Vec<String> {
|
||||
@@ -181,6 +177,9 @@ impl PlatformTextSystem for CosmicTextSystem {
|
||||
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
|
||||
self.0.write().layout_line(text, font_size, runs)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn destroy(&self) {}
|
||||
}
|
||||
|
||||
impl CosmicTextSystemState {
|
||||