Compare commits

...

40 Commits

Author SHA1 Message Date
Conrad Irwin
1c010cd369 revert back to SendKeystrokes to show off that feature
Also remove !menu as it's unlikely to be a problem in this case and
muddies the example
2025-06-03 19:12:04 -06:00
Michael Sloan
0437ecc901 "vim::SwitchToNormalMode" instead of ["workspace::SendKeystrokes", "escape"] 2025-05-29 16:23:53 -06:00
VladKopylets
a804e57bd4 fix: return old binding 2025-05-22 15:59:48 +03:00
VladKopylets
7bd38164fc fix: wrong indentation 2025-05-22 13:26:55 +03:00
VladKopylets
828129c46c fix: comment line 2025-05-22 11:12:53 +03:00
VladKopylets
dde5fac4b9 fix: "J" key latency in vim mode 2025-05-22 11:07:41 +03:00
Cole Miller
71fb17c507 debugger: Update the default layout (#31057)
- Remove the modules list and loaded sources list from the default
layout
- Move the console to the center pane so it's visible initially

Release Notes:

- Debugger Beta: changed the default layout of the debugger panel,
hiding the modules list and loaded sources list by default and making
the console more prominent.

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-05-22 04:32:44 +00:00
Cole Miller
97e437c632 Remove test-support feature from auto_update's gpui dep (#31147)
Fixes `cargo run` on main.

Release Notes:

- N/A
2025-05-22 03:32:29 +00:00
Jon Gretar Borgthorsson
66667d1eef Add kernel detection for language support of runnable markdown cells (#29664)
Closes #27757

Release Notes:

- List of runnable markdown cells is now based on detected jupyter
kernels instead of hardcoded to Python and TypeScript
2025-05-21 20:23:05 -07:00
Remco Smits
dce22a965e project search: Reduce clones and allocations (#31133)
Release Notes:

- N/A
2025-05-21 22:11:00 -04:00
Cole Miller
5f452dbca2 debugger: Add a couple more keybindings (#31103)
- Add missing handler for `debugger::Continue` so `f5` works
- Add bindings based on VS Code for `debugger::Restart` and
`debug_panel::ToggleFocus`
- Remove breakpoint-related buttons from the debug panel's top strip,
and surface the bindings for `editor::ToggleBreakpoint` in gutter
tooltip instead

Release Notes:

- Debugger Beta: Added keybindings for `debugger::Continue`,
`debugger::Restart`, and `debug_panel::ToggleFocus`.
- Debugger Beta: Removed breakpoint-related buttons from the top of the
debug panel.
- Compatibility note: on Linux, `ctrl-shift-d` is now bound to
`debug_panel::ToggleFocus` by default, instead of
`editor::DuplicateLineDown`.
2025-05-22 00:59:44 +00:00
Cole Miller
b2a92097ee debugger: Add actions and keybindings for opening the thread and session menus (#31135)
Makes it possible to open and navigate these menus from the keyboard.

I also removed the eager previewing behavior for the thread picker,
which was buggy and came with a jarring layout shift.

Release Notes:

- Debugger Beta: Added the `debugger: open thread picker` and `debugger:
open session picker` actions.
2025-05-21 20:56:39 -04:00
Marshall Bowers
eb35d25a7d collab: Drop billing_events table (#31131)
This PR drops the `billing_events` table, as we're no longer using it.

Release Notes:

- N/A
2025-05-21 22:43:46 +00:00
smit
8742d4ab90 editor: Fix regression causing incorrect delimiter on newline in case of multiple comment prefixes (#31129)
Closes #31115

This fixes regression caused by
https://github.com/zed-industries/zed/pull/30824 while keeping that fix.

- [x] Test

Release Notes:

- Fixed the issue where adding a newline after the `///` comment would
extend it with `//` instead of `///` in Rust and other similar
languages.
2025-05-22 03:56:20 +05:30
Marshall Bowers
b829f72c17 collab: Prefer the plan on the subscription over the one on the usage (#31127)
This PR makes it so we always prefer the plan on the subscription.

The plan stored on the subscription usage is informational only.

Release Notes:

- N/A
2025-05-21 22:00:19 +00:00
Marshall Bowers
ffa8310d04 collab: Drop monthly_usages and lifetime_usages tables (#31124)
This PR drops the `monthly_usages` and `lifetime_usages` tables from the
LLM database, as they are no longer used.

Release Notes:

- N/A
2025-05-21 21:55:48 +00:00
Joseph T. Lyons
3fda539c46 Allow updater to check for updates after downloading one (#31066)
This PR brings back https://github.com/zed-industries/zed/pull/30969 and
adds some initial testing.

https://github.com/zed-industries/zed/pull/30969 did indeed allow Zed to
continue doing downloads after downloading one, but it introduced a bug
where Zed would download a new binary every time it polled, even if the
version was the same as the running instance.

This code could use a refactor to allow more / better testing, but this
is a start.

Release Notes:

- N/A
2025-05-21 17:54:46 -04:00
Marshall Bowers
b444b326cb collab: Remove GET /billing/monthly_spend endpoint (#31123)
This PR removes the `GET /billing/monthly_spend` endpoint, as it is no
longer used.

Release Notes:

- N/A
2025-05-21 21:30:12 +00:00
Peter Tripp
f196288e2d docs: Fix broken link in ai/configuration (#31119)
Release Notes:

- N/A
2025-05-21 20:37:12 +00:00
Rob McBroom
e30cc131b4 Rename 'Quit' to 'Quit Zed' in macOS menu (#31109)
This is standard for Mac apps.

I should have included this with [my other
PR](https://github.com/zed-industries/zed/pull/30697), but didn’t catch
it. 🤦🏻‍♂️

Release Notes:

- N/A
2025-05-21 20:01:08 +00:00
morgankrey
09c8a84935 docs: Link to models supported directly from table (#31112)
Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-21 15:30:09 -04:00
Kirill Bulatov
6e5996a815 Fix unzipping clangd and codelldb on Windows (#31080)
Closes https://github.com/zed-industries/zed/pull/30454

Release Notes:

- N/A
2025-05-21 21:17:14 +03:00
Marshall Bowers
c8f56e38b1 Update Cargo.lock (#31105)
This PR updates the `Cargo.lock` file, as running `cargo check` was
producing a diff on `main`.

Release Notes:

- N/A
2025-05-21 17:32:23 +00:00
Max Brunsfeld
cfd3b0ff7b Meter edit predictions by acceptance in free plan (#30984)
TODO:

- [x] Release  a new version of `zed_llm_client`

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-21 17:11:42 +00:00
hrou0003
afe23cf85a Canonicalize markdown link paths (#29119)
Closes #28657

Release Notes:

- Fixed markdown preview not canonicalizing file paths
2025-05-21 12:57:51 -04:00
Umesh Yadav
f915c24279 copilot: Fix rate limit due to Copilot-Vision-Request header (#30989)
Issues: #30994

I've implemented an important optimisation in response to GitHub
Copilot's recent rate limit on concurrent Vision API calls. Previously,
our system was defaulting to vision header: true for all API calls. To
prevent unnecessary calls and adhere to the new limits, I've updated our
logic: the vision header is now only sent if the current message is a
vision message, specifically when the preceding message includes an
image.

Prompt used to reproduce and verify the fix: `Give me a context for my
agent crate about. Browse my repo.`

Release Notes:

- copilot: Set Copilot-Vision-Request header based on message content
2025-05-21 12:51:35 -04:00
Joseph T. Lyons
bdd9e015ab Bump Zed to v0.189 (#31101)
Release Notes:

-N/A
2025-05-21 15:38:20 +00:00
smit
6bbab4b55a editor: Fix multi-cursor not added to lines shorter than current cursor column (#31100)
Closes #5255, #1046, #28322, #15728

This PR makes `AddSelectionBelow` and `AddSelectionAbove` not skip lines
that are shorter than the current cursor column. This follows the same
behavior as VSCode and Sublime.

This change is only applicable in the case of an empty selection; if
there is a non-empty selection, it continues to skip empty and shorter
lines to create a Vim-like column selection, which is the better default
for that case.

- [x] Tests

The empty selection no longer skips shorter lines:


https://github.com/user-attachments/assets/4bde2357-20b6-44f2-a9d9-b595c12d3939

Non-empty selection continues to skip shorter lines.


https://github.com/user-attachments/assets/4cd47c9f-b698-40fc-ad50-f2bf64f5519b

Release Notes:

- Improved `AddSelectionBelow` and `AddSelectionAbove` to no longer skip
shorter lines when the selection is empty, aligning with VSCode and
Sublime behavior.
2025-05-21 21:06:33 +05:30
smit
7450b788f3 editor: Prevent overlapping of signature/hover popovers and context menus (#31090)
Closes #29358

If hover popovers or signature popovers ever clash with the context menu
(like code completion or code actions), they find the best spot by
trying different directions around the context menu to show the popover.
If they can’t find a good spot, they just overlap with the context menu.

Not overlapping state:
<img width="350" alt="image"
src="https://github.com/user-attachments/assets/2f1bdc4c-eb01-405c-b5fb-eb28eadc9957"
/>

Overlapping case, moves popover to bottom of context menu:
<img width="350" alt="image"
src="https://github.com/user-attachments/assets/3ce4be23-7701-4711-b604-5e29682360e1"
/>

Overlapping case, moves popover to right of context menu:
<img width="350" alt="image"
src="https://github.com/user-attachments/assets/60d47518-e412-4d64-9d17-a69a17248bdf"
/> <img width="350" alt="image"
src="https://github.com/user-attachments/assets/2a3de176-7443-46d8-99d1-b2973a0ffaa6"
/>

Overlapping case, moves popover to left of context menu:
<img width="350" alt="image"
src="https://github.com/user-attachments/assets/015b799b-8c6e-4405-aee6-e205d4caebec"
/>

Overlapping case, moves popover to top of context menu:
<img width="350" alt="image"
src="https://github.com/user-attachments/assets/fbd03d84-9a49-44eb-846b-a9852d2ff43e"
/>

Release Notes:

- Fixed an issue where hover popovers or signature popovers would
overlap with existing opened completion or code actions context menus.
2025-05-21 18:45:00 +05:30
Anthony Eid
0c03519393 Fix project search panic (#31089)
The panic occurred when querying a second search in the project search
multibuffer while there were dirty buffers.

The panic only happened in Nightly so there's no release notes 

Release Notes:

- N/A
2025-05-21 12:42:20 +00:00
Joseph T. Lyons
636eff2e9a Revert "Allow updater to check for updates after downloading one (#30969)" (#31086)
This reverts commit 5c4f9e57d8.

Release Notes:

- N/A
2025-05-21 12:37:03 +00:00
Julia Ryan
6c8f4002d9 nix: Prevent spurious bindgen rebuilds in the devshell (#31083)
Release Notes:

- N/A
2025-05-21 11:18:14 +00:00
Oleksiy Syvokon
91bc5aefa4 evals: Add system prompt to edit agent evals + fix edit agent (#31082)
1. Add system prompt: this is how it's called from threads. Previously,
we were sending
2. Fix an issue with writing agent thought into a newly created empty
file.

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-05-21 10:14:58 +00:00
Aleksei Gusev
2f3564b85f Add icons to the built-in picker for Open (#30893)
![image](https://github.com/user-attachments/assets/f1167251-627f-48f7-a948-25c06c842e4b)


Release Notes:

- Added icons to the built-in picker for `Open` dialog
2025-05-21 13:07:22 +03:00
Aleksei Gusev
d61a544400 Fix Replace Next Match command (#30890)
Currently, `search::ReplaceNext` works only first time it is executed
because Zed switches the focus to the editor. It seems
`self.editor_focus` call is unnecessary.

Closes #17466

Release Notes:

- Fixed `Replace Next Match` command. Previously it worked once, then
Zed incorrectly switched the focus to the editor


https://github.com/user-attachments/assets/66ef61d6-1efe-43ca-8d8c-6b40540a9930
2025-05-21 13:05:44 +03:00
Adam Sherwood
8061bacee3 Add excluded_files to pane::DeploySearch (#30699)
In accordance with #30327, I saw no reason for included files to get
special treatment, and I actually get use out of prefilling excluded
files because I like not to search symlinked files which, in my
workflow, use a naming convention.

This is simply implementing the same exact changes, but for excluded. It
was tested with `"space /": ["pane::DeploySearch", { "excluded_files":
"**/_*.tf" }]` and works just fine.

Release Notes:

- Added `excluded_files` to `pane::DeploySearch`.
2025-05-21 13:03:39 +03:00
Piotr Osiewicz
77dadfedfe chore: Make terminal_view own the TerminalSlashCommand (#31070)
This reduces 'touch crates/editor/src/editor.rs && cargo +nightly build'
from 8.9s to 8.5s. That same scenario used to take 8s less than a week
ago. :)
I'm measuring with nightly rustc, because it's compile times are better
than those of stable thanks to
https://github.com/rust-lang/rust/pull/138522

main (8.2s total):

![image](https://github.com/user-attachments/assets/767a2ac4-7bba-4147-bd16-9b09eed5b433)

[cargo-timing.html.zip](https://github.com/user-attachments/files/20364175/cargo-timing.html.zip)

#22be776 (7.5s total):

[cargo-timing-20250521T085303.892834Z.html.zip](https://github.com/user-attachments/files/20364391/cargo-timing-20250521T085303.892834Z.html.zip)

![image](https://github.com/user-attachments/assets/c4476df9-cb6e-4403-b0db-de00521f1fd0)


Release Notes:

- N/A
2025-05-21 09:27:54 +00:00
Ben Brandt
0023b37bfc extension_host: fix missing debug adapters (#31069)
Missed because of lack of rebase

Release Notes:

- N/A
2025-05-21 09:01:18 +00:00
Ben Brandt
4ece4a635f extension_host: Use wasmtime incremental compilation (#30948)
Builds on top of https://github.com/zed-industries/zed/pull/30942

This turns on incremental compilation and decreases extension
compilation times by up to another 41%
Putting us at roughly 92% improved extension load times from what is in
the app today.

Because we only have a static engine, I can't reset the cache between
every run. So technically the benchmarks are always running with a
warmed cache. So the first extension we load will take the 8.8ms, and
then any subsequent extensions will be closer to the measured time in
this benchmark.

This is also measuring the entire load process, not just the
compilation. However, since this is the loading we likely think of when
thinking about extensions, I felt it was likely more helpful to see the
impact on the overall time.

This works because our extensions are largely the same Wasm bytecode
(SDK code + std lib functions etc) with minor changes in the trait impl.
The more different that extensions implementation is, there will be less
benefit, however, there will always be a large part of every extension
that is always the same across extensions, so this should be a speedup
regardless.

I used `moka` to provide a bound to the cache. We could use a bare
`DashMap`, however if there was some issue this could lead to a memory
leak. `moka` has some slight overhead, but makes sure that we don't go
over 32mb while using an LRU-style mechanism for deciding which
compilation artifacts to keep.

I measured our current extensions to take roughly 512kb in the cache.
Which means with a cap of 32mb, we can keep roughly 64 *completely
novel* extensions with no overlap. Since our extensions will have more
overlap than this though, we can actually keep much more in the cache
without having to worry about it.

#### Before:

```
load/1                  time:   [8.8301 ms 8.8616 ms 8.8931 ms]
                        change: [-0.1880% +0.3221% +0.8679%] (p = 0.23 > 0.05)
                        No change in performance detected.
```

#### After:

```
load/1                  time:   [5.1575 ms 5.1726 ms 5.1876 ms]
                        change: [-41.894% -41.628% -41.350%] (p = 0.00 < 0.05)
                        Performance has improved.
```

Release Notes:

- N/A
2025-05-21 10:12:16 +02:00
Jonathan LEI
77c2aecf93 Fix socks proxy local DNS resolution not respected (#30619)
Closes #30618

Release Notes:

- Fixed SOCKS proxy incorrectly always uses remote DNS resolution.
2025-05-21 14:55:39 +08:00
87 changed files with 1797 additions and 849 deletions

102
Cargo.lock generated
View File

@@ -86,7 +86,6 @@ dependencies = [
"jsonschema",
"language",
"language_model",
"language_model_selector",
"log",
"lsp",
"markdown",
@@ -492,6 +491,7 @@ dependencies = [
"collections",
"context_server",
"editor",
"feature_flags",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -499,17 +499,18 @@ dependencies = [
"indexed_docs",
"language",
"language_model",
"language_model_selector",
"languages",
"log",
"multi_buffer",
"open_ai",
"ordered-float 2.10.1",
"parking_lot",
"paths",
"picker",
"pretty_assertions",
"project",
"prompt_store",
"proto",
"rand 0.8.5",
"regex",
"rope",
@@ -611,7 +612,6 @@ dependencies = [
"serde_json",
"settings",
"smol",
"terminal_view",
"text",
"toml 0.8.20",
"ui",
@@ -688,6 +688,7 @@ dependencies = [
"portable-pty",
"pretty_assertions",
"project",
"prompt_store",
"rand 0.8.5",
"regex",
"reqwest_client",
@@ -3636,9 +3637,12 @@ dependencies = [
"gimli",
"hashbrown 0.14.5",
"log",
"postcard",
"regalloc2",
"rustc-hash 2.1.1",
"serde",
"serde_derive",
"sha2",
"smallvec",
"target-lexicon 0.13.2",
]
@@ -4052,6 +4056,7 @@ dependencies = [
"paths",
"serde",
"serde_json",
"smol",
"task",
"util",
"workspace-hack",
@@ -5016,6 +5021,7 @@ dependencies = [
"shellexpand 2.1.2",
"smol",
"telemetry",
"terminal_view",
"toml 0.8.20",
"unindent",
"util",
@@ -5154,6 +5160,7 @@ dependencies = [
"language_extension",
"log",
"lsp",
"moka",
"node_runtime",
"parking_lot",
"paths",
@@ -5910,6 +5917,20 @@ dependencies = [
"thread_local",
]
[[package]]
name = "generator"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827"
dependencies = [
"cc",
"cfg-if",
"libc",
"log",
"rustversion",
"windows 0.61.1",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -8753,25 +8774,6 @@ dependencies = [
"zed_llm_client",
]
[[package]]
name = "language_model_selector"
version = "0.1.0"
dependencies = [
"collections",
"feature_flags",
"futures 0.3.31",
"fuzzy",
"gpui",
"language_model",
"log",
"ordered-float 2.10.1",
"picker",
"proto",
"ui",
"workspace-hack",
"zed_actions",
]
[[package]]
name = "language_models"
version = "0.1.0"
@@ -9348,6 +9350,19 @@ dependencies = [
"logos-codegen",
]
[[package]]
name = "loom"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "loop9"
version = "0.1.5"
@@ -9526,6 +9541,7 @@ dependencies = [
"async-recursion 1.1.1",
"collections",
"editor",
"fs",
"gpui",
"language",
"linkify",
@@ -9846,6 +9862,25 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "moka"
version = "0.12.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926"
dependencies = [
"crossbeam-channel",
"crossbeam-epoch",
"crossbeam-utils",
"loom",
"parking_lot",
"portable-atomic",
"rustc_version",
"smallvec",
"tagptr",
"thiserror 1.0.69",
"uuid",
]
[[package]]
name = "msvc_spectre_libs"
version = "0.1.3"
@@ -10043,7 +10078,6 @@ dependencies = [
"async-tar",
"async-trait",
"async-watch",
"async_zip",
"futures 0.3.31",
"http_client",
"log",
@@ -10052,9 +10086,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
"tempfile",
"util",
"walkdir",
"which 6.0.3",
"workspace-hack",
]
@@ -12782,6 +12814,7 @@ dependencies = [
"hashbrown 0.15.3",
"log",
"rustc-hash 2.1.1",
"serde",
"smallvec",
]
@@ -15433,6 +15466,12 @@ dependencies = [
"slotmap",
]
[[package]]
name = "tagptr"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "take-until"
version = "0.2.0"
@@ -15616,6 +15655,7 @@ name = "terminal_view"
version = "0.1.0"
dependencies = [
"anyhow",
"assistant_slash_command",
"async-recursion 1.1.1",
"breadcrumbs",
"client",
@@ -16988,6 +17028,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-fs",
"async_zip",
"collections",
"dirs 4.0.0",
"dunce",
@@ -17009,6 +17050,7 @@ dependencies = [
"tendril",
"unicase",
"util_macros",
"walkdir",
"workspace-hack",
]
@@ -19096,6 +19138,7 @@ dependencies = [
"aho-corasick",
"anstream",
"arrayvec",
"async-compression",
"async-std",
"async-tungstenite",
"aws-config",
@@ -19128,7 +19171,9 @@ dependencies = [
"core-foundation 0.9.4",
"core-foundation-sys",
"coreaudio-sys",
"cranelift-codegen",
"crc32fast",
"crossbeam-epoch",
"crossbeam-utils",
"crypto-common",
"deranged",
@@ -19204,6 +19249,7 @@ dependencies = [
"rand 0.9.1",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
"regalloc2",
"regex",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
@@ -19608,7 +19654,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.188.0"
version = "0.189.0"
dependencies = [
"activity_indicator",
"agent",
@@ -19803,9 +19849,9 @@ dependencies = [
[[package]]
name = "zed_llm_client"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16d993fc42f9ec43ab76fa46c6eb579a66e116bb08cd2bc9a67f3afcaa05d39d"
checksum = "9be71e2f9b271e1eb8eb3e0d986075e770d1a0a299fb036abc3f1fc13a2fa7eb"
dependencies = [
"anyhow",
"serde",

View File

@@ -80,7 +80,6 @@ members = [
"crates/language",
"crates/language_extension",
"crates/language_model",
"crates/language_model_selector",
"crates/language_models",
"crates/language_selector",
"crates/language_tools",
@@ -287,7 +286,6 @@ journal = { path = "crates/journal" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
language_model_selector = { path = "crates/language_model_selector" }
language_models = { path = "crates/language_models" }
language_selector = { path = "crates/language_selector" }
language_tools = { path = "crates/language_tools" }
@@ -476,6 +474,7 @@ lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189
markup5ever_rcdom = "0.3.0"
metal = "0.29"
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
moka = { version = "0.12.10", features = ["sync"] }
naga = { version = "25.0", features = ["wgsl-in"] }
nanoid = "0.4"
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
@@ -599,7 +598,7 @@ unindent = "0.2.0"
url = "2.2"
urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
walkdir = "2.3"
walkdir = "2.5"
wasi-preview1-component-adapter-provider = "29"
wasm-encoder = "0.221"
wasmparser = "0.221"
@@ -609,13 +608,14 @@ wasmtime = { version = "29", default-features = false, features = [
"runtime",
"cranelift",
"component-model",
"incremental-cache",
"parallel-compilation",
] }
wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.8.1"
zed_llm_client = "0.8.2"
zstd = "0.11"
[workspace.dependencies.async-stripe]

View File

@@ -33,6 +33,7 @@
"f4": "debugger::Start",
"f5": "debugger::Continue",
"shift-f5": "debugger::Stop",
"ctrl-shift-f5": "debugger::Restart",
"f6": "debugger::Pause",
"f7": "debugger::StepOver",
"cmd-f11": "debugger::StepInto",
@@ -558,6 +559,7 @@
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-b": "outline_panel::ToggleFocus",
"ctrl-shift-g": "git_panel::ToggleFocus",
"ctrl-shift-d": "debug_panel::ToggleFocus",
"ctrl-?": "agent::ToggleFocus",
"alt-save": "workspace::SaveAll",
"ctrl-alt-s": "workspace::SaveAll",
@@ -595,7 +597,6 @@
{
"context": "Editor",
"bindings": {
"ctrl-shift-d": "editor::DuplicateLineDown",
"ctrl-shift-j": "editor::JoinLines",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
@@ -862,6 +863,13 @@
"alt-l": "git::GenerateCommitMessage"
}
},
{
"context": "DebugPanel",
"bindings": {
"ctrl-t": "debugger::ToggleThreadPicker",
"ctrl-i": "debugger::ToggleSessionPicker"
}
},
{
"context": "CollabPanel && not_editing",
"bindings": {

View File

@@ -17,6 +17,7 @@
"f4": "debugger::Start",
"f5": "debugger::Continue",
"shift-f5": "debugger::Stop",
"shift-cmd-f5": "debugger::Restart",
"f6": "debugger::Pause",
"f7": "debugger::StepOver",
"f11": "debugger::StepInto",
@@ -624,6 +625,7 @@
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-shift-b": "outline_panel::ToggleFocus",
"ctrl-shift-g": "git_panel::ToggleFocus",
"cmd-shift-d": "debug_panel::ToggleFocus",
"cmd-?": "agent::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle",
@@ -929,6 +931,13 @@
"alt-tab": "git::GenerateCommitMessage"
}
},
{
"context": "DebugPanel",
"bindings": {
"cmd-t": "debugger::ToggleThreadPicker",
"cmd-i": "debugger::ToggleSessionPicker"
}
},
{
"context": "CollabPanel && not_editing",
"use_key_equivalents": true,

View File

@@ -13,7 +13,7 @@
}
},
{
"context": "Editor",
"context": "Editor && vim_mode == insert",
"bindings": {
// "j k": ["workspace::SendKeystrokes", "escape"]
}

View File

@@ -52,7 +52,6 @@ itertools.workspace = true
jsonschema.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true

View File

@@ -217,7 +217,6 @@ fn register_slash_commands(cx: &mut App) {
slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
slash_command_registry.register_command(assistant_slash_commands::TerminalSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::NowSlashCommand, false);
slash_command_registry
.register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);

View File

@@ -3,10 +3,10 @@ use fs::Fs;
use gpui::{Entity, FocusHandle, SharedString};
use crate::Thread;
use language_model::{ConfiguredModel, LanguageModelRegistry};
use language_model_selector::{
use assistant_context_editor::language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
use language_model::{ConfiguredModel, LanguageModelRegistry};
use settings::update_settings_file;
use std::sync::Arc;
use ui::{PopoverMenuHandle, Tooltip, prelude::*};

View File

@@ -17,6 +17,7 @@ use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use assistant_context_editor::language_model_selector::ToggleModelSelector;
use client::{UserStore, zed_urls};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
@@ -30,7 +31,6 @@ use language::LanguageRegistry;
use language_model::{
LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,
};
use language_model_selector::ToggleModelSelector;
use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use proto::Plan;

View File

@@ -9,6 +9,7 @@ use crate::terminal_codegen::TerminalCodegen;
use crate::thread_store::{TextThreadStore, ThreadStore};
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{RemoveAllContext, ToggleContextPicker};
use assistant_context_editor::language_model_selector::ToggleModelSelector;
use client::ErrorExt;
use collections::VecDeque;
use db::kvp::Dismissable;
@@ -24,7 +25,6 @@ use gpui::{
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use language_model_selector::ToggleModelSelector;
use parking_lot::Mutex;
use settings::Settings;
use std::cmp;

View File

@@ -8,6 +8,7 @@ use crate::ui::{
AnimatedLabel, MaxModeTooltip,
preview::{AgentPreview, UsageCallout},
};
use assistant_context_editor::language_model_selector::ToggleModelSelector;
use assistant_settings::{AssistantSettings, CompletionMode};
use buffer_diff::BufferDiff;
use client::UserStore;
@@ -30,7 +31,6 @@ use language_model::{
ConfiguredModel, LanguageModelRequestMessage, MessageContent, RequestUsage,
ZED_CLOUD_PROVIDER_ID,
};
use language_model_selector::ToggleModelSelector;
use multi_buffer;
use project::Project;
use prompt_store::PromptStore;

View File

@@ -22,6 +22,7 @@ clock.workspace = true
collections.workspace = true
context_server.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
@@ -29,15 +30,16 @@ gpui.workspace = true
indexed_docs.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
log.workspace = true
multi_buffer.workspace = true
open_ai.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
project.workspace = true
prompt_store.workspace = true
proto.workspace = true
regex.workspace = true
rope.workspace = true
rpc.workspace = true

View File

@@ -2,6 +2,7 @@ mod context;
mod context_editor;
mod context_history;
mod context_store;
pub mod language_model_selector;
mod slash_command;
mod slash_command_picker;

View File

@@ -1,3 +1,6 @@
use crate::language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
use anyhow::Result;
use assistant_settings::AssistantSettings;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
@@ -36,9 +39,6 @@ use language_model::{
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
Role,
};
use language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
use multi_buffer::MultiBufferRow;
use picker::Picker;
use project::{Project, Worktree};

View File

@@ -9,6 +9,7 @@ use anyhow::Result;
use futures::StreamExt;
use futures::stream::{self, BoxStream};
use gpui::{App, SharedString, Task, WeakEntity, Window};
use language::HighlightId;
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
pub use language_model::Role;
use serde::{Deserialize, Serialize};
@@ -16,6 +17,7 @@ use std::{
ops::Range,
sync::{Arc, atomic::AtomicBool},
};
use ui::ActiveTheme;
use workspace::{Workspace, ui::IconName};
pub fn init(cx: &mut App) {
@@ -325,6 +327,18 @@ impl SlashCommandLine {
}
}
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
let mut label = CodeLabel::default();
label.push_str(command_name, None);
label.push_str(" ", None);
label.push_str(
&arguments.join(" "),
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0..command_name.len();
label
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;

View File

@@ -35,7 +35,6 @@ rope.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
terminal_view.workspace = true
text.workspace = true
toml.workspace = true
ui.workspace = true

View File

@@ -12,11 +12,6 @@ mod selection_command;
mod streaming_example_command;
mod symbols_command;
mod tab_command;
mod terminal_command;
use gpui::App;
use language::{CodeLabel, HighlightId};
use ui::ActiveTheme as _;
pub use crate::cargo_workspace_command::*;
pub use crate::context_server_command::*;
@@ -32,16 +27,5 @@ pub use crate::selection_command::*;
pub use crate::streaming_example_command::*;
pub use crate::symbols_command::*;
pub use crate::tab_command::*;
pub use crate::terminal_command::*;
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
let mut label = CodeLabel::default();
label.push_str(command_name, None);
label.push_str(" ", None);
label.push_str(
&arguments.join(" "),
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0..command_name.len();
label
}
use assistant_slash_command::create_label_for_command;

View File

@@ -41,6 +41,7 @@ open.workspace = true
paths.workspace = true
portable-pty.workspace = true
project.workspace = true
prompt_store.workspace = true
regex.workspace = true
rust-embed.workspace = true
schemars.workspace = true

View File

@@ -18,6 +18,7 @@ use language_model::{
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, SelectedModel,
};
use project::Project;
use prompt_store::{ModelContext, ProjectContext, PromptBuilder, WorktreeContext};
use rand::prelude::*;
use reqwest_client::ReqwestClient;
use serde_json::json;
@@ -895,52 +896,24 @@ fn eval_add_overwrite_test() {
}
#[test]
#[ignore] // until we figure out the mystery described in the comments
// #[cfg_attr(not(feature = "eval"), ignore)]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_create_empty_file() {
// Check that Edit Agent can create a file without writing its
// thoughts into it. This issue is not specific to empty files, but
// it's easier to reproduce with them.
//
// NOTE: For some mysterious reason, I could easily reproduce this
// issue roughly 90% of the time in actual Zed. However, once I
// extract the exact LLM request before the failure point and
// generate from that, the reproduction rate drops to 2%!
//
// Things I've tried to make sure it's not a fluke: disabling prompt
// caching, capturing the LLM request via a proxy server, running the
// prompt on Claude separately from evals. Every time it was mostly
// giving good outcomes, which doesn't match my actual experience in
// Zed.
//
// At some point I discovered that simply adding one insignificant
// space or a newline to the prompt suddenly results in an outcome I
// tried to reproduce almost perfectly.
//
// This weirdness happens even outside of the Zed code base and even
// when using a different subscription. The result is the same: an
// extra newline or space changes the model behavior significantly
// enough, so that the pass rate drops from 99% to 0-3%
//
// I have no explanation to this.
//
//
// Model | Pass rate
// ============================================
//
// --------------------------------------------
// Prompt version: 2025-05-19
// Prompt version: 2025-05-21
// --------------------------------------------
//
// claude-3.7-sonnet | 0.98
// + one extra space in prompt | 0.00
// + original prompt again | 0.99
// + extra newline | 0.03
// claude-3.7-sonnet | 1.00
// gemini-2.5-pro-preview-03-25 | 1.00
// gemini-2.5-flash-preview-04-17 | 1.00
// + one extra space | 1.00
// gpt-4.1 | 1.00
// + one extra space | 1.00
//
//
// TODO: gpt-4.1-mini errored 38 times:
@@ -949,8 +922,8 @@ fn eval_create_empty_file() {
let input_file_content = None;
let expected_output_content = String::new();
eval(
1,
1.0,
100,
0.99,
EvalInput::from_conversation(
vec![
message(User, [text("Create a second empty todo file ")]),
@@ -1442,24 +1415,59 @@ impl EditAgentTest {
.update(cx, |project, cx| project.open_buffer(path, cx))
.await
.unwrap();
let conversation = LanguageModelRequest {
messages: eval.conversation,
tools: cx.update(|cx| {
ToolRegistry::default_global(cx)
.tools()
.into_iter()
.filter_map(|tool| {
let input_schema = tool
.input_schema(self.agent.model.tool_input_format())
.ok()?;
Some(LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema,
})
let tools = cx.update(|cx| {
ToolRegistry::default_global(cx)
.tools()
.into_iter()
.filter_map(|tool| {
let input_schema = tool
.input_schema(self.agent.model.tool_input_format())
.ok()?;
Some(LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema,
})
.collect()
}),
})
.collect::<Vec<_>>()
});
let tool_names = tools
.iter()
.map(|tool| tool.name.clone())
.collect::<Vec<_>>();
let worktrees = vec![WorktreeContext {
root_name: "root".to_string(),
rules_file: None,
}];
let prompt_builder = PromptBuilder::new(None)?;
let project_context = ProjectContext::new(worktrees, Vec::default());
let system_prompt = prompt_builder.generate_assistant_system_prompt(
&project_context,
&ModelContext {
available_tools: tool_names,
},
)?;
let has_system_prompt = eval
.conversation
.first()
.map_or(false, |msg| msg.role == Role::System);
let messages = if has_system_prompt {
eval.conversation
} else {
[LanguageModelRequestMessage {
role: Role::System,
content: vec![MessageContent::Text(system_prompt)],
cache: true,
}]
.into_iter()
.chain(eval.conversation)
.collect::<Vec<_>>()
};
let conversation = LanguageModelRequest {
messages,
tools,
..Default::default()
};
let edit_output = if matches!(eval.edit_file_input.mode, EditFileMode::Edit) {

View File

@@ -1,12 +1,13 @@
You are an expert engineer and your task is to write a new file from scratch.
<file_to_edit>
You MUST respond directly with the file's content, without explanations, additional text or triple backticks.
The text you output will be saved verbatim as the content of the file.
Tool calls have been disabled. You MUST start your response directly with the file's new content.
<file_path>
{{path}}
</file_to_edit>
</file_path>
<edit_description>
{{edit_description}}
</edit_description>
You MUST respond directly with the file's content, without explanations, additional text or triple backticks.
The text you output will be saved verbatim as the content of the file.

View File

@@ -39,7 +39,7 @@ struct UpdateRequestBody {
destination: &'static str,
}
#[derive(Clone, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VersionCheckType {
Sha(String),
Semantic(SemanticVersion),
@@ -491,62 +491,38 @@ impl AutoUpdater {
Self::get_release(this, asset, os, arch, None, release_channel, cx).await
}
fn installed_update_version(&self) -> Option<VersionCheckType> {
match &self.status {
AutoUpdateStatus::Updated { version, .. } => Some(version.clone()),
_ => None,
}
}
async fn update(this: Entity<Self>, mut cx: AsyncApp) -> Result<()> {
let (client, current_version, installed_update_version, release_channel) =
let (client, installed_version, status, release_channel) =
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Checking;
cx.notify();
(
this.http_client.clone(),
this.current_version,
this.installed_update_version(),
this.status.clone(),
ReleaseChannel::try_global(cx),
)
})?;
let release =
let fetched_release_data =
Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
let fetched_version = fetched_release_data.clone().version;
let app_commit_sha = cx.update(|cx| AppCommitSha::try_global(cx).map(|sha| sha.0));
let newer_version = Self::check_for_newer_version(
*RELEASE_CHANNEL,
app_commit_sha,
installed_version,
status,
fetched_version,
)?;
let update_version_to_install = match *RELEASE_CHANNEL {
ReleaseChannel::Nightly => {
let should_download = cx
.update(|cx| AppCommitSha::try_global(cx).map(|sha| release.version != sha.0))
.ok()
.flatten()
.unwrap_or(true);
should_download.then(|| VersionCheckType::Sha(release.version.clone()))
}
_ => {
let installed_version =
installed_update_version.unwrap_or(VersionCheckType::Semantic(current_version));
match installed_version {
VersionCheckType::Sha(_) => {
log::warn!("Unexpected SHA-based version in non-nightly build");
Some(installed_version)
}
VersionCheckType::Semantic(semantic_comparison_version) => {
let latest_release_version = release.version.parse::<SemanticVersion>()?;
let should_download = latest_release_version > semantic_comparison_version;
should_download.then(|| VersionCheckType::Semantic(latest_release_version))
}
let Some(newer_version) = newer_version else {
return this.update(&mut cx, |this, cx| {
if !matches!(this.status, AutoUpdateStatus::Updated { .. }) {
this.status = AutoUpdateStatus::Idle;
cx.notify();
}
}
};
let Some(update_version) = update_version_to_install else {
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Idle;
cx.notify();
})?;
return Ok(());
});
};
this.update(&mut cx, |this, cx| {
@@ -555,6 +531,71 @@ impl AutoUpdater {
})?;
let installer_dir = InstallerDir::new().await?;
let target_path = Self::target_path(&installer_dir).await?;
download_release(&target_path, fetched_release_data, client, &cx).await?;
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Installing;
cx.notify();
})?;
let binary_path = Self::binary_path(installer_dir, target_path, &cx).await?;
this.update(&mut cx, |this, cx| {
this.set_should_show_update_notification(true, cx)
.detach_and_log_err(cx);
this.status = AutoUpdateStatus::Updated {
binary_path,
version: newer_version,
};
cx.notify();
})
}
fn check_for_newer_version(
release_channel: ReleaseChannel,
app_commit_sha: Result<Option<String>>,
installed_version: SemanticVersion,
status: AutoUpdateStatus,
fetched_version: String,
) -> Result<Option<VersionCheckType>> {
let parsed_fetched_version = fetched_version.parse::<SemanticVersion>();
if let AutoUpdateStatus::Updated { version, .. } = status {
match version {
VersionCheckType::Sha(cached_version) => {
let should_download = fetched_version != cached_version;
let newer_version =
should_download.then(|| VersionCheckType::Sha(fetched_version));
return Ok(newer_version);
}
VersionCheckType::Semantic(cached_version) => {
return Self::check_for_newer_version_non_nightly(
cached_version,
parsed_fetched_version?,
);
}
}
}
match release_channel {
ReleaseChannel::Nightly => {
let should_download = app_commit_sha
.ok()
.flatten()
.map(|sha| fetched_version != sha)
.unwrap_or(true);
let newer_version = should_download.then(|| VersionCheckType::Sha(fetched_version));
Ok(newer_version)
}
_ => Self::check_for_newer_version_non_nightly(
installed_version,
parsed_fetched_version?,
),
}
}
async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
let filename = match OS {
"macos" => anyhow::Ok("Zed.dmg"),
"linux" => Ok("zed.tar.gz"),
@@ -568,32 +609,29 @@ impl AutoUpdater {
"Aborting. Could not find rsync which is required for auto-updates."
);
let downloaded_asset = installer_dir.path().join(filename);
download_release(&downloaded_asset, release.clone(), client, &cx).await?;
Ok(installer_dir.path().join(filename))
}
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Installing;
cx.notify();
})?;
let binary_path = match OS {
"macos" => install_release_macos(&installer_dir, downloaded_asset, &cx).await,
"linux" => install_release_linux(&installer_dir, downloaded_asset, &cx).await,
"windows" => install_release_windows(downloaded_asset).await,
async fn binary_path(
installer_dir: InstallerDir,
target_path: PathBuf,
cx: &AsyncApp,
) -> Result<PathBuf> {
match OS {
"macos" => install_release_macos(&installer_dir, target_path, cx).await,
"linux" => install_release_linux(&installer_dir, target_path, cx).await,
"windows" => install_release_windows(target_path).await,
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
}?;
}
}
this.update(&mut cx, |this, cx| {
this.set_should_show_update_notification(true, cx)
.detach_and_log_err(cx);
this.status = AutoUpdateStatus::Updated {
binary_path,
version: update_version,
};
cx.notify();
})?;
Ok(())
fn check_for_newer_version_non_nightly(
installed_version: SemanticVersion,
fetched_version: SemanticVersion,
) -> Result<Option<VersionCheckType>> {
let should_download = fetched_version > installed_version;
let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
Ok(newer_version)
}
pub fn set_should_show_update_notification(
@@ -868,3 +906,255 @@ pub fn check_pending_installation() -> bool {
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_version = SemanticVersion::new(1, 0, 0);
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_version.to_string(),
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_stable_does_update_when_fetched_version_is_higher() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_version = SemanticVersion::new(1, 0, 1);
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_version.to_string(),
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Semantic(fetched_version))
);
}
#[test]
fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
binary_path: PathBuf::new(),
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
};
let fetched_version = SemanticVersion::new(1, 0, 1);
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_version.to_string(),
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
binary_path: PathBuf::new(),
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
};
let fetched_version = SemanticVersion::new(1, 0, 2);
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_version.to_string(),
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Semantic(fetched_version))
);
}
#[test]
fn test_nightly_does_not_update_when_fetched_sha_is_same() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_sha = "a".to_string();
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_sha,
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_nightly_does_update_when_fetched_sha_is_not_same() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_sha = "b".to_string();
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_sha.clone(),
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Sha(fetched_sha))
);
}
#[test]
fn test_nightly_does_not_update_when_fetched_sha_is_same_as_cached() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
binary_path: PathBuf::new(),
version: VersionCheckType::Sha("b".to_string()),
};
let fetched_sha = "b".to_string();
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_sha,
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
binary_path: PathBuf::new(),
version: VersionCheckType::Sha("b".to_string()),
};
let fetched_sha = "c".to_string();
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_sha.clone(),
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Sha(fetched_sha))
);
}
#[test]
fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(None);
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_sha = "a".to_string();
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_sha.clone(),
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Sha(fetched_sha))
);
}
#[test]
fn test_nightly_does_not_update_when_cached_update_is_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
{
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(None);
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
binary_path: PathBuf::new(),
version: VersionCheckType::Sha("b".to_string()),
};
let fetched_sha = "b".to_string();
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_sha,
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_nightly_does_update_when_cached_update_is_not_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
{
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(None);
let installed_version = SemanticVersion::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
binary_path: PathBuf::new(),
version: VersionCheckType::Sha("b".to_string()),
};
let fetched_sha = "c".to_string();
let newer_version = AutoUpdater::check_for_newer_version(
release_channel,
app_commit_sha,
installed_version,
status,
fetched_sha.clone(),
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Sha(fetched_sha))
);
}
}

View File

@@ -3,7 +3,10 @@
use anyhow::{Context as _, Result};
use http_client::Url;
use tokio::net::TcpStream;
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
use tokio_socks::{
IntoTargetAddr, TargetAddr,
tcp::{Socks4Stream, Socks5Stream},
};
use super::AsyncReadWrite;
@@ -23,8 +26,14 @@ pub(super) struct Socks5Authorization<'a> {
/// V4 allows idenfication using a user_id
/// V5 allows authorization using a username and password
pub(super) enum SocksVersion<'a> {
V4(Option<Socks4Identification<'a>>),
V5(Option<Socks5Authorization<'a>>),
V4 {
local_dns: bool,
identification: Option<Socks4Identification<'a>>,
},
V5 {
local_dns: bool,
authorization: Option<Socks5Authorization<'a>>,
},
}
pub(super) fn parse_socks_proxy<'t>(scheme: &str, proxy: &'t Url) -> SocksVersion<'t> {
@@ -33,13 +42,19 @@ pub(super) fn parse_socks_proxy<'t>(scheme: &str, proxy: &'t Url) -> SocksVersio
"" => None,
username => Some(Socks4Identification { user_id: username }),
};
SocksVersion::V4(identification)
SocksVersion::V4 {
local_dns: scheme != "socks4a",
identification,
}
} else {
let authorization = proxy.password().map(|password| Socks5Authorization {
username: proxy.username(),
password,
});
SocksVersion::V5(authorization)
SocksVersion::V5 {
local_dns: scheme != "socks5h",
authorization,
}
}
}
@@ -48,26 +63,58 @@ pub(super) async fn connect_socks_proxy_stream(
socks_version: SocksVersion<'_>,
rpc_host: (&str, u16),
) -> Result<Box<dyn AsyncReadWrite>> {
let rpc_host = rpc_host
.into_target_addr()
.context("Failed to parse target addr")?;
let local_dns = match &socks_version {
SocksVersion::V4 { local_dns, .. } => local_dns,
SocksVersion::V5 { local_dns, .. } => local_dns,
};
let rpc_host = match (rpc_host, local_dns) {
(TargetAddr::Domain(domain, port), true) => {
let ip_addr = tokio::net::lookup_host((domain.as_ref(), port))
.await
.with_context(|| format!("Failed to lookup domain {}", domain))?
.next()
.ok_or_else(|| anyhow::anyhow!("Failed to lookup domain {}", domain))?;
TargetAddr::Ip(ip_addr)
}
(rpc_host, _) => rpc_host,
};
match socks_version {
SocksVersion::V4(None) => {
SocksVersion::V4 {
identification: None,
..
} => {
let socks = Socks4Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Ok(Box::new(socks))
}
SocksVersion::V4(Some(Socks4Identification { user_id })) => {
SocksVersion::V4 {
identification: Some(Socks4Identification { user_id }),
..
} => {
let socks = Socks4Stream::connect_with_userid_and_socket(stream, rpc_host, user_id)
.await
.context("error connecting to socks")?;
Ok(Box::new(socks))
}
SocksVersion::V5(None) => {
SocksVersion::V5 {
authorization: None,
..
} => {
let socks = Socks5Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Ok(Box::new(socks))
}
SocksVersion::V5(Some(Socks5Authorization { username, password })) => {
SocksVersion::V5 {
authorization: Some(Socks5Authorization { username, password }),
..
} => {
let socks = Socks5Stream::connect_with_password_and_socket(
stream, rpc_host, username, password,
)
@@ -90,7 +137,13 @@ mod tests {
let scheme = proxy.scheme();
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(version, SocksVersion::V4(None)))
assert!(matches!(
version,
SocksVersion::V4 {
local_dns: true,
identification: None
}
))
}
#[test]
@@ -101,7 +154,25 @@ mod tests {
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V4(Some(Socks4Identification { user_id: "userid" }))
SocksVersion::V4 {
local_dns: true,
identification: Some(Socks4Identification { user_id: "userid" })
}
))
}
#[test]
fn parse_socks4_with_remote_dns() {
let proxy = Url::parse("socks4a://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V4 {
local_dns: false,
identification: None
}
))
}
@@ -111,7 +182,13 @@ mod tests {
let scheme = proxy.scheme();
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(version, SocksVersion::V5(None)))
assert!(matches!(
version,
SocksVersion::V5 {
local_dns: true,
authorization: None
}
))
}
#[test]
@@ -122,10 +199,28 @@ mod tests {
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V5(Some(Socks5Authorization {
username: "username",
password: "password"
}))
SocksVersion::V5 {
local_dns: true,
authorization: Some(Socks5Authorization {
username: "username",
password: "password"
})
}
))
}
#[test]
fn parse_socks5_with_remote_dns() {
let proxy = Url::parse("socks5h://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V5 {
local_dns: false,
authorization: None
}
))
}
}

View File

@@ -0,0 +1,2 @@
drop table monthly_usages;
drop table lifetime_usages;

View File

@@ -0,0 +1 @@
drop table billing_events;

View File

@@ -27,11 +27,9 @@ use crate::db::billing_subscription::{
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
};
use crate::llm::db::subscription_usage_meter::CompletionMode;
use crate::llm::{
AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT,
};
use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND};
use crate::rpc::{ResultExt as _, Server};
use crate::{AppState, Cents, Error, Result};
use crate::{AppState, Error, Result};
use crate::{db::UserId, llm::db::LlmDatabase};
use crate::{
db::{
@@ -64,7 +62,6 @@ pub fn router() -> Router {
"/billing/subscriptions/sync",
post(sync_billing_subscription),
)
.route("/billing/monthly_spend", get(get_monthly_spend))
.route("/billing/usage", get(get_current_usage))
}
@@ -1223,54 +1220,6 @@ async fn handle_customer_subscription_event(
Ok(())
}
#[derive(Debug, Deserialize)]
struct GetMonthlySpendParams {
github_user_id: i32,
}
#[derive(Debug, Serialize)]
struct GetMonthlySpendResponse {
monthly_free_tier_spend_in_cents: u32,
monthly_free_tier_allowance_in_cents: u32,
monthly_spend_in_cents: u32,
}
async fn get_monthly_spend(
Extension(app): Extension<Arc<AppState>>,
Query(params): Query<GetMonthlySpendParams>,
) -> Result<Json<GetMonthlySpendResponse>> {
let user = app
.db
.get_user_by_github_user_id(params.github_user_id)
.await?
.context("user not found")?;
let Some(llm_db) = app.llm_db.clone() else {
return Err(Error::http(
StatusCode::NOT_IMPLEMENTED,
"LLM database not available".into(),
));
};
let free_tier = user
.custom_llm_monthly_allowance_in_cents
.map(|allowance| Cents(allowance as u32))
.unwrap_or(FREE_TIER_MONTHLY_SPENDING_LIMIT);
let spending_for_month = llm_db
.get_user_spending_for_month(user.id, Utc::now())
.await?;
let free_tier_spend = Cents::min(spending_for_month, free_tier);
let monthly_spend = spending_for_month.saturating_sub(free_tier);
Ok(Json(GetMonthlySpendResponse {
monthly_free_tier_spend_in_cents: free_tier_spend.0,
monthly_free_tier_allowance_in_cents: free_tier.0,
monthly_spend_in_cents: monthly_spend.0,
}))
}
#[derive(Debug, Deserialize)]
struct GetCurrentUsageParams {
github_user_id: i32,
@@ -1344,15 +1293,10 @@ async fn get_current_usage(
.get_subscription_usage_for_period(user.id, period_start_at, period_end_at)
.await?;
let plan = usage
.as_ref()
.map(|usage| usage.plan.into())
.unwrap_or_else(|| {
subscription
.kind
.map(Into::into)
.unwrap_or(zed_llm_client::Plan::ZedFree)
});
let plan = subscription
.kind
.map(Into::into)
.unwrap_or(zed_llm_client::Plan::ZedFree);
let model_requests_limit = match plan.model_requests_limit() {
zed_llm_client::UsageLimit::Limited(limit) => {

View File

@@ -7,10 +7,6 @@ pub use token::*;
pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
/// The maximum monthly spending an individual user can reach on the free tier
/// before they have to pay.
pub const FREE_TIER_MONTHLY_SPENDING_LIMIT: Cents = Cents::from_dollars(10);
/// The default value to use for maximum spend per month if the user did not
/// explicitly set a maximum spend.
///

View File

@@ -1,7 +1,3 @@
use crate::db::UserId;
use crate::llm::Cents;
use chrono::Datelike;
use futures::StreamExt as _;
use std::str::FromStr;
use strum::IntoEnumIterator as _;
@@ -45,68 +41,4 @@ impl LlmDatabase {
.collect();
Ok(())
}
pub async fn get_user_spending_for_month(
&self,
user_id: UserId,
now: DateTimeUtc,
) -> Result<Cents> {
self.transaction(|tx| async move {
let month = now.date_naive().month() as i32;
let year = now.date_naive().year();
let mut monthly_usages = monthly_usage::Entity::find()
.filter(
monthly_usage::Column::UserId
.eq(user_id)
.and(monthly_usage::Column::Month.eq(month))
.and(monthly_usage::Column::Year.eq(year)),
)
.stream(&*tx)
.await?;
let mut monthly_spending = Cents::ZERO;
while let Some(usage) = monthly_usages.next().await {
let usage = usage?;
let Ok(model) = self.model_by_id(usage.model_id) else {
continue;
};
monthly_spending += calculate_spending(
model,
usage.input_tokens as usize,
usage.cache_creation_input_tokens as usize,
usage.cache_read_input_tokens as usize,
usage.output_tokens as usize,
);
}
Ok(monthly_spending)
})
.await
}
}
fn calculate_spending(
model: &model::Model,
input_tokens_this_month: usize,
cache_creation_input_tokens_this_month: usize,
cache_read_input_tokens_this_month: usize,
output_tokens_this_month: usize,
) -> Cents {
let input_token_cost =
input_tokens_this_month * model.price_per_million_input_tokens as usize / 1_000_000;
let cache_creation_input_token_cost = cache_creation_input_tokens_this_month
* model.price_per_million_cache_creation_input_tokens as usize
/ 1_000_000;
let cache_read_input_token_cost = cache_read_input_tokens_this_month
* model.price_per_million_cache_read_input_tokens as usize
/ 1_000_000;
let output_token_cost =
output_tokens_this_month * model.price_per_million_output_tokens as usize / 1_000_000;
let spending = input_token_cost
+ cache_creation_input_token_cost
+ cache_read_input_token_cost
+ output_token_cost;
Cents::new(spending as u32)
}

View File

@@ -1,5 +1,4 @@
pub mod model;
pub mod monthly_usage;
pub mod provider;
pub mod subscription_usage;
pub mod subscription_usage_meter;

View File

@@ -1,22 +0,0 @@
use crate::{db::UserId, llm::db::ModelId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "monthly_usages")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: UserId,
pub model_id: ModelId,
pub month: i32,
pub year: i32,
pub input_tokens: i64,
pub cache_creation_input_tokens: i64,
pub cache_read_input_tokens: i64,
pub output_tokens: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -581,6 +581,15 @@ async fn stream_completion(
api_key: String,
request: Request,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let is_vision_request = request.messages.last().map_or(false, |message| match message {
ChatMessage::User { content }
| ChatMessage::Assistant { content, .. }
| ChatMessage::Tool { content, .. } => {
matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
}
_ => false,
});
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(COPILOT_CHAT_COMPLETION_URL)
@@ -594,7 +603,7 @@ async fn stream_completion(
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.header("Copilot-Integration-Id", "vscode-chat")
.header("Copilot-Vision-Request", "true");
.header("Copilot-Vision-Request", is_vision_request.to_string());
let is_streaming = request.stream;

View File

@@ -12,7 +12,7 @@ use language::{LanguageName, LanguageToolchainStore};
use node_runtime::NodeRuntime;
use serde::{Deserialize, Serialize};
use settings::WorktreeId;
use smol::{self, fs::File};
use smol::fs::File;
use std::{
borrow::Borrow,
ffi::OsStr,
@@ -23,6 +23,7 @@ use std::{
sync::Arc,
};
use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate};
use util::archive::extract_zip;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DapStatus {
@@ -358,17 +359,13 @@ pub async fn download_adapter_from_github(
}
DownloadedFileType::Zip | DownloadedFileType::Vsix => {
let zip_path = version_path.with_extension("zip");
let mut file = File::create(&zip_path).await?;
futures::io::copy(response.body_mut(), &mut file).await?;
// we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
util::command::new_smol_command("unzip")
.arg(&zip_path)
.arg("-d")
.arg(&version_path)
.output()
.await?;
let file = File::open(&zip_path).await?;
extract_zip(&version_path, BufReader::new(file))
.await
// we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
.ok();
util::fs::remove_matching(&adapter_path, |entry| {
entry

View File

@@ -30,6 +30,7 @@ language.workspace = true
paths.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
task.workspace = true
util.workspace = true
workspace-hack.workspace = true

View File

@@ -136,6 +136,34 @@ impl DebugAdapter for CodeLldbDebugAdapter {
};
let adapter_dir = version_path.join("extension").join("adapter");
let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
// todo("windows")
#[cfg(not(windows))]
{
use smol::fs;
fs::set_permissions(
&path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await
.with_context(|| format!("Settings executable permissions to {path:?}"))?;
let lldb_binaries_dir = version_path.join("extension").join("lldb").join("bin");
let mut lldb_binaries =
fs::read_dir(&lldb_binaries_dir).await.with_context(|| {
format!("reading lldb binaries dir contents {lldb_binaries_dir:?}")
})?;
while let Some(binary) = lldb_binaries.next().await {
let binary_entry = binary?;
let path = binary_entry.path();
fs::set_permissions(
&path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await
.with_context(|| format!("Settings executable permissions to {path:?}"))?;
}
}
self.path_to_codelldb.set(path.clone()).ok();
command = Some(path);
};

View File

@@ -5,7 +5,7 @@ use crate::{
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
persistence,
ToggleSessionPicker, ToggleThreadPicker, persistence,
};
use anyhow::{Context as _, Result, anyhow};
use command_palette_hooks::CommandPaletteFilter;
@@ -31,7 +31,7 @@ use settings::Settings;
use std::any::TypeId;
use std::sync::Arc;
use task::{DebugScenario, TaskContext};
use ui::{ContextMenu, Divider, Tooltip, prelude::*};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
use workspace::SplitDirection;
use workspace::{
Pane, Workspace,
@@ -65,6 +65,8 @@ pub struct DebugPanel {
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
fs: Arc<dyn Fs>,
}
@@ -77,6 +79,8 @@ impl DebugPanel {
cx.new(|cx| {
let project = workspace.project().clone();
let focus_handle = cx.focus_handle();
let thread_picker_menu_handle = PopoverMenuHandle::default();
let session_picker_menu_handle = PopoverMenuHandle::default();
let debug_panel = Self {
size: px(300.),
@@ -87,6 +91,8 @@ impl DebugPanel {
workspace: workspace.weak_handle(),
context_menu: None,
fs: workspace.app_state().fs.clone(),
thread_picker_menu_handle,
session_picker_menu_handle,
};
debug_panel
@@ -746,55 +752,6 @@ impl DebugPanel {
}),
)
.child(Divider::vertical())
.child(
IconButton::new(
"debug-enable-breakpoint",
IconName::DebugDisabledBreakpoint,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.disabled(thread_status != ThreadStatus::Stopped),
)
.child(
IconButton::new(
"debug-disable-breakpoint",
IconName::CircleOff,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.disabled(thread_status != ThreadStatus::Stopped),
)
.child(
IconButton::new(
"debug-disable-all-breakpoints",
IconName::BugOff,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.disabled(
thread_status == ThreadStatus::Exited
|| thread_status == ThreadStatus::Ended,
)
.on_click(window.listener_for(
&running_state,
|this, _, _window, cx| {
this.toggle_ignore_breakpoints(cx);
},
))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Disable all breakpoints",
&ToggleIgnoreBreakpoints,
&focus_handle,
window,
cx,
)
}
}),
)
.child(Divider::vertical())
.child(
IconButton::new("debug-restart", IconName::DebugRestart)
.icon_size(IconSize::XSmall)
@@ -1033,6 +990,14 @@ impl DebugPanel {
})
.unwrap_or_else(|err| Task::ready(Err(err)))
}
pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread_picker_menu_handle.toggle(window, cx);
}
pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.session_picker_menu_handle.toggle(window, cx);
}
}
impl EventEmitter<PanelEvent> for DebugPanel {}
@@ -1249,6 +1214,24 @@ impl Render for DebugPanel {
.ok();
}
})
.on_action({
let this = this.clone();
move |_: &ToggleThreadPicker, window, cx| {
this.update(cx, |this, cx| {
this.toggle_thread_picker(window, cx);
})
.ok();
}
})
.on_action({
let this = this.clone();
move |_: &ToggleSessionPicker, window, cx| {
this.update(cx, |this, cx| {
this.toggle_session_picker(window, cx);
})
.ok();
}
})
.when(self.active_session.is_some(), |this| {
this.on_mouse_down(
MouseButton::Right,

View File

@@ -45,6 +45,8 @@ actions!(
FocusLoadedSources,
FocusTerminal,
ShowStackTrace,
ToggleThreadPicker,
ToggleSessionPicker,
]
);
@@ -93,6 +95,17 @@ pub fn init(cx: &mut App) {
}
}
})
.register_action(|workspace, _: &Continue, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
panel
.active_session()
.map(|session| session.read(cx).running_state().clone())
}) {
active_item.update(cx, |item, cx| item.continue_thread(cx))
}
}
})
.register_action(|workspace, _: &StepInto, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {

View File

@@ -132,7 +132,8 @@ impl DebugPanel {
this
}),
)
.style(DropdownStyle::Ghost),
.style(DropdownStyle::Ghost)
.handle(self.session_picker_menu_handle.clone()),
)
} else {
None
@@ -163,7 +164,7 @@ impl DebugPanel {
DropdownMenu::new_with_element(
("thread-list", session_id.0),
trigger,
ContextMenu::build_eager(window, cx, move |mut this, _, _| {
ContextMenu::build(window, cx, move |mut this, _, _| {
for (thread, _) in threads {
let running_state = running_state.clone();
let thread_id = thread.id;
@@ -177,7 +178,8 @@ impl DebugPanel {
}),
)
.disabled(session_terminated)
.style(DropdownStyle::Ghost),
.style(DropdownStyle::Ghost)
.handle(self.thread_picker_menu_handle.clone()),
)
} else {
None

View File

@@ -96,7 +96,7 @@ impl Render for RunningState {
.find(|pane| pane.read(cx).is_zoomed());
let active = self.panes.panes().into_iter().next();
let x = if let Some(ref zoomed_pane) = zoomed_pane {
let pane = if let Some(ref zoomed_pane) = zoomed_pane {
zoomed_pane.update(cx, |pane, cx| pane.render(window, cx).into_any_element())
} else if let Some(active) = active {
self.panes
@@ -122,7 +122,7 @@ impl Render for RunningState {
.size_full()
.key_context("DebugSessionItem")
.track_focus(&self.focus_handle(cx))
.child(h_flex().flex_1().child(x))
.child(h_flex().flex_1().child(pane))
}
}
@@ -628,10 +628,9 @@ impl RunningState {
&workspace,
&stack_frame_list,
&variable_list,
&module_list,
&loaded_source_list,
&console,
&breakpoint_list,
&debug_terminal,
dock_axis,
&mut pane_close_subscriptions,
window,
@@ -1468,10 +1467,9 @@ impl RunningState {
workspace: &WeakEntity<Workspace>,
stack_frame_list: &Entity<StackFrameList>,
variable_list: &Entity<VariableList>,
module_list: &Entity<ModuleList>,
loaded_source_list: &Entity<LoadedSourceList>,
console: &Entity<Console>,
breakpoints: &Entity<BreakpointList>,
debug_terminal: &Entity<DebugTerminal>,
dock_axis: Axis,
subscriptions: &mut HashMap<EntityId, Subscription>,
window: &mut Window,
@@ -1512,6 +1510,26 @@ impl RunningState {
let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
center_pane.update(cx, |this, cx| {
let weak_console = console.downgrade();
this.add_item(
Box::new(SubView::new(
console.focus_handle(cx),
console.clone().into(),
DebuggerPaneItem::Console,
Some(Box::new(move |cx| {
weak_console
.read_with(cx, |console, cx| console.show_indicator(cx))
.unwrap_or_default()
})),
cx,
)),
true,
false,
None,
window,
cx,
);
this.add_item(
Box::new(SubView::new(
variable_list.focus_handle(cx),
@@ -1526,54 +1544,20 @@ impl RunningState {
window,
cx,
);
this.add_item(
Box::new(SubView::new(
module_list.focus_handle(cx),
module_list.clone().into(),
DebuggerPaneItem::Modules,
None,
cx,
)),
false,
false,
None,
window,
cx,
);
this.add_item(
Box::new(SubView::new(
loaded_source_list.focus_handle(cx),
loaded_source_list.clone().into(),
DebuggerPaneItem::LoadedSources,
None,
cx,
)),
false,
false,
None,
window,
cx,
);
this.activate_item(0, false, false, window, cx);
});
let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
rightmost_pane.update(cx, |this, cx| {
let weak_console = console.downgrade();
this.add_item(
Box::new(SubView::new(
this.focus_handle(cx),
console.clone().into(),
DebuggerPaneItem::Console,
Some(Box::new(move |cx| {
weak_console
.read_with(cx, |console, cx| console.show_indicator(cx))
.unwrap_or_default()
})),
debug_terminal.focus_handle(cx),
debug_terminal.clone().into(),
DebuggerPaneItem::Terminal,
None,
cx,
)),
true,
false,
false,
None,
window,

View File

@@ -14,7 +14,7 @@ use language::{Buffer, CodeLabel, ToOffset};
use menu::Confirm;
use project::{
Completion,
debugger::session::{CompletionsQuery, OutputToken, Session},
debugger::session::{CompletionsQuery, OutputToken, Session, SessionEvent},
};
use settings::Settings;
use std::{cell::RefCell, rc::Rc, usize};
@@ -79,6 +79,11 @@ impl Console {
let _subscriptions = vec![
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
cx.subscribe_in(&session, window, |this, _, event, window, cx| {
if let SessionEvent::ConsoleOutput = event {
this.update_output(window, cx)
}
}),
cx.on_focus_in(&focus_handle, window, |console, window, cx| {
if console.is_running(cx) {
console.query_bar.focus_handle(cx).focus(window);
@@ -200,12 +205,11 @@ impl Console {
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
EditorElement::new(&self.query_bar, self.editor_style(cx))
}
}
impl Render for Console {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let session = self.session.clone();
let token = self.last_token;
self.update_output_task = cx.spawn_in(window, async move |this, cx| {
_ = session.update_in(cx, move |session, window, cx| {
let (output, last_processed_token) = session.output(token);
@@ -220,7 +224,11 @@ impl Render for Console {
});
});
});
}
}
impl Render for Console {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.track_focus(&self.focus_handle)
.key_context("DebugConsole")

View File

@@ -154,12 +154,15 @@ impl VariableList {
let _subscriptions = vec![
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
cx.subscribe(&session, |this, _, event, _| match event {
cx.subscribe(&session, |this, _, event, cx| match event {
SessionEvent::Stopped(_) => {
this.selection.take();
this.edited_path.take();
this.selected_stack_frame_id.take();
}
SessionEvent::Variables => {
this.build_entries(cx);
}
_ => {}
}),
cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
@@ -300,7 +303,7 @@ impl VariableList {
match event {
StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => {
self.selected_stack_frame_id = Some(*stack_frame_id);
cx.notify();
self.build_entries(cx);
}
StackFrameListEvent::BuiltEntries => {}
}
@@ -344,14 +347,14 @@ impl VariableList {
};
entry.is_expanded = !entry.is_expanded;
cx.notify();
self.build_entries(cx);
}
fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
self.cancel_variable_edit(&Default::default(), window, cx);
if let Some(variable) = self.entries.first() {
self.selection = Some(variable.path.clone());
cx.notify();
self.build_entries(cx);
}
}
@@ -359,7 +362,7 @@ impl VariableList {
self.cancel_variable_edit(&Default::default(), window, cx);
if let Some(variable) = self.entries.last() {
self.selection = Some(variable.path.clone());
cx.notify();
self.build_entries(cx);
}
}
@@ -378,7 +381,7 @@ impl VariableList {
index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
{
self.selection = Some(new_selection);
cx.notify();
self.build_entries(cx);
} else {
self.select_last(&SelectLast, window, cx);
}
@@ -402,7 +405,7 @@ impl VariableList {
index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
{
self.selection = Some(new_selection);
cx.notify();
self.build_entries(cx);
} else {
self.select_first(&SelectFirst, window, cx);
}
@@ -464,7 +467,7 @@ impl VariableList {
self.select_prev(&SelectPrevious, window, cx);
} else {
entry_state.is_expanded = false;
cx.notify();
self.build_entries(cx);
}
}
}
@@ -485,7 +488,7 @@ impl VariableList {
self.select_next(&SelectNext, window, cx);
} else {
entry_state.is_expanded = true;
cx.notify();
self.build_entries(cx);
}
}
}
@@ -929,8 +932,6 @@ impl Focusable for VariableList {
impl Render for VariableList {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.build_entries(cx);
v_flex()
.track_focus(&self.focus_handle)
.key_context("VariableList")
@@ -946,7 +947,6 @@ impl Render for VariableList {
.on_action(cx.listener(Self::collapse_selected_entry))
.on_action(cx.listener(Self::cancel_variable_edit))
.on_action(cx.listener(Self::confirm_variable_edit))
//
.child(
uniform_list(
cx.entity().clone(),

View File

@@ -1,5 +1,6 @@
use crate::{
debugger_panel::DebugPanel,
persistence::DebuggerPaneItem,
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
};
use dap::{
@@ -110,7 +111,8 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
});
running_state.update_in(cx, |this, window, cx| {
this.activate_item(crate::persistence::DebuggerPaneItem::Modules, window, cx);
this.ensure_pane_item(DebuggerPaneItem::Modules, window, cx);
this.activate_item(DebuggerPaneItem::Modules, window, cx);
cx.refresh_windows();
});

View File

@@ -5,6 +5,7 @@ use std::sync::{
use crate::{
DebugPanel,
persistence::DebuggerPaneItem,
session::running::variable_list::{CollapseSelectedEntry, ExpandSelectedEntry},
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
};
@@ -706,7 +707,13 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp
cx.focus_self(window);
let running = item.running_state().clone();
let variable_list = running.read_with(cx, |state, _| state.variable_list().clone());
let variable_list = running.update(cx, |state, cx| {
// have to do this because the variable list pane should be shown/active
// for testing keyboard navigation
state.activate_item(DebuggerPaneItem::Variables, window, cx);
state.variable_list().clone()
});
variable_list.update(cx, |_, cx| cx.focus_self(window));
running
});

View File

@@ -3964,15 +3964,18 @@ impl Editor {
.skip(num_of_whitespaces)
.take(max_len_of_delimiter)
.collect::<String>();
let (delimiter, trimmed_len) =
delimiters.iter().find_map(|delimiter| {
let trimmed = delimiter.trim_end();
if comment_candidate.starts_with(trimmed) {
Some((delimiter, trimmed.len()))
let (delimiter, trimmed_len) = delimiters
.iter()
.filter_map(|delimiter| {
let prefix = delimiter.trim_end();
if comment_candidate.starts_with(prefix) {
Some((delimiter, prefix.len()))
} else {
None
}
})?;
})
.max_by_key(|(_, len)| *len)?;
let cursor_is_placed_after_comment_marker =
num_of_whitespaces + trimmed_len <= start_point.column as usize;
if cursor_is_placed_after_comment_marker {
@@ -7263,24 +7266,22 @@ impl Editor {
..Default::default()
};
let primary_action_text = if breakpoint.is_disabled() {
"enable"
"Enable breakpoint"
} else if is_phantom && !collides_with_existing {
"set"
"Set breakpoint"
} else {
"unset"
"Unset breakpoint"
};
let mut primary_text = format!("Click to {primary_action_text}");
if collides_with_existing && !breakpoint.is_disabled() {
use std::fmt::Write;
write!(primary_text, ", {alt_as_text}-click to disable").ok();
}
let primary_text = SharedString::from(primary_text);
let focus_handle = self.focus_handle.clone();
let meta = if is_rejected {
"No executable code is associated with this line."
SharedString::from("No executable code is associated with this line.")
} else if collides_with_existing && !breakpoint.is_disabled() {
SharedString::from(format!(
"{alt_as_text}-click to disable,\nright-click for more options."
))
} else {
"Right-click for more options."
SharedString::from("Right-click for more options.")
};
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
.icon_size(IconSize::XSmall)
@@ -7319,7 +7320,14 @@ impl Editor {
);
}))
.tooltip(move |window, cx| {
Tooltip::with_meta_in(primary_text.clone(), None, meta, &focus_handle, window, cx)
Tooltip::with_meta_in(
primary_action_text,
Some(&ToggleBreakpoint),
meta.clone(),
&focus_handle,
window,
cx,
)
})
}
@@ -20236,6 +20244,7 @@ impl SemanticsProvider for Entity<Project> {
fn inline_values(
&self,
buffer_handle: Entity<Buffer>,
range: Range<text::Anchor>,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<InlayHint>>>> {

View File

@@ -2820,6 +2820,42 @@ async fn test_newline_comments(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.tab_size = NonZeroU32::new(4)
});
let language = Arc::new(Language::new(
LanguageConfig {
line_comments: vec!["// ".into(), "/// ".into()],
..LanguageConfig::default()
},
None,
));
{
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {"
//ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
//
// ˇ
"});
cx.set_state(indoc! {"
///ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
///
/// ˇ
"});
}
}
#[gpui::test]
async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
init_test(cx, |settings| {
@@ -5965,8 +6001,34 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) {
cx.assert_editor_state(indoc!(
r#"abc
defˇghi
ˇ
jk
nlmo
"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_below(&Default::default(), window, cx);
});
cx.assert_editor_state(indoc!(
r#"abc
defˇghi
ˇ
jkˇ
nlmo
"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_below(&Default::default(), window, cx);
});
cx.assert_editor_state(indoc!(
r#"abc
defˇghi
ˇ
jkˇ
nlmˇo
"#
));
@@ -5978,10 +6040,10 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) {
cx.assert_editor_state(indoc!(
r#"abc
defˇghi
jk
ˇ
jkˇ
nlmˇo
"#
ˇ"#
));
// change selections

View File

@@ -3597,7 +3597,7 @@ impl EditorElement {
style: &EditorStyle,
window: &mut Window,
cx: &mut App,
) {
) -> Option<ContextMenuLayout> {
let mut min_menu_height = Pixels::ZERO;
let mut max_menu_height = Pixels::ZERO;
let mut height_above_menu = Pixels::ZERO;
@@ -3638,7 +3638,7 @@ impl EditorElement {
let visible = edit_prediction_popover_visible || context_menu_visible;
if !visible {
return;
return None;
}
let cursor_row_layout = &line_layouts[cursor.row().minus(start_row) as usize];
@@ -3663,7 +3663,7 @@ impl EditorElement {
let min_height = height_above_menu + min_menu_height + height_below_menu;
let max_height = height_above_menu + max_menu_height + height_below_menu;
let Some((laid_out_popovers, y_flipped)) = self.layout_popovers_above_or_below_line(
let (laid_out_popovers, y_flipped) = self.layout_popovers_above_or_below_line(
target_position,
line_height,
min_height,
@@ -3721,16 +3721,11 @@ impl EditorElement {
.flatten()
.collect::<Vec<_>>()
},
) else {
return;
};
)?;
let Some((menu_ix, (_, menu_bounds))) = laid_out_popovers
let (menu_ix, (_, menu_bounds)) = laid_out_popovers
.iter()
.find_position(|(x, _)| matches!(x, CursorPopoverType::CodeContextMenu))
else {
return;
};
.find_position(|(x, _)| matches!(x, CursorPopoverType::CodeContextMenu))?;
let last_ix = laid_out_popovers.len() - 1;
let menu_is_last = menu_ix == last_ix;
let first_popover_bounds = laid_out_popovers[0].1;
@@ -3771,7 +3766,7 @@ impl EditorElement {
false
};
self.layout_context_menu_aside(
let aside_bounds = self.layout_context_menu_aside(
y_flipped,
*menu_bounds,
target_bounds,
@@ -3783,6 +3778,23 @@ impl EditorElement {
window,
cx,
);
if let Some(menu_bounds) = laid_out_popovers.iter().find_map(|(popover_type, bounds)| {
if matches!(popover_type, CursorPopoverType::CodeContextMenu) {
Some(*bounds)
} else {
None
}
}) {
let bounds = if let Some(aside_bounds) = aside_bounds {
menu_bounds.union(&aside_bounds)
} else {
menu_bounds
};
return Some(ContextMenuLayout { y_flipped, bounds });
}
None
}
fn layout_gutter_menu(
@@ -3988,7 +4000,7 @@ impl EditorElement {
viewport_bounds: Bounds<Pixels>,
window: &mut Window,
cx: &mut App,
) {
) -> Option<Bounds<Pixels>> {
let available_within_viewport = target_bounds.space_within(&viewport_bounds);
let positioned_aside = if available_within_viewport.right >= MENU_ASIDE_MIN_WIDTH
&& !must_place_above_or_below
@@ -3997,16 +4009,14 @@ impl EditorElement {
available_within_viewport.right - px(1.),
MENU_ASIDE_MAX_WIDTH,
);
let Some(mut aside) = self.render_context_menu_aside(
let mut aside = self.render_context_menu_aside(
size(max_width, max_height - POPOVER_Y_PADDING),
window,
cx,
) else {
return;
};
aside.layout_as_root(AvailableSpace::min_size(), window, cx);
)?;
let size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
let right_position = point(target_bounds.right(), menu_bounds.origin.y);
Some((aside, right_position))
Some((aside, right_position, size))
} else {
let max_size = size(
// TODO(mgsloan): Once the menu is bounded by viewport width the bound on viewport
@@ -4023,9 +4033,7 @@ impl EditorElement {
),
) - POPOVER_Y_PADDING,
);
let Some(mut aside) = self.render_context_menu_aside(max_size, window, cx) else {
return;
};
let mut aside = self.render_context_menu_aside(max_size, window, cx)?;
let actual_size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
let top_position = point(
@@ -4059,13 +4067,17 @@ impl EditorElement {
// Fallback: fit actual size in window.
.or_else(|| fit_within(available_within_viewport, actual_size));
aside_position.map(|position| (aside, position))
aside_position.map(|position| (aside, position, actual_size))
};
// Skip drawing if it doesn't fit anywhere.
if let Some((aside, position)) = positioned_aside {
if let Some((aside, position, size)) = positioned_aside {
let aside_bounds = Bounds::new(position, size);
window.defer_draw(aside, position, 2);
return Some(aside_bounds);
}
None
}
fn render_context_menu(
@@ -4174,13 +4186,13 @@ impl EditorElement {
&self,
snapshot: &EditorSnapshot,
hitbox: &Hitbox,
text_hitbox: &Hitbox,
visible_display_row_range: Range<DisplayRow>,
content_origin: gpui::Point<Pixels>,
scroll_pixel_position: gpui::Point<Pixels>,
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
em_width: Pixels,
context_menu_layout: Option<ContextMenuLayout>,
window: &mut Window,
cx: &mut App,
) {
@@ -4224,21 +4236,24 @@ impl EditorElement {
let mut overall_height = Pixels::ZERO;
let mut measured_hover_popovers = Vec::new();
for mut hover_popover in hover_popovers {
for (position, mut hover_popover) in hover_popovers.into_iter().with_position() {
let size = hover_popover.layout_as_root(AvailableSpace::min_size(), window, cx);
let horizontal_offset =
(text_hitbox.top_right().x - POPOVER_RIGHT_OFFSET - (hovered_point.x + size.width))
(hitbox.top_right().x - POPOVER_RIGHT_OFFSET - (hovered_point.x + size.width))
.min(Pixels::ZERO);
overall_height += HOVER_POPOVER_GAP + size.height;
match position {
itertools::Position::Middle | itertools::Position::Last => {
overall_height += HOVER_POPOVER_GAP
}
_ => {}
}
overall_height += size.height;
measured_hover_popovers.push(MeasuredHoverPopover {
element: hover_popover,
size,
horizontal_offset,
});
}
overall_height += HOVER_POPOVER_GAP;
fn draw_occluder(
width: Pixels,
@@ -4255,8 +4270,12 @@ impl EditorElement {
window.defer_draw(occlusion, origin, 2);
}
if hovered_point.y > overall_height {
// There is enough space above. Render popovers above the hovered point
fn place_popovers_above(
hovered_point: gpui::Point<Pixels>,
measured_hover_popovers: Vec<MeasuredHoverPopover>,
window: &mut Window,
cx: &mut App,
) {
let mut current_y = hovered_point.y;
for (position, popover) in measured_hover_popovers.into_iter().with_position() {
let size = popover.size;
@@ -4273,8 +4292,15 @@ impl EditorElement {
current_y = popover_origin.y - HOVER_POPOVER_GAP;
}
} else {
// There is not enough space above. Render popovers below the hovered point
}
fn place_popovers_below(
hovered_point: gpui::Point<Pixels>,
measured_hover_popovers: Vec<MeasuredHoverPopover>,
line_height: Pixels,
window: &mut Window,
cx: &mut App,
) {
let mut current_y = hovered_point.y + line_height;
for (position, popover) in measured_hover_popovers.into_iter().with_position() {
let size = popover.size;
@@ -4289,6 +4315,123 @@ impl EditorElement {
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
}
}
let intersects_menu = |bounds: Bounds<Pixels>| -> bool {
context_menu_layout
.as_ref()
.map_or(false, |menu| bounds.intersects(&menu.bounds))
};
let can_place_above = {
let mut bounds_above = Vec::new();
let mut current_y = hovered_point.y;
for popover in &measured_hover_popovers {
let size = popover.size;
let popover_origin = point(
hovered_point.x + popover.horizontal_offset,
current_y - size.height,
);
bounds_above.push(Bounds::new(popover_origin, size));
current_y = popover_origin.y - HOVER_POPOVER_GAP;
}
bounds_above
.iter()
.all(|b| b.is_contained_within(hitbox) && !intersects_menu(*b))
};
let can_place_below = || {
let mut bounds_below = Vec::new();
let mut current_y = hovered_point.y + line_height;
for popover in &measured_hover_popovers {
let size = popover.size;
let popover_origin = point(hovered_point.x + popover.horizontal_offset, current_y);
bounds_below.push(Bounds::new(popover_origin, size));
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
}
bounds_below
.iter()
.all(|b| b.is_contained_within(hitbox) && !intersects_menu(*b))
};
if can_place_above {
// try placing above hovered point
place_popovers_above(hovered_point, measured_hover_popovers, window, cx);
} else if can_place_below() {
// try placing below hovered point
place_popovers_below(
hovered_point,
measured_hover_popovers,
line_height,
window,
cx,
);
} else {
// try to place popovers around the context menu
let origin_surrounding_menu = context_menu_layout.as_ref().and_then(|menu| {
let total_width = measured_hover_popovers
.iter()
.map(|p| p.size.width)
.max()
.unwrap_or(Pixels::ZERO);
let y_for_horizontal_positioning = if menu.y_flipped {
menu.bounds.bottom() - overall_height
} else {
menu.bounds.top()
};
let possible_origins = vec![
// left of context menu
point(
menu.bounds.left() - total_width - HOVER_POPOVER_GAP,
y_for_horizontal_positioning,
),
// right of context menu
point(
menu.bounds.right() + HOVER_POPOVER_GAP,
y_for_horizontal_positioning,
),
// top of context menu
point(
menu.bounds.left(),
menu.bounds.top() - overall_height - HOVER_POPOVER_GAP,
),
// bottom of context menu
point(menu.bounds.left(), menu.bounds.bottom() + HOVER_POPOVER_GAP),
];
possible_origins.into_iter().find(|&origin| {
Bounds::new(origin, size(total_width, overall_height))
.is_contained_within(hitbox)
})
});
if let Some(origin) = origin_surrounding_menu {
let mut current_y = origin.y;
for (position, popover) in measured_hover_popovers.into_iter().with_position() {
let size = popover.size;
let popover_origin = point(origin.x, current_y);
window.defer_draw(popover.element, popover_origin, 2);
if position != itertools::Position::Last {
let origin = point(popover_origin.x, popover_origin.y + size.height);
draw_occluder(size.width, origin, window, cx);
}
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
}
} else {
// fallback to existing above/below cursor logic
// this might overlap menu or overflow in rare case
if can_place_above {
place_popovers_above(hovered_point, measured_hover_popovers, window, cx);
} else {
place_popovers_below(
hovered_point,
measured_hover_popovers,
line_height,
window,
cx,
);
}
}
}
}
fn layout_diff_hunk_controls(
@@ -4395,7 +4538,6 @@ impl EditorElement {
fn layout_signature_help(
&self,
hitbox: &Hitbox,
text_hitbox: &Hitbox,
content_origin: gpui::Point<Pixels>,
scroll_pixel_position: gpui::Point<Pixels>,
newest_selection_head: Option<DisplayPoint>,
@@ -4403,6 +4545,7 @@ impl EditorElement {
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
em_width: Pixels,
context_menu_layout: Option<ContextMenuLayout>,
window: &mut Window,
cx: &mut App,
) {
@@ -4448,22 +4591,82 @@ impl EditorElement {
let target_point = content_origin + point(target_x, target_y);
let actual_size = element.layout_as_root(Size::<AvailableSpace>::default(), window, cx);
let overall_height = actual_size.height + HOVER_POPOVER_GAP;
let popover_origin = if target_point.y > overall_height {
point(target_point.x, target_point.y - actual_size.height)
} else {
point(
target_point.x,
target_point.y + line_height + HOVER_POPOVER_GAP,
let (popover_bounds_above, popover_bounds_below) = {
let horizontal_offset = (hitbox.top_right().x
- POPOVER_RIGHT_OFFSET
- (target_point.x + actual_size.width))
.min(Pixels::ZERO);
let initial_x = target_point.x + horizontal_offset;
(
Bounds::new(
point(initial_x, target_point.y - actual_size.height),
actual_size,
),
Bounds::new(
point(initial_x, target_point.y + line_height + HOVER_POPOVER_GAP),
actual_size,
),
)
};
let horizontal_offset = (text_hitbox.top_right().x
- POPOVER_RIGHT_OFFSET
- (popover_origin.x + actual_size.width))
.min(Pixels::ZERO);
let final_origin = point(popover_origin.x + horizontal_offset, popover_origin.y);
let intersects_menu = |bounds: Bounds<Pixels>| -> bool {
context_menu_layout
.as_ref()
.map_or(false, |menu| bounds.intersects(&menu.bounds))
};
let final_origin = if popover_bounds_above.is_contained_within(hitbox)
&& !intersects_menu(popover_bounds_above)
{
// try placing above cursor
popover_bounds_above.origin
} else if popover_bounds_below.is_contained_within(hitbox)
&& !intersects_menu(popover_bounds_below)
{
// try placing below cursor
popover_bounds_below.origin
} else {
// try surrounding context menu if exists
let origin_surrounding_menu = context_menu_layout.as_ref().and_then(|menu| {
let y_for_horizontal_positioning = if menu.y_flipped {
menu.bounds.bottom() - actual_size.height
} else {
menu.bounds.top()
};
let possible_origins = vec![
// left of context menu
point(
menu.bounds.left() - actual_size.width - HOVER_POPOVER_GAP,
y_for_horizontal_positioning,
),
// right of context menu
point(
menu.bounds.right() + HOVER_POPOVER_GAP,
y_for_horizontal_positioning,
),
// top of context menu
point(
menu.bounds.left(),
menu.bounds.top() - actual_size.height - HOVER_POPOVER_GAP,
),
// bottom of context menu
point(menu.bounds.left(), menu.bounds.bottom() + HOVER_POPOVER_GAP),
];
possible_origins
.into_iter()
.find(|&origin| Bounds::new(origin, actual_size).is_contained_within(hitbox))
});
origin_surrounding_menu.unwrap_or_else(|| {
// fallback to existing above/below cursor logic
// this might overlap menu or overflow in rare case
if popover_bounds_above.is_contained_within(hitbox) {
popover_bounds_above.origin
} else {
popover_bounds_below.origin
}
})
};
window.defer_draw(element, final_origin, 2);
}
@@ -7884,27 +8087,31 @@ impl Element for EditorElement {
let gutter_settings = EditorSettings::get_global(cx).gutter;
if let Some(newest_selection_head) = newest_selection_head {
let newest_selection_point =
newest_selection_head.to_point(&snapshot.display_snapshot);
if (start_row..end_row).contains(&newest_selection_head.row()) {
self.layout_cursor_popovers(
line_height,
&text_hitbox,
content_origin,
right_margin,
start_row,
scroll_pixel_position,
&line_layouts,
newest_selection_head,
newest_selection_point,
&style,
window,
cx,
);
}
}
let context_menu_layout =
if let Some(newest_selection_head) = newest_selection_head {
let newest_selection_point =
newest_selection_head.to_point(&snapshot.display_snapshot);
if (start_row..end_row).contains(&newest_selection_head.row()) {
self.layout_cursor_popovers(
line_height,
&text_hitbox,
content_origin,
right_margin,
start_row,
scroll_pixel_position,
&line_layouts,
newest_selection_head,
newest_selection_point,
&style,
window,
cx,
)
} else {
None
}
} else {
None
};
self.layout_gutter_menu(
line_height,
@@ -7958,7 +8165,6 @@ impl Element for EditorElement {
self.layout_signature_help(
&hitbox,
&text_hitbox,
content_origin,
scroll_pixel_position,
newest_selection_head,
@@ -7966,6 +8172,7 @@ impl Element for EditorElement {
&line_layouts,
line_height,
em_width,
context_menu_layout,
window,
cx,
);
@@ -7974,13 +8181,13 @@ impl Element for EditorElement {
self.layout_hover_popovers(
&snapshot,
&hitbox,
&text_hitbox,
start_row..end_row,
content_origin,
scroll_pixel_position,
&line_layouts,
line_height,
em_width,
context_menu_layout,
window,
cx,
);
@@ -8212,6 +8419,12 @@ pub(super) fn gutter_bounds(
}
}
#[derive(Clone, Copy)]
struct ContextMenuLayout {
y_flipped: bool,
bounds: Bounds<Pixels>,
}
/// Holds information required for layouting the editor scrollbars.
struct ScrollbarLayoutInformation {
/// The bounds of the editor area (excluding the content offset).

View File

@@ -352,28 +352,32 @@ impl SelectionsCollection {
) -> Option<Selection<Point>> {
let is_empty = positions.start == positions.end;
let line_len = display_map.line_len(row);
let line = display_map.layout_row(row, text_layout_details);
let start_col = line.closest_index_for_x(positions.start) as u32;
if start_col < line_len || (is_empty && positions.start == line.width) {
let (start, end) = if is_empty {
let point = DisplayPoint::new(row, std::cmp::min(start_col, line_len));
(point, point)
} else {
if start_col >= line_len {
return None;
}
let start = DisplayPoint::new(row, start_col);
let end_col = line.closest_index_for_x(positions.end) as u32;
let end = DisplayPoint::new(row, end_col);
(start, end)
};
Some(Selection {
id: post_inc(&mut self.next_selection_id),
start: start.to_point(display_map),
end: end.to_point(display_map),
reversed,
goal: SelectionGoal::HorizontalRange {
start: positions.start.into(),
end: positions.end.into(),
},
})
} else {
None
}
Some(Selection {
id: post_inc(&mut self.next_selection_id),
start: start.to_point(display_map),
end: end.to_point(display_map),
reversed,
goal: SelectionGoal::HorizontalRange {
start: positions.start.into(),
end: positions.end.into(),
},
})
}
pub fn change_with<R>(

View File

@@ -61,6 +61,7 @@ settings.workspace = true
shellexpand.workspace = true
smol.workspace = true
telemetry.workspace = true
terminal_view.workspace = true
toml.workspace = true
unindent.workspace = true
util.workspace = true

View File

@@ -424,6 +424,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
languages::init(languages.clone(), node_runtime.clone(), cx);
prompt_store::init(cx);
terminal_view::init(cx);
let stdout_is_a_pty = false;
let prompt_builder = PromptBuilder::load(fs.clone(), stdout_is_a_pty, cx);
agent::init(

View File

@@ -31,6 +31,7 @@ http_client.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
moka.workspace = true
node_runtime.workspace = true
paths.workspace = true
project.workspace = true

View File

@@ -22,15 +22,18 @@ use gpui::{App, AsyncApp, BackgroundExecutor, Task};
use http_client::HttpClient;
use language::LanguageName;
use lsp::LanguageServerName;
use moka::sync::Cache;
use node_runtime::NodeRuntime;
use release_channel::ReleaseChannel;
use semantic_version::SemanticVersion;
use std::borrow::Cow;
use std::sync::LazyLock;
use std::{
path::{Path, PathBuf},
sync::{Arc, OnceLock},
sync::Arc,
};
use wasmtime::{
Engine, Store,
CacheStore, Engine, Store,
component::{Component, ResourceTable},
};
use wasmtime_wasi::{self as wasi, WasiView};
@@ -411,16 +414,23 @@ type ExtensionCall = Box<
>;
fn wasm_engine() -> wasmtime::Engine {
static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
static WASM_ENGINE: LazyLock<wasmtime::Engine> = LazyLock::new(|| {
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
config.async_support(true);
config
.enable_incremental_compilation(cache_store())
.unwrap();
wasmtime::Engine::new(&config).unwrap()
});
WASM_ENGINE
.get_or_init(|| {
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
config.async_support(true);
wasmtime::Engine::new(&config).unwrap()
})
.clone()
WASM_ENGINE.clone()
}
fn cache_store() -> Arc<IncrementalCompilationCache> {
static CACHE_STORE: LazyLock<Arc<IncrementalCompilationCache>> =
LazyLock::new(|| Arc::new(IncrementalCompilationCache::new()));
CACHE_STORE.clone()
}
impl WasmHost {
@@ -667,3 +677,133 @@ impl wasi::WasiView for WasmState {
&mut self.ctx
}
}
/// Wrapper around a mini-moka bounded cache for storing incremental compilation artifacts.
/// Since wasm modules have many similar elements, this can save us a lot of work at the
/// cost of a small memory footprint. However, we don't want this to be unbounded, so we use
/// a LFU/LRU cache to evict less used cache entries.
#[derive(Debug)]
struct IncrementalCompilationCache {
cache: Cache<Vec<u8>, Vec<u8>>,
}
impl IncrementalCompilationCache {
fn new() -> Self {
let cache = Cache::builder()
// Cap this at 32 MB for now. Our extensions turn into roughly 512kb in the cache,
// which means we could store 64 completely novel extensions in the cache, but in
// practice we will more than that, which is more than enough for our use case.
.max_capacity(32 * 1024 * 1024)
.weigher(|k: &Vec<u8>, v: &Vec<u8>| (k.len() + v.len()).try_into().unwrap_or(u32::MAX))
.build();
Self { cache }
}
}
impl CacheStore for IncrementalCompilationCache {
fn get(&self, key: &[u8]) -> Option<Cow<[u8]>> {
self.cache.get(key).map(|v| v.into())
}
fn insert(&self, key: &[u8], value: Vec<u8>) -> bool {
self.cache.insert(key.to_vec(), value);
true
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use extension::{
ExtensionCapability, ExtensionLibraryKind, LanguageServerManifestEntry, LibManifestEntry,
SchemaVersion,
extension_builder::{CompileExtensionOptions, ExtensionBuilder},
};
use gpui::TestAppContext;
use reqwest_client::ReqwestClient;
use super::*;
#[gpui::test]
fn test_cache_size_for_test_extension(cx: &TestAppContext) {
let cache_store = cache_store();
let engine = wasm_engine();
let wasm_bytes = wasm_bytes(cx, &mut manifest());
Component::new(&engine, wasm_bytes).unwrap();
cache_store.cache.run_pending_tasks();
let size: usize = cache_store
.cache
.iter()
.map(|(k, v)| k.len() + v.len())
.sum();
// If this assertion fails, it means extensions got larger and we may want to
// reconsider our cache size.
assert!(size < 512 * 1024);
}
fn wasm_bytes(cx: &TestAppContext, manifest: &mut ExtensionManifest) -> Vec<u8> {
let extension_builder = extension_builder();
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("extensions/test-extension");
cx.executor()
.block(extension_builder.compile_extension(
&path,
manifest,
CompileExtensionOptions { release: true },
))
.unwrap();
std::fs::read(path.join("extension.wasm")).unwrap()
}
fn extension_builder() -> ExtensionBuilder {
let user_agent = format!(
"Zed Extension CLI/{} ({}; {})",
env!("CARGO_PKG_VERSION"),
std::env::consts::OS,
std::env::consts::ARCH
);
let http_client = Arc::new(ReqwestClient::user_agent(&user_agent).unwrap());
// Local dir so that we don't have to download it on every run
let build_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("benches/.build");
ExtensionBuilder::new(http_client, build_dir)
}
fn manifest() -> ExtensionManifest {
ExtensionManifest {
id: "test-extension".into(),
name: "Test Extension".into(),
version: "0.1.0".into(),
schema_version: SchemaVersion(1),
description: Some("An extension for use in tests.".into()),
authors: Vec::new(),
repository: None,
themes: Default::default(),
icon_themes: Vec::new(),
lib: LibManifestEntry {
kind: Some(ExtensionLibraryKind::Rust),
version: Some(SemanticVersion::new(0, 1, 0)),
},
languages: Vec::new(),
grammars: BTreeMap::default(),
language_servers: [("gleam".into(), LanguageServerManifestEntry::default())]
.into_iter()
.collect(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: vec![ExtensionCapability::ProcessExec {
command: "echo".into(),
args: vec!["hello!".into()],
}],
debug_adapters: Vec::new(),
}
}
}

View File

@@ -15,6 +15,7 @@ use std::{
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::archive::extract_zip;
use util::maybe;
use wasmtime::component::{Linker, Resource};
@@ -543,9 +544,9 @@ impl ExtensionImports for WasmState {
}
DownloadedFileType::Zip => {
futures::pin_mut!(body);
node_runtime::extract_zip(&destination_path, body)
extract_zip(&destination_path, body)
.await
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
.with_context(|| format!("unzipping {path:?} archive"))?;
}
}

View File

@@ -27,7 +27,7 @@ use std::{
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::maybe;
use util::{archive::extract_zip, maybe};
use wasmtime::component::{Linker, Resource};
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 6, 0);
@@ -906,9 +906,9 @@ impl ExtensionImports for WasmState {
}
DownloadedFileType::Zip => {
futures::pin_mut!(body);
node_runtime::extract_zip(&destination_path, body)
extract_zip(&destination_path, body)
.await
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
.with_context(|| format!("unzipping {path:?} archive"))?;
}
}

View File

@@ -1,9 +1,12 @@
use crate::file_finder_settings::FileFinderSettings;
use file_icons::FileIcons;
use futures::channel::oneshot;
use fuzzy::{StringMatch, StringMatchCandidate};
use picker::{Picker, PickerDelegate};
use project::{DirectoryItem, DirectoryLister};
use settings::Settings;
use std::{
path::{MAIN_SEPARATOR_STR, Path, PathBuf},
path::{self, MAIN_SEPARATOR_STR, Path, PathBuf},
sync::{
Arc,
atomic::{self, AtomicBool},
@@ -349,8 +352,9 @@ impl PickerDelegate for OpenPathDelegate {
ix: usize,
selected: bool,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let settings = FileFinderSettings::get_global(cx);
let m = self.matches.get(ix)?;
let directory_state = self.directory_state.as_ref()?;
let candidate = directory_state.match_candidates.get(*m)?;
@@ -361,9 +365,23 @@ impl PickerDelegate for OpenPathDelegate {
.map(|string_match| string_match.positions.clone())
.unwrap_or_default();
let file_icon = maybe!({
if !settings.file_icons {
return None;
}
let icon = if candidate.is_dir {
FileIcons::get_folder_icon(false, cx)?
} else {
let path = path::Path::new(&candidate.path.string);
FileIcons::get_icon(&path, cx)?
};
Some(Icon::from_path(icon).color(Color::Muted))
});
Some(
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.start_slot::<Icon>(file_icon)
.inset(true)
.toggle_state(selected)
.child(HighlightedLabel::new(

View File

@@ -1388,6 +1388,44 @@ where
&& point.y <= self.origin.y.clone() + self.size.height.clone()
}
/// Checks if this bounds is completely contained within another bounds.
///
/// This method determines whether the current bounds is entirely enclosed by the given bounds.
/// A bounds is considered to be contained within another if its origin (top-left corner) and
/// its bottom-right corner are both contained within the other bounds.
///
/// # Arguments
///
/// * `other` - A reference to another `Bounds` that might contain this bounds.
///
/// # Returns
///
/// Returns `true` if this bounds is completely inside the other bounds, `false` otherwise.
///
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size};
/// let outer_bounds = Bounds {
/// origin: Point { x: 0, y: 0 },
/// size: Size { width: 20, height: 20 },
/// };
/// let inner_bounds = Bounds {
/// origin: Point { x: 5, y: 5 },
/// size: Size { width: 10, height: 10 },
/// };
/// let overlapping_bounds = Bounds {
/// origin: Point { x: 15, y: 15 },
/// size: Size { width: 10, height: 10 },
/// };
///
/// assert!(inner_bounds.is_contained_within(&outer_bounds));
/// assert!(!overlapping_bounds.is_contained_within(&outer_bounds));
/// ```
pub fn is_contained_within(&self, other: &Self) -> bool {
other.contains(&self.origin) && other.contains(&self.bottom_right())
}
/// Applies a function to the origin and size of the bounds, producing a new `Bounds<U>`.
///
/// This method allows for converting a `Bounds<T>` to a `Bounds<U>` by specifying a closure

View File

@@ -1,36 +0,0 @@
[package]
name = "language_model_selector"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/language_model_selector.rs"
[features]
test-support = [
"gpui/test-support",
]
[dependencies]
collections.workspace = true
feature_flags.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
language_model.workspace = true
log.workspace = true
ordered-float.workspace = true
picker.workspace = true
proto.workspace = true
ui.workspace = true
workspace-hack.workspace = true
zed_actions.workspace = true
[dev-dependencies]
gpui = { workspace = true, "features" = ["test-support"] }
language_model = { workspace = true, "features" = ["test-support"] }

View File

@@ -1 +0,0 @@
../../LICENSE-GPL

View File

@@ -7,9 +7,9 @@ pub use language::*;
use lsp::{DiagnosticTag, InitializeParams, LanguageServerBinary, LanguageServerName};
use project::lsp_store::clangd_ext;
use serde_json::json;
use smol::fs::{self, File};
use smol::{fs, io::BufReader};
use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into};
pub struct CLspAdapter;
@@ -32,7 +32,7 @@ impl super::LspAdapter for CLspAdapter {
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
Some(LanguageServerBinary {
path,
arguments: vec![],
arguments: Vec::new(),
env: None,
})
}
@@ -69,7 +69,6 @@ impl super::LspAdapter for CLspAdapter {
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
let version_dir = container_dir.join(format!("clangd_{}", version.name));
let binary_path = version_dir.join("bin/clangd");
@@ -79,28 +78,31 @@ impl super::LspAdapter for CLspAdapter {
.get(&version.url, Default::default(), true)
.await
.context("error downloading release")?;
let mut file = File::create(&zip_path).await?;
anyhow::ensure!(
response.status().is_success(),
"download failed with status {}",
response.status().to_string()
);
futures::io::copy(response.body_mut(), &mut file).await?;
let unzip_status = util::command::new_smol_command("unzip")
.current_dir(&container_dir)
.arg(&zip_path)
.output()
.await?
.status;
anyhow::ensure!(unzip_status.success(), "failed to unzip clangd archive");
extract_zip(&container_dir, BufReader::new(response.body_mut()))
.await
.with_context(|| format!("unzipping clangd archive to {container_dir:?}"))?;
remove_matching(&container_dir, |entry| entry != version_dir).await;
// todo("windows")
#[cfg(not(windows))]
{
fs::set_permissions(
&binary_path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await?;
}
}
Ok(LanguageServerBinary {
path: binary_path,
env: None,
arguments: vec![],
arguments: Vec::new(),
})
}
@@ -306,7 +308,7 @@ impl super::LspAdapter for CLspAdapter {
.map(move |diag| {
let range =
language::range_to_lsp(diag.range.to_point_utf16(&snapshot)).unwrap();
let mut tags = vec![];
let mut tags = Vec::with_capacity(1);
if diag.diagnostic.is_unnecessary {
tags.push(DiagnosticTag::UNNECESSARY);
}
@@ -344,7 +346,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
Ok(LanguageServerBinary {
path: clangd_bin,
env: None,
arguments: vec![],
arguments: Vec::new(),
})
})
.await

View File

@@ -26,7 +26,7 @@ use std::{
sync::Arc,
};
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into};
const SERVER_PATH: &str =
"node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
@@ -429,7 +429,7 @@ impl LspAdapter for NodeVersionAdapter {
.await
.context("downloading release")?;
if version.url.ends_with(".zip") {
node_runtime::extract_zip(
extract_zip(
&destination_container_path,
BufReader::new(response.body_mut()),
)

View File

@@ -22,6 +22,7 @@ use std::{
sync::{Arc, LazyLock},
};
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use util::archive::extract_zip;
use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe};
@@ -215,14 +216,11 @@ impl LspAdapter for RustLspAdapter {
})?;
}
AssetKind::Zip => {
node_runtime::extract_zip(
&destination_path,
BufReader::new(response.body_mut()),
)
.await
.with_context(|| {
format!("unzipping {} to {:?}", version.url, destination_path)
})?;
extract_zip(&destination_path, BufReader::new(response.body_mut()))
.await
.with_context(|| {
format!("unzipping {} to {:?}", version.url, destination_path)
})?;
}
};

View File

@@ -19,6 +19,7 @@ use std::{
sync::Arc,
};
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::archive::extract_zip;
use util::{ResultExt, fs::remove_matching, maybe};
pub(super) fn typescript_task_context() -> ContextProviderWithTasks {
@@ -514,14 +515,11 @@ impl LspAdapter for EsLintLspAdapter {
})?;
}
AssetKind::Zip => {
node_runtime::extract_zip(
&destination_path,
BufReader::new(response.body_mut()),
)
.await
.with_context(|| {
format!("unzipping {} to {:?}", version.url, destination_path)
})?;
extract_zip(&destination_path, BufReader::new(response.body_mut()))
.await
.with_context(|| {
format!("unzipping {} to {:?}", version.url, destination_path)
})?;
}
}

View File

@@ -31,6 +31,7 @@ ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
fs.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -4,6 +4,7 @@ use crate::markdown_elements::{
ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable,
ParsedMarkdownTableAlignment, ParsedMarkdownTableRow,
};
use fs::normalize_path;
use gpui::{
AbsoluteLength, AnyElement, App, AppContext as _, ClipboardItem, Context, DefiniteLength, Div,
Element, ElementId, Entity, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement,
@@ -680,7 +681,7 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext)
_ = workspace.update(cx, |workspace, cx| {
workspace
.open_abs_path(
path.clone(),
normalize_path(path.clone().as_path()),
OpenOptions {
visible: Some(OpenVisible::None),
..Default::default()

View File

@@ -13,7 +13,7 @@ path = "src/node_runtime.rs"
doctest = false
[features]
test-support = ["tempfile"]
test-support = []
[dependencies]
anyhow.workspace = true
@@ -21,7 +21,6 @@ async-compression.workspace = true
async-watch.workspace = true
async-tar.workspace = true
async-trait.workspace = true
async_zip.workspace = true
futures.workspace = true
http_client.workspace = true
log.workspace = true
@@ -30,14 +29,9 @@ semver.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
tempfile = { workspace = true, optional = true }
util.workspace = true
walkdir = "2.5.0"
which.workspace = true
workspace-hack.workspace = true
[target.'cfg(windows)'.dependencies]
async-std = { version = "1.12.0", features = ["unstable"] }
[dev-dependencies]
tempfile.workspace = true

View File

@@ -1,7 +1,4 @@
mod archive;
use anyhow::{Context as _, Result, anyhow, bail};
pub use archive::extract_zip;
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use futures::{AsyncReadExt, FutureExt as _, channel::oneshot, future::Shared};
@@ -19,6 +16,7 @@ use std::{
sync::Arc,
};
use util::ResultExt;
use util::archive::extract_zip;
const NODE_CA_CERTS_ENV_VAR: &str = "NODE_EXTRA_CA_CERTS";
@@ -353,7 +351,7 @@ impl ManagedNodeRuntime {
let archive = Archive::new(decompressed_bytes);
archive.unpack(&node_containing_dir).await?;
}
ArchiveType::Zip => archive::extract_zip(&node_containing_dir, body).await?,
ArchiveType::Zip => extract_zip(&node_containing_dir, body).await?,
}
}

View File

@@ -24,7 +24,7 @@ use dap::{
messages::{Events, Message},
};
use dap::{
ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEventCategory,
ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEvent, OutputEventCategory,
RunInTerminalRequestArguments, StartDebuggingRequestArguments,
};
use futures::channel::{mpsc, oneshot};
@@ -674,6 +674,7 @@ pub enum SessionEvent {
request: RunInTerminalRequestArguments,
sender: mpsc::Sender<Result<u32>>,
},
ConsoleOutput,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -885,9 +886,8 @@ impl Session {
cx.spawn(async move |this, cx| {
while let Some(output) = rx.next().await {
this.update(cx, |this, _| {
this.output_token.0 += 1;
this.output.push_back(dap::OutputEvent {
this.update(cx, |this, cx| {
let event = dap::OutputEvent {
category: None,
output,
group: None,
@@ -897,7 +897,8 @@ impl Session {
column: None,
data: None,
location_reference: None,
});
};
this.push_output(event, cx);
})?;
}
anyhow::Ok(())
@@ -1266,8 +1267,7 @@ impl Session {
return;
}
self.output.push_back(event);
self.output_token.0 += 1;
self.push_output(event, cx);
cx.notify();
}
Events::Breakpoint(event) => self.breakpoint_store.update(cx, |store, _| {
@@ -1445,6 +1445,12 @@ impl Session {
});
}
fn push_output(&mut self, event: OutputEvent, cx: &mut Context<Self>) {
self.output.push_back(event);
self.output_token.0 += 1;
cx.emit(SessionEvent::ConsoleOutput);
}
pub fn any_stopped_thread(&self) -> bool {
self.thread_states.any_stopped_thread()
}
@@ -2063,8 +2069,7 @@ impl Session {
source: Option<Source>,
cx: &mut Context<Self>,
) -> Task<()> {
self.output_token.0 += 1;
self.output.push_back(dap::OutputEvent {
let event = dap::OutputEvent {
category: None,
output: format!("> {expression}"),
group: None,
@@ -2074,7 +2079,8 @@ impl Session {
column: None,
data: None,
location_reference: None,
});
};
self.push_output(event, cx);
let request = self.mode.request_dap(EvaluateCommand {
expression,
context,
@@ -2086,8 +2092,7 @@ impl Session {
this.update(cx, |this, cx| {
match response {
Ok(response) => {
this.output_token.0 += 1;
this.output.push_back(dap::OutputEvent {
let event = dap::OutputEvent {
category: None,
output: format!("< {}", &response.result),
group: None,
@@ -2097,11 +2102,11 @@ impl Session {
column: None,
data: None,
location_reference: None,
});
};
this.push_output(event, cx);
}
Err(e) => {
this.output_token.0 += 1;
this.output.push_back(dap::OutputEvent {
let event = dap::OutputEvent {
category: None,
output: format!("{}", e),
group: None,
@@ -2111,7 +2116,8 @@ impl Session {
column: None,
data: None,
location_reference: None,
});
};
this.push_output(event, cx);
}
};
this.invalidate_command_type::<ScopesCommand>();

View File

@@ -348,11 +348,11 @@ impl LocalLspStore {
delegate.update_status(
adapter.name(),
BinaryStatus::Failed {
error: format!("{err}\n-- stderr--\n{}", log),
error: format!("{err}\n-- stderr--\n{log}"),
},
);
log::error!("Failed to start language server {server_name:?}: {err}");
log::error!("server stderr: {:?}", log);
log::error!("Failed to start language server {server_name:?}: {err:#?}");
log::error!("server stderr: {log}");
None
}
}

View File

@@ -3662,9 +3662,8 @@ impl Project {
// ranges in the buffer matched by the query.
let mut chunks = pin!(chunks);
'outer: while let Some(matching_buffer_chunk) = chunks.next().await {
let mut chunk_results = Vec::new();
let mut chunk_results = Vec::with_capacity(matching_buffer_chunk.len());
for buffer in matching_buffer_chunk {
let buffer = buffer.clone();
let query = query.clone();
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
chunk_results.push(cx.background_spawn(async move {

View File

@@ -15,7 +15,7 @@ use anyhow::Result;
use collections::HashMap;
use fs::Fs;
use gpui::{App, AppContext as _, Context, Entity, Task};
use util::ResultExt;
use util::{ResultExt, archive::extract_zip};
pub(crate) struct YarnPathStore {
temp_dirs: HashMap<Arc<Path>, tempfile::TempDir>,
@@ -131,7 +131,7 @@ fn zip_path(path: &Path) -> Option<&Path> {
async fn dump_zip(path: Arc<Path>, fs: Arc<dyn Fs>) -> Result<tempfile::TempDir> {
let dir = tempfile::tempdir()?;
let contents = fs.load_bytes(&path).await?;
node_runtime::extract_zip(dir.path(), futures::io::Cursor::new(contents)).await?;
extract_zip(dir.path(), futures::io::Cursor::new(contents)).await?;
Ok(dir)
}

View File

@@ -97,7 +97,7 @@ pub fn run(
};
let (runnable_ranges, next_cell_point) =
runnable_ranges(&buffer.read(cx).snapshot(), selected_range);
runnable_ranges(&buffer.read(cx).snapshot(), selected_range, cx);
for runnable_range in runnable_ranges {
let Some(language) = multibuffer.read(cx).language_at(runnable_range.start, cx) else {
@@ -215,7 +215,8 @@ pub fn session(editor: WeakEntity<Editor>, cx: &mut App) -> SessionSupport {
match kernelspec {
Some(kernelspec) => SessionSupport::Inactive(kernelspec),
None => {
if language_supported(&language.clone()) {
// For language_supported, need to check available kernels for language
if language_supported(&language.clone(), cx) {
SessionSupport::RequiresSetup(language.name())
} else {
SessionSupport::Unsupported
@@ -414,10 +415,11 @@ fn jupytext_cells(
fn runnable_ranges(
buffer: &BufferSnapshot,
range: Range<Point>,
cx: &mut App,
) -> (Vec<Range<Point>>, Option<Point>) {
if let Some(language) = buffer.language() {
if language.name() == "Markdown".into() {
return (markdown_code_blocks(buffer, range.clone()), None);
return (markdown_code_blocks(buffer, range.clone(), cx), None);
}
}
@@ -442,21 +444,30 @@ fn runnable_ranges(
// We allow markdown code blocks to end in a trailing newline in order to render the output
// below the final code fence. This is different than our behavior for selections and Jupytext cells.
fn markdown_code_blocks(buffer: &BufferSnapshot, range: Range<Point>) -> Vec<Range<Point>> {
fn markdown_code_blocks(
buffer: &BufferSnapshot,
range: Range<Point>,
cx: &mut App,
) -> Vec<Range<Point>> {
buffer
.injections_intersecting_range(range)
.filter(|(_, language)| language_supported(language))
.filter(|(_, language)| language_supported(language, cx))
.map(|(content_range, _)| {
buffer.offset_to_point(content_range.start)..buffer.offset_to_point(content_range.end)
})
.collect()
}
fn language_supported(language: &Arc<Language>) -> bool {
match language.name().as_ref() {
"TypeScript" | "Python" => true,
_ => false,
}
fn language_supported(language: &Arc<Language>, cx: &mut App) -> bool {
let store = ReplStore::global(cx);
let store_read = store.read(cx);
// Since we're just checking for general language support, we only need to look at
// the pure Jupyter kernels - these are all the globally available ones
store_read.pure_jupyter_kernel_specifications().any(|spec| {
// Convert to lowercase for case-insensitive comparison since kernels might report "python" while our language is "Python"
spec.language().as_ref().to_lowercase() == language.name().as_ref().to_lowercase()
})
}
fn get_language(editor: WeakEntity<Editor>, cx: &mut App) -> Option<Arc<Language>> {
@@ -506,7 +517,7 @@ mod tests {
let snapshot = buffer.read(cx).snapshot();
// Single-point selection
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4));
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4), cx);
let snippets = snippets
.into_iter()
.map(|range| snapshot.text_for_range(range).collect::<String>())
@@ -514,7 +525,7 @@ mod tests {
assert_eq!(snippets, vec!["print(1 + 1)"]);
// Multi-line selection
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0));
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0), cx);
let snippets = snippets
.into_iter()
.map(|range| snapshot.text_for_range(range).collect::<String>())
@@ -527,7 +538,7 @@ mod tests {
);
// Trimming multiple trailing blank lines
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0));
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0), cx);
let snippets = snippets
.into_iter()
@@ -580,7 +591,7 @@ mod tests {
let snapshot = buffer.read(cx).snapshot();
// Jupytext snippet surrounding an empty selection
let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5));
let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5), cx);
let snippets = snippets
.into_iter()
@@ -596,7 +607,7 @@ mod tests {
);
// Jupytext snippets intersecting a non-empty selection
let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2));
let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2), cx);
let snippets = snippets
.into_iter()
.map(|range| snapshot.text_for_range(range).collect::<String>())
@@ -623,6 +634,49 @@ mod tests {
#[gpui::test]
fn test_markdown_code_blocks(cx: &mut App) {
use crate::kernels::LocalKernelSpecification;
use jupyter_protocol::JupyterKernelspec;
// Initialize settings
settings::init(cx);
editor::init(cx);
// Initialize the ReplStore with a fake filesystem
let fs = Arc::new(project::RealFs::new(None, cx.background_executor().clone()));
ReplStore::init(fs, cx);
// Add mock kernel specifications for TypeScript and Python
let store = ReplStore::global(cx);
store.update(cx, |store, cx| {
let typescript_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
name: "typescript".into(),
kernelspec: JupyterKernelspec {
argv: vec![],
display_name: "TypeScript".into(),
language: "typescript".into(),
interrupt_mode: None,
metadata: None,
env: None,
},
path: std::path::PathBuf::new(),
});
let python_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
name: "python".into(),
kernelspec: JupyterKernelspec {
argv: vec![],
display_name: "Python".into(),
language: "python".into(),
interrupt_mode: None,
metadata: None,
env: None,
},
path: std::path::PathBuf::new(),
});
store.set_kernel_specs_for_testing(vec![typescript_spec, python_spec], cx);
});
let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
let typescript = languages::language(
"typescript",
@@ -658,7 +712,7 @@ mod tests {
});
let snapshot = buffer.read(cx).snapshot();
let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5));
let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5), cx);
let snippets = snippets
.into_iter()
.map(|range| snapshot.text_for_range(range).collect::<String>())
@@ -703,7 +757,7 @@ mod tests {
});
let snapshot = buffer.read(cx).snapshot();
let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5));
let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5), cx);
let snippets = snippets
.into_iter()
.map(|range| snapshot.text_for_range(range).collect::<String>())
@@ -742,7 +796,7 @@ mod tests {
});
let snapshot = buffer.read(cx).snapshot();
let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5));
let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5), cx);
let snippets = snippets
.into_iter()
.map(|range| snapshot.text_for_range(range).collect::<String>())

View File

@@ -279,4 +279,14 @@ impl ReplStore {
pub fn remove_session(&mut self, entity_id: EntityId) {
self.sessions.remove(&entity_id);
}
#[cfg(test)]
pub fn set_kernel_specs_for_testing(
&mut self,
specs: Vec<KernelSpecification>,
cx: &mut Context<Self>,
) {
self.kernel_specifications = specs;
cx.notify();
}
}

View File

@@ -1460,7 +1460,6 @@ impl BufferSearchBar {
self.select_next_match(&SelectNextMatch, window, cx);
}
should_propagate = false;
self.focus_editor(&FocusEditor, window, cx);
}
}
}

View File

@@ -324,24 +324,24 @@ impl ProjectSearch {
}
}
let excerpts = project_search
.update(cx, |project_search, _| project_search.excerpts.clone())
.ok()?;
let mut new_ranges = excerpts
.update(cx, |excerpts, cx| {
buffers_with_ranges
.into_iter()
.map(|(buffer, ranges)| {
excerpts.set_anchored_excerpts_for_path(
buffer,
ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
)
})
.collect::<FuturesOrdered<_>>()
let mut new_ranges = project_search
.update(cx, |project_search, cx| {
project_search.excerpts.update(cx, |excerpts, cx| {
buffers_with_ranges
.into_iter()
.map(|(buffer, ranges)| {
excerpts.set_anchored_excerpts_for_path(
buffer,
ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
)
})
.collect::<FuturesOrdered<_>>()
})
})
.ok()?;
while let Some(new_ranges) = new_ranges.next().await {
project_search
.update(cx, |project_search, _| {
@@ -1031,6 +1031,12 @@ impl ProjectSearchView {
.update(cx, |editor, cx| editor.set_text(included_files, window, cx));
search.filters_enabled = true;
}
if let Some(excluded_files) = action.excluded_files.as_deref() {
search
.excluded_files_editor
.update(cx, |editor, cx| editor.set_text(excluded_files, window, cx));
search.filters_enabled = true;
}
search.focus_query_editor(window, cx)
});
}
@@ -1055,16 +1061,17 @@ impl ProjectSearchView {
let is_dirty = self.is_dirty(cx);
let skip_save_on_close = self
.workspace
.read_with(cx, |workspace, cx| {
workspace::Pane::skip_save_on_close(&self.results_editor, workspace, cx)
})
.unwrap_or(false);
let should_prompt_to_save = !skip_save_on_close && !will_autosave && is_dirty;
cx.spawn_in(window, async move |this, cx| {
let skip_save_on_close = this
.read_with(cx, |this, cx| {
this.workspace.read_with(cx, |workspace, cx| {
workspace::Pane::skip_save_on_close(&this.results_editor, workspace, cx)
})
})?
.unwrap_or(false);
let should_prompt_to_save = !skip_save_on_close && !will_autosave && is_dirty;
let should_search = if should_prompt_to_save {
let options = &["Save", "Don't Save", "Cancel"];
let result_channel = this.update_in(cx, |_, window, cx| {

View File

@@ -18,6 +18,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
async-recursion.workspace = true
assistant_slash_command.workspace = true
breadcrumbs.workspace = true
collections.workspace = true
db.workspace = true

View File

@@ -1,6 +1,7 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use crate::{TerminalView, terminal_panel::TerminalPanel};
use anyhow::Result;
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
@@ -8,11 +9,10 @@ use assistant_slash_command::{
};
use gpui::{App, Entity, Task, WeakEntity};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use ui::prelude::*;
use workspace::{Workspace, dock::Panel};
use super::create_label_for_command;
use assistant_slash_command::create_label_for_command;
pub struct TerminalSlashCommand;

View File

@@ -2,8 +2,10 @@ mod persistence;
pub mod terminal_element;
pub mod terminal_panel;
pub mod terminal_scrollbar;
mod terminal_slash_command;
pub mod terminal_tab_tooltip;
use assistant_slash_command::SlashCommandRegistry;
use editor::{Editor, EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide};
use gpui::{
AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
@@ -29,6 +31,7 @@ use terminal::{
use terminal_element::{TerminalElement, is_blank};
use terminal_panel::TerminalPanel;
use terminal_scrollbar::TerminalScrollHandle;
use terminal_slash_command::TerminalSlashCommand;
use terminal_tab_tooltip::TerminalTooltip;
use ui::{
ContextMenu, Icon, IconName, Label, Scrollbar, ScrollbarState, Tooltip, h_flex, prelude::*,
@@ -78,6 +81,7 @@ actions!(terminal, [RerunTask]);
impl_actions!(terminal, [SendText, SendKeystroke]);
pub fn init(cx: &mut App) {
assistant_slash_command::init(cx);
terminal_panel::init(cx);
terminal::init(cx);
@@ -87,6 +91,7 @@ pub fn init(cx: &mut App) {
workspace.register_action(TerminalView::deploy);
})
.detach();
SlashCommandRegistry::global(cx).register_command(TerminalSlashCommand, true);
}
pub struct BlockProperties {

View File

@@ -154,7 +154,6 @@ pub struct ContextMenu {
key_context: SharedString,
_on_blur_subscription: Subscription,
keep_open_on_confirm: bool,
eager: bool,
documentation_aside: Option<(usize, DocumentationAside)>,
fixed_width: Option<DefiniteLength>,
}
@@ -207,7 +206,6 @@ impl ContextMenu {
key_context: "menu".into(),
_on_blur_subscription,
keep_open_on_confirm: false,
eager: false,
documentation_aside: None,
fixed_width: None,
end_slot_action: None,
@@ -250,43 +248,6 @@ impl ContextMenu {
key_context: "menu".into(),
_on_blur_subscription,
keep_open_on_confirm: true,
eager: false,
documentation_aside: None,
fixed_width: None,
end_slot_action: None,
},
window,
cx,
)
})
}
pub fn build_eager(
window: &mut Window,
cx: &mut App,
f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
) -> Entity<Self> {
cx.new(|cx| {
let focus_handle = cx.focus_handle();
let _on_blur_subscription = cx.on_blur(
&focus_handle,
window,
|this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
);
window.refresh();
f(
Self {
builder: None,
items: Default::default(),
focus_handle,
action_context: None,
selected_index: None,
delayed: false,
clicked: false,
key_context: "menu".into(),
_on_blur_subscription,
keep_open_on_confirm: false,
eager: true,
documentation_aside: None,
fixed_width: None,
end_slot_action: None,
@@ -327,7 +288,6 @@ impl ContextMenu {
|this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
),
keep_open_on_confirm: false,
eager: false,
documentation_aside: None,
fixed_width: None,
end_slot_action: None,
@@ -634,10 +594,7 @@ impl ContextMenu {
..
})
| ContextMenuItem::CustomEntry { handler, .. },
) = self
.selected_index
.and_then(|ix| self.items.get(ix))
.filter(|_| !self.eager)
) = self.selected_index.and_then(|ix| self.items.get(ix))
{
(handler)(context, window, cx)
}
@@ -740,10 +697,9 @@ impl ContextMenu {
fn select_index(
&mut self,
ix: usize,
window: &mut Window,
cx: &mut Context<Self>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
let context = self.action_context.as_ref();
self.documentation_aside = None;
let item = self.items.get(ix)?;
if item.is_selectable() {
@@ -752,9 +708,6 @@ impl ContextMenu {
if let Some(callback) = &entry.documentation_aside {
self.documentation_aside = Some((ix, callback.clone()));
}
if self.eager && !entry.disabled {
(entry.handler)(context, window, cx)
}
}
}
Some(ix)

View File

@@ -2,6 +2,8 @@ use gpui::{ClickEvent, Corner, CursorStyle, Entity, Hsla, MouseButton};
use crate::{ContextMenu, PopoverMenu, prelude::*};
use super::PopoverMenuHandle;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DropdownStyle {
#[default]
@@ -22,6 +24,7 @@ pub struct DropdownMenu {
menu: Entity<ContextMenu>,
full_width: bool,
disabled: bool,
handle: Option<PopoverMenuHandle<ContextMenu>>,
}
impl DropdownMenu {
@@ -37,6 +40,7 @@ impl DropdownMenu {
menu,
full_width: false,
disabled: false,
handle: None,
}
}
@@ -52,6 +56,7 @@ impl DropdownMenu {
menu,
full_width: false,
disabled: false,
handle: None,
}
}
@@ -64,6 +69,11 @@ impl DropdownMenu {
self.full_width = full_width;
self
}
pub fn handle(mut self, handle: PopoverMenuHandle<ContextMenu>) -> Self {
self.handle = Some(handle);
self
}
}
impl Disableable for DropdownMenu {
@@ -85,6 +95,7 @@ impl RenderOnce for DropdownMenu {
.style(self.style),
)
.attach(Corner::BottomLeft)
.when_some(self.handle.clone(), |el, handle| el.with_handle(handle))
}
}
@@ -159,17 +170,11 @@ pub struct DropdownTriggerStyle {
impl DropdownTriggerStyle {
pub fn for_style(style: DropdownStyle, cx: &App) -> Self {
let colors = cx.theme().colors();
if style == DropdownStyle::Solid {
Self {
// why is this editor_background?
bg: colors.editor_background,
}
} else {
Self {
bg: colors.ghost_element_background,
}
}
let bg = match style {
DropdownStyle::Solid => colors.editor_background,
DropdownStyle::Ghost => colors.ghost_element_background,
};
Self { bg }
}
}

View File

@@ -18,6 +18,7 @@ test-support = ["tempfile", "git2", "rand", "util_macros"]
[dependencies]
anyhow.workspace = true
async-fs.workspace = true
async_zip.workspace = true
collections.workspace = true
dirs.workspace = true
futures-lite.workspace = true
@@ -36,6 +37,7 @@ take-until.workspace = true
tempfile = { workspace = true, optional = true }
unicase.workspace = true
util_macros = { workspace = true, optional = true }
walkdir.workspace = true
workspace-hack.workspace = true
[target.'cfg(unix)'.dependencies]

View File

@@ -1,4 +1,5 @@
pub mod arc_cow;
pub mod archive;
pub mod command;
pub mod fs;
pub mod markdown;

View File

@@ -156,6 +156,8 @@ pub struct DeploySearch {
pub replace_enabled: bool,
#[serde(default)]
pub included_files: Option<String>,
#[serde(default)]
pub excluded_files: Option<String>,
}
impl_actions!(
@@ -203,6 +205,7 @@ impl DeploySearch {
Self {
replace_enabled: false,
included_files: None,
excluded_files: None,
}
}
}
@@ -3114,6 +3117,7 @@ fn default_render_tab_bar_buttons(
DeploySearch {
replace_enabled: false,
included_files: None,
excluded_files: None,
}
.boxed_clone(),
)

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.188.0"
version = "0.189.0"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -45,7 +45,7 @@ pub fn app_menus() -> Vec<Menu> {
#[cfg(target_os = "macos")]
MenuItem::action("Show All", super::ShowAll),
MenuItem::separator(),
MenuItem::action("Quit", Quit),
MenuItem::action("Quit Zed", Quit),
],
},
Menu {

View File

@@ -14,7 +14,7 @@ use license_detection::LICENSE_FILES_TO_CHECK;
pub use license_detection::is_license_eligible_for_data_collection;
pub use rate_completion_modal::*;
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use arrayvec::ArrayVec;
use client::{Client, UserStore};
use collections::{HashMap, HashSet, VecDeque};
@@ -23,7 +23,7 @@ use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EntityId, Global, SemanticVersion,
Subscription, Task, WeakEntity, actions,
};
use http_client::{HttpClient, Method};
use http_client::{AsyncBody, HttpClient, Method, Request, Response};
use input_excerpt::excerpt_for_cursor_position;
use language::{
Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToOffset, ToPoint, text_diff,
@@ -54,8 +54,8 @@ use workspace::Workspace;
use workspace::notifications::{ErrorMessagePrompt, NotificationId};
use worktree::Worktree;
use zed_llm_client::{
EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME, PredictEditsBody,
PredictEditsResponse, ZED_VERSION_HEADER_NAME,
AcceptEditPredictionBody, EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME,
PredictEditsBody, PredictEditsResponse, ZED_VERSION_HEADER_NAME,
};
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
@@ -823,6 +823,74 @@ and then another
}
}
fn accept_edit_prediction(
&mut self,
request_id: InlineCompletionId,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
let llm_token = self.llm_token.clone();
let app_version = AppVersion::global(cx);
cx.spawn(async move |this, cx| {
let http_client = client.http_client();
let mut response = llm_token_retry(&llm_token, &client, |token| {
let request_builder = http_client::Request::builder().method(Method::POST);
let request_builder =
if let Ok(accept_prediction_url) = std::env::var("ZED_ACCEPT_PREDICTION_URL") {
request_builder.uri(accept_prediction_url)
} else {
request_builder.uri(
http_client
.build_zed_llm_url("/predict_edits/accept", &[])?
.as_ref(),
)
};
Ok(request_builder
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", token))
.header(ZED_VERSION_HEADER_NAME, app_version.to_string())
.body(
serde_json::to_string(&AcceptEditPredictionBody {
request_id: request_id.0,
})?
.into(),
)?)
})
.await?;
if let Some(minimum_required_version) = response
.headers()
.get(MINIMUM_REQUIRED_VERSION_HEADER_NAME)
.and_then(|version| SemanticVersion::from_str(version.to_str().ok()?).ok())
{
if app_version < minimum_required_version {
return Err(anyhow!(ZedUpdateRequiredError {
minimum_version: minimum_required_version
}));
}
}
if response.status().is_success() {
if let Some(usage) = EditPredictionUsage::from_headers(response.headers()).ok() {
this.update(cx, |this, cx| {
this.last_usage = Some(usage);
cx.notify();
})?;
}
Ok(())
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
Err(anyhow!(
"error accepting edit prediction.\nStatus: {:?}\nBody: {}",
response.status(),
body
))
}
})
}
fn process_completion_response(
prediction_response: PredictEditsResponse,
buffer: Entity<Buffer>,
@@ -1381,6 +1449,34 @@ impl ProviderDataCollection {
}
}
async fn llm_token_retry(
llm_token: &LlmApiToken,
client: &Arc<Client>,
build_request: impl Fn(String) -> Result<Request<AsyncBody>>,
) -> Result<Response<AsyncBody>> {
let mut did_retry = false;
let http_client = client.http_client();
let mut token = llm_token.acquire(client).await?;
loop {
let request = build_request(token.clone())?;
let response = http_client.send(request).await?;
if !did_retry
&& !response.status().is_success()
&& response
.headers()
.get(EXPIRED_LLM_TOKEN_HEADER_NAME)
.is_some()
{
did_retry = true;
token = llm_token.refresh(client).await?;
continue;
}
return Ok(response);
}
}
pub struct ZetaInlineCompletionProvider {
zeta: Entity<Zeta>,
pending_completions: ArrayVec<PendingCompletion, 2>,
@@ -1597,7 +1693,18 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider
// Right now we don't support cycling.
}
fn accept(&mut self, _cx: &mut Context<Self>) {
fn accept(&mut self, cx: &mut Context<Self>) {
let completion_id = self
.current_completion
.as_ref()
.map(|completion| completion.completion.id);
if let Some(completion_id) = completion_id {
self.zeta
.update(cx, |zeta, cx| {
zeta.accept_edit_prediction(completion_id, cx)
})
.detach();
}
self.pending_completions.clear();
}

View File

@@ -1,7 +1,7 @@
# Configuration
There are various aspects about the Agent Panel that you can customize.
All of them can be seen by either visiting [the Configuring Zed page](/configuring-zed.md#agent) or by running the `zed: open default settings` action and searching for `"agent"`.
All of them can be seen by either visiting [the Configuring Zed page](./configuring-zed.md#agent) or by running the `zed: open default settings` action and searching for `"agent"`.
Alternatively, you can also visit the panel's Settings view by running the `agent: open configuration` action or going to the top-right menu and hitting "Settings".
## LLM Providers
@@ -9,17 +9,17 @@ Alternatively, you can also visit the panel's Settings view by running the `agen
Zed supports multiple large language model providers.
Here's an overview of the supported providers and tool call support:
| Provider | Tool Use Supported |
| ----------------------------------------------- | ------------------ |
| [Anthropic](#anthropic) | ✅ |
| [GitHub Copilot Chat](#github-copilot-chat) | In Some Cases |
| [Google AI](#google-ai) | ✅ |
| [Mistral](#mistral) | ✅ |
| [Ollama](#ollama) | ✅ |
| [OpenAI](#openai) | ✅ |
| [DeepSeek](#deepseek) | 🚫 |
| [OpenAI API Compatible](#openai-api-compatible) | 🚫 |
| [LM Studio](#lmstudio) | 🚫 |
| Provider | Tool Use Supported |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Anthropic](#anthropic) | ✅ |
| [GitHub Copilot Chat](#github-copilot-chat) | For Some Models ([link](https://github.com/zed-industries/zed/blob/9e0330ba7d848755c9734bf456c716bddf0973f3/crates/language_models/src/provider/copilot_chat.rs#L189-L198)) |
| [Google AI](#google-ai) | ✅ |
| [Mistral](#mistral) | ✅ |
| [Ollama](#ollama) | ✅ |
| [OpenAI](#openai) | ✅ |
| [DeepSeek](#deepseek) | 🚫 |
| [OpenAI API Compatible](#openai-api-compatible) | 🚫 |
| [LM Studio](#lmstudio) | 🚫 |
## Use Your Own Keys {#use-your-own-keys}

View File

@@ -191,6 +191,8 @@ let
wayland
]
}";
NIX_OUTPATH_USED_AS_RANDOM_SEED = "norebuilds";
};
# prevent nix from removing the "unused" wayland/gpu-lib rpaths

View File

@@ -19,6 +19,7 @@ ahash = { version = "0.8", features = ["serde"] }
aho-corasick = { version = "1" }
anstream = { version = "0.6" }
arrayvec = { version = "0.7", features = ["serde"] }
async-compression = { version = "0.4", default-features = false, features = ["deflate", "deflate64", "futures-io", "gzip"] }
async-std = { version = "1", features = ["attributes", "unstable"] }
async-tungstenite = { version = "0.29", features = ["tokio-rustls-manual-roots"] }
aws-config = { version = "1", features = ["behavior-version-latest"] }
@@ -44,7 +45,9 @@ chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4", features = ["cargo", "derive", "string", "wrap_help"] }
clap_builder = { version = "4", default-features = false, features = ["cargo", "color", "std", "string", "suggestions", "usage", "wrap_help"] }
concurrent-queue = { version = "2" }
cranelift-codegen = { version = "0.116", default-features = false, features = ["host-arch", "incremental-cache", "std", "timing", "unwind"] }
crc32fast = { version = "1" }
crossbeam-epoch = { version = "0.9" }
crossbeam-utils = { version = "0.8" }
deranged = { version = "0.4", default-features = false, features = ["powerfmt", "serde", "std"] }
digest = { version = "0.10", features = ["mac", "oid", "std"] }
@@ -95,6 +98,7 @@ prost-types = { version = "0.9" }
rand-c38e5c1d305a1b54 = { package = "rand", version = "0.8", features = ["small_rng"] }
rand_chacha = { version = "0.3" }
rand_core = { version = "0.6", default-features = false, features = ["std"] }
regalloc2 = { version = "0.11", features = ["checker", "enable-serde"] }
regex = { version = "1" }
regex-automata = { version = "0.4" }
regex-syntax = { version = "0.8" }
@@ -132,8 +136,8 @@ url = { version = "2", features = ["serde"] }
uuid = { version = "1", features = ["serde", "v4", "v5", "v7"] }
wasm-encoder = { version = "0.221", features = ["wasmparser"] }
wasmparser = { version = "0.221" }
wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "parallel-compilation"] }
wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc"] }
wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "incremental-cache", "parallel-compilation"] }
wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc", "incremental-cache"] }
wasmtime-environ = { version = "29", default-features = false, features = ["compile", "component-model", "demangle", "gc-drc"] }
winnow = { version = "0.7", features = ["simd"] }
@@ -142,6 +146,7 @@ ahash = { version = "0.8", features = ["serde"] }
aho-corasick = { version = "1" }
anstream = { version = "0.6" }
arrayvec = { version = "0.7", features = ["serde"] }
async-compression = { version = "0.4", default-features = false, features = ["deflate", "deflate64", "futures-io", "gzip"] }
async-std = { version = "1", features = ["attributes", "unstable"] }
async-tungstenite = { version = "0.29", features = ["tokio-rustls-manual-roots"] }
aws-config = { version = "1", features = ["behavior-version-latest"] }
@@ -168,7 +173,9 @@ chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4", features = ["cargo", "derive", "string", "wrap_help"] }
clap_builder = { version = "4", default-features = false, features = ["cargo", "color", "std", "string", "suggestions", "usage", "wrap_help"] }
concurrent-queue = { version = "2" }
cranelift-codegen = { version = "0.116", default-features = false, features = ["host-arch", "incremental-cache", "std", "timing", "unwind"] }
crc32fast = { version = "1" }
crossbeam-epoch = { version = "0.9" }
crossbeam-utils = { version = "0.8" }
deranged = { version = "0.4", default-features = false, features = ["powerfmt", "serde", "std"] }
digest = { version = "0.10", features = ["mac", "oid", "std"] }
@@ -224,6 +231,7 @@ quote = { version = "1" }
rand-c38e5c1d305a1b54 = { package = "rand", version = "0.8", features = ["small_rng"] }
rand_chacha = { version = "0.3" }
rand_core = { version = "0.6", default-features = false, features = ["std"] }
regalloc2 = { version = "0.11", features = ["checker", "enable-serde"] }
regex = { version = "1" }
regex-automata = { version = "0.4" }
regex-syntax = { version = "0.8" }
@@ -267,8 +275,8 @@ url = { version = "2", features = ["serde"] }
uuid = { version = "1", features = ["serde", "v4", "v5", "v7"] }
wasm-encoder = { version = "0.221", features = ["wasmparser"] }
wasmparser = { version = "0.221" }
wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "parallel-compilation"] }
wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc"] }
wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "incremental-cache", "parallel-compilation"] }
wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc", "incremental-cache"] }
wasmtime-environ = { version = "29", default-features = false, features = ["compile", "component-model", "demangle", "gc-drc"] }
winnow = { version = "0.7", features = ["simd"] }