Compare commits

...
Sign in to create a new pull request.

44 commits

Author SHA1 Message Date
Peter Tripp
2cef0e1a31
Fix escape in terminal with JetBrains keymap (#35585)
Closes https://github.com/zed-industries/zed/issues/35429 Closes
https://github.com/zed-industries/zed/issues/35091 Follow-up to:
https://github.com/zed-industries/zed/pull/35230

Release Notes:

- Fix `escape` in Terminal broken in JetBrains compatability keymaps
2025-08-04 11:43:05 -04:00
Joseph T. Lyons
c0f25b7549 zed 0.197.5 2025-08-01 16:07:33 -04:00
Joseph T. Lyons
c5c190e56d zed 0.197.4 2025-08-01 14:14:45 -04:00
Cole Miller
f9ac3761fb debugger: Send initialized event from fake server at a more realistic time (#35446)
The spec says:

> ⬅️ Initialized Event
> This event indicates that the debug adapter is ready to accept
configuration requests (e.g. setBreakpoints, setExceptionBreakpoints).
>
> A debug adapter is expected to send this event when it is ready to
accept configuration requests (but not before the initialize request has
finished).

Previously in tests, `intercept_debug_sessions` was just spawning off a
background task to send the event after setting up the client, so the
event wasn't actually synchronized with the flow of messages in the way
the spec says it should be. This PR makes it so that the `FakeTransport`
injects the event right after a successful response to the initialize
request, and doesn't send it otherwise.

Release Notes:

- N/A
2025-08-01 14:04:07 -04:00
Peter Tripp
b6ad19d9b4
Fix broken workspace_hack in v0.197.x (#35455)
Fixes workstation_hack broken in
[v0.197.x](https://github.com/zed-industries/zed/commits/v0.197.x/) by
17404118de
cherry-picking https://github.com/zed-industries/zed/pull/35408.
See: [failing
job](https://github.com/zed-industries/zed/actions/runs/16655247386/job/47138480901)

This could've been caught by a merge queue, but only if we were to
require it for release branches too (e.g. v0.197.x). 🤷

CC: @SomeoneToIgnore

Release Notes:

- N/A
2025-08-01 10:20:40 -04:00
Peter Tripp
4980f2a6ae
jetbrains: Unmap cmd-k in Jetbrains keymap (#35443)
This only works after a delay in most situations because of the all
chorded `cmd-k` mappings in the so disable them for now.

Reported by @jer-k:
https://x.com/J_Kreutzbender/status/1951033355434336606

Release Notes:

- Undo mapping of `cmd-k` for Git Panel in default Jetbrains keymap
(thanks [@jer-k](https://github.com/jer-k))
2025-07-31 18:30:27 -04:00
Kirill Bulatov
17404118de Fix panic with completion ranges and autoclose regions interop (#35408)
As reported [in
Discord](https://discord.com/channels/869392257814519848/1106226198494859355/1398470747227426948)
C projects with `"` as "brackets" that autoclose, may invoke panics when
edited at the end of the file.

With a single selection-caret (`ˇ`), at the end of the file,
```c
ifndef BAR_H

int fn_branch(bool do_branch1, bool do_branch2);

```
gets an LSP response from clangd
```jsonc
{
  "filterText": "AGL/",
  "insertText": "AGL/",
  "insertTextFormat": 1,
  "kind": 17,
  "label": " AGL/",
  "labelDetails": {},
  "score": 0.78725427389144897,
  "sortText": "40b67681AGL/",
  "textEdit": {
    "newText": "AGL/",
    "range": { "end": { "character": 11, "line": 8 }, "start": { "character": 10, "line": 8 } }
  }
}
```

which replaces `"` after the caret (character/column 11, 0-indexed).
This is reasonable, as regular follow-up (proposed in further
completions), is a suffix + a closing `"`:

<img width="842" height="259" alt="image"
src="https://github.com/user-attachments/assets/ea56f621-7008-4ce2-99ba-87344ddf33d2"
/>

Yet when Zed handles user input of `"`, it panics due to multiple
reasons:

* after applying any snippet text edit, Zed did a selection change:
5537987630/crates/editor/src/editor.rs (L9539-L9545)
which caused eventual autoclose region invalidation:
5537987630/crates/editor/src/editor.rs (L2970)

This covers all cases that insert the `include""` text.

* after applying any user input and "plain" text edit, Zed did not
invalidate any autoclose regions at all, relying on the "bracket" (which
includes `"`) autoclose logic to rule edge cases out

* bracket autoclose logic detects previous `"` and considers the new
user input as a valid closure, hence no autoclose region needed.
But there is an autoclose bracket data after the plaintext completion
insertion (`AGL/`) really, and it's not invalidated after `"` handling

* in addition to that, `Anchor::is_valid` method in `text` panicked, and
required `fn try_fragment_id_for_anchor` to handle "pointing at odd,
after the end of the file, offset" cases as `false`

A test reproducing the feedback and 2 fixes added: proper, autoclose
region invalidation call which required the invalidation logic tweaked a
bit, and "superficial", "do not apply bad selections that cause panics"
fix in the editor to be more robust

Release Notes:

- Fixed panic with completion ranges and autoclose regions interop

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-07-31 20:01:25 +03:00
gcp-cherry-pick-bot[bot]
ae1bf978e5
linux: Fix caps lock not working consistently for certain X11 systems (cherry-pick #35361) (#35364)
Cherry-picked linux: Fix caps lock not working consistently for certain
X11 systems (#35361)

Closes #35316

Bug in https://github.com/zed-industries/zed/pull/34514

Turns out you are not supposed to call `update_key` for modifiers on
`KeyPress`/`KeyRelease`, as modifiers are already updated in
`XkbStateNotify` events. Not sure why this only causes issues on a few
systems and works on others.

Tested on Ubuntu 24.04.2 LTS (initial bug) and Kubuntu 25.04 (worked
fine before too).

Release Notes:

- Fixed an issue where caps lock stopped working consistently on some
Linux X11 systems.

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-07-31 01:22:45 +05:30
Joseph T. Lyons
002b8b9f5a v0.197.x stable 2025-07-30 12:07:29 -04:00
Antonio Scandurra
268fc411a2 Always double reconnection delay and add jitter (#35337)
Previously, we would pick an exponent between 0.5 and 2.5, which would
cause a lot of clients to try reconnecting in rapid succession,
overwhelming the server as a result.

This pull request always doubles the previous delay and introduces a
jitter that can, at most, double it.

As part of this, we're also increasing the maximum reconnection delay
from 10s to 30s: this gives us more space to spread out the reconnection
requests.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-07-30 11:38:01 -04:00
Kirill Bulatov
4d44b9f659 Actually disable ai for now (#35327)
Closes https://github.com/zed-industries/zed/issues/35325

* removes Supermaven actions
* removes copilot-related action
* stops re-enabling edit predictions when disabled

Release Notes:

- N/A
2025-07-30 16:22:11 +03:00
gcp-cherry-pick-bot[bot]
b4b57b586a
Fix racy leaked extension server adapters handling (cherry-pick #35319) (#35320)
Cherry-picked Kb/wasm panics (#35319)

Follow-up of https://github.com/zed-industries/zed/pull/34208
Closes https://github.com/zed-industries/zed/issues/35185

Previous code assumed that extensions' language server wrappers may leak
only in static data (e.g. fields that were not cleared on deinit), but
we seem to have a race that breaks this assumption.

1. We do clean `all_lsp_adapters` field after
https://github.com/zed-industries/zed/pull/34334 and it's called for
every extension that is unregistered.
2. `LspStore::maintain_workspace_config` ->
`LspStore::refresh_workspace_configurations` chain is triggered
independently, apparently on `ToolchainStoreEvent::ToolchainActivated`
event which means somewhere behind there's potentially a Python code
that gets executed to activate the toolchian, making
`refresh_workspace_configurations` start timings unpredictable.
3. Seems that toolchain activation overlaps with plugin reload, as 
`2025-07-28T12:16:19+03:00 INFO [extension_host] extensions updated.
loading 0, reloading 1, unloading 0` suggests in the issue logs.

The plugin reload seem to happen faster than workspace configuration
refresh in



c65da547c9/crates/project/src/lsp_store.rs (L7426-L7456)

as the language servers are just starting and take extra time to respond
to the notification.

At least one of the `.clone()`d `adapter`s there is the adapter that got
removed during plugin reload and has its channel closed, which causes a
panic later.

----------------------------

A good fix would be to re-architect the workspace refresh approach, same
as other accesses to the language server collections.
One way could be to use `Weak`-based structures instead, as definitely
the extension server data belongs to extension, not the `LspStore`.
This is quite a large undertaking near the extension core though, so is
not done yet.

Currently, to stop the excessive panics, no more `.expect` is done on
the channel result, as indeed, it now can be closed very dynamically.
This will result in more errors (and backtraces, presumably) printed in
the logs and no panics.

More logging and comments are added, and workspace querying is replaced
to the concurrent one: no need to wait until a previous server had
processed the notification to send the same to the next one.

Release Notes:

- Fixed warm-related panic happening during startup

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-07-30 15:06:06 +03:00
gcp-cherry-pick-bot[bot]
bfb8f24acd
project_panel: Fix autoscroll to treat entries behind sticky items as out of viewport (cherry-pick #35067) (#35315)
Cherry-picked project_panel: Fix autoscroll to treat entries behind
sticky items as out of viewport (#35067)

Closes #34831

Autoscroll centers items only if they’re out of viewport. Before this
PR, entry behind sticky items was not considered out of viewport, and
hence actions like `reveal in project panel` or focusing buffer would
not autoscroll that entry into the view in that case.

This PR fixes that by using recently added `scroll_to_item_with_offset`
in https://github.com/zed-industries/zed/pull/35064.

Release Notes:

- Fixed issue where `pane: reveal in project panel` action was not
working if the entry was behind sticky items.

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-07-30 13:03:43 +05:30
gcp-cherry-pick-bot[bot]
4fd3e220db
gpui: Add scroll_to_item_with_offset to UniformListScrollState (cherry-pick #35064) (#35313)
Cherry-picked gpui: Add `scroll_to_item_with_offset` to
`UniformListScrollState` (#35064)

Previously we had `ScrollStrategy::ToPosition(usize)` which lets you
define the offset where you want to scroll that item to. This is the
same as `ScrollStrategy::Top` but imagine some space reserved at the
top.

This PR removes `ScrollStrategy::ToPosition` in favor of
`scroll_to_item_with_offset` which is the method to do the same. The
reason to add this method is that now not just `ScrollStrategy::Top` but
`ScrollStrategy::Center` can also uses this offset to center the item in
the remaining unreserved space.

```rs
// Before
scroll_handle.scroll_to_item(index, ScrollStrategy::ToPosition(offset));

// After
scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, offset);

// New! Centers item skipping first x items
scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Center, offset);
```

This will be useful for follow up PR.

Release Notes:

- N/A

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-07-30 12:42:16 +05:30
gcp-cherry-pick-bot[bot]
dd60cc285b
client: Send User-Agent header on WebSocket connection requests (cherry-pick #35280) (#35283)
Cherry-picked client: Send `User-Agent` header on WebSocket connection
requests (#35280)

This PR makes it so we send the `User-Agent` header on the WebSocket
connection requests when connecting to Collab.

We use the user agent set on the parent HTTP client.

Release Notes:

- N/A

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-07-29 13:35:35 -04:00
gcp-cherry-pick-bot[bot]
bd1cc7fa50
Add more data to see which extension got leaked (cherry-pick #35272) (#35278)
Cherry-picked Add more data to see which extension got leaked (#35272)

Part of https://github.com/zed-industries/zed/issues/35185

Release Notes:

- N/A

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-07-29 13:02:15 -04:00
Finn Evers
e85c466632 Ensure context servers are spawned in the workspace directory (#35271)
This fixes an issue where we were not setting the context server working
directory at all.

Release Notes:

- Context servers will now be spawned in the currently active project
root.

---------

Co-authored-by: Danilo Leal <danilo@zed.dev>
2025-07-29 18:11:37 +02:00
gcp-cherry-pick-bot[bot]
da887b0cae
Cache LSP code lens requests (cherry-pick #35207) (#35257) 2025-07-29 10:21:40 +03:00
gcp-cherry-pick-bot[bot]
151f330dc5
Fix tasks leaked despite workspace window close (cherry-pick #35246) (#35252) 2025-07-29 10:21:05 +03:00
Zed Bot
f7aa90b2ec Bump to 0.197.3 for @probably-neb 2025-07-28 22:20:50 +00:00
gcp-cherry-pick-bot[bot]
4031bedee5
keymap_ui: Fix bug introduced in #35208 (cherry-pick #35237) (#35240)
Cherry-picked keymap_ui: Fix bug introduced in #35208 (#35237)

Closes #ISSUE

Fixes a bug that was cherry picked onto stable and preview branches
introduced in #35208 whereby modifier keys would show up and not be
removable when editing a keybind

Release Notes:

- (preview only) Keymap Editor: Fixed an issue introduced in v0.197.2
whereby modifier keys would show up and not be removable while recording
keystrokes in the keybind edit modal

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-28 18:02:00 -04:00
Peter Tripp
99debc2504
Fix environment loading with tcsh (#35054)
Closes https://github.com/zed-industries/zed/issues/34973

Fixes an issue where environment variables were not loaded when the
user's shell was tcsh and instead a file named `0` was dumped in the
current working directory with a copy of your environment variables as
json.

Follow-up to: 
- https://github.com/zed-industries/zed/pull/35002
- https://github.com/zed-industries/zed/pull/33599

Release Notes:

- Fixed a regression with loading environment variables in nushell
2025-07-28 16:57:01 -04:00
Peter Tripp
0a9e3c4185
zed 0.197.2 2025-07-28 15:49:12 -04:00
Richard Feldman
2ee15a75db
Use zed settings to detect .zed folders (#35224)
Behind-the-scenes enhancement of
https://github.com/zed-industries/zed/pull/35221

Release Notes:

- N/A
2025-07-28 15:48:34 -04:00
Richard Feldman
e2b863116d
Allow edit tool to access files outside project (with confirmation) (#35221)
Now the edit tool can access files outside the current project (just
like the terminal tool can), but it's behind a prompt (unlike other edit
tool actions).

Release Notes:

- The edit tool can now access files outside the current project, but
only if the user grants it permission to.
2025-07-28 14:02:54 -04:00
gcp-cherry-pick-bot[bot]
916eb996bc
keymap_ui: Additional keystroke input polish (cherry-pick #35208) (#35218)
Cherry-picked keymap_ui: Additional keystroke input polish (#35208)

Closes #ISSUE

Fixed various issues and improved UX around the keystroke input
primarily when used for keystroke search.

Release Notes:

- Keymap Editor: FIxed an issue where the modifiers used to activate
keystroke search would appear in the keystroke search
- Keymap Editor: Made it possible to search for repeat modifiers, such
as a binding with `cmd-shift cmd`
- Keymap Editor: Made keystroke search matches match based on ordered
(not necessarily contiguous) runs. For example, searching for `cmd
shift-j` will match `cmd-k cmd-shift-j alt-q` and `cmd-i g shift-j` but
not `alt-k shift-j` or `cmd-k alt-j`
- Keymap Editor: Fixed the clear keystrokes binding (`delete` by
default) not working in the keystroke input

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-28 13:37:39 -04:00
gcp-cherry-pick-bot[bot]
185122d74e
git: Touch up amend UX (cherry-pick #35114) (#35213)
Cherry-picked git: Touch up amend UX (#35114)

Follow-up to #26114

- Ensure that the previous commit message is filled in when toggling on
amend mode from the context menu
- Fix keybinding flicker in context menu

Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-07-28 13:33:37 -04:00
Todd L Smith
44c6382e13
Fix Nushell environment variables (#35166)
- Fixes environment variable ingestion for Nushell.

Closes #35056

Release Notes:

- N/A
2025-07-28 13:01:09 -04:00
Peter Tripp
b06f843efd
Fix environment loading with nushell (#35002)
Closes https://github.com/zed-industries/zed/issues/34739

I believe this is a regression introduced here:
- https://github.com/zed-industries/zed/pull/33599

Release Notes:

- Fixed a regression with loading environment variables in nushell
2025-07-28 11:52:18 -04:00
Umesh Yadav
39a4409597
lmstudio: Propagate actual error message from server (#34538)
Discovered in this issue: #34513

Previously, we were propagating deserialization errors to users when
using LMStudio, instead of the actual error message sent from the
LMStudio server. This change will help users understand why their
request failed while streaming responses.

Release Notes:

- lmsudio: Display specific backend error messaging on failure rather
than generic ones

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
Co-authored-by: Peter Tripp <peter@zed.dev>
2025-07-25 09:37:17 -04:00
Smit Barmase
c86f82ba72 project_panel: Automatically open project panel when Rename or Duplicate is triggered from workspace (#34988)
In project panel, `rename` and `duplicate` action further needs user
input for editing, so if panel is closed we should open it.

Release Notes:

- Fixed project panel not opening when `project panel: rename` and
`project panel: duplicate` actions are triggered from workspace.
2025-07-24 13:18:16 -04:00
Richard Feldman
ceab8c17f4 Don't auto-retry in certain circumstances (#35037)
Someone encountered this in production, which should not happen:

<img width="1266" height="623" alt="Screenshot 2025-07-24 at 10 38
40 AM"
src="https://github.com/user-attachments/assets/40f3f977-5110-4808-a456-7e708d953b3b"
/>

This moves certain errors into the category of "never retry" and reduces
the number of retries for some others. Also it adds some diagnostic
logging for retry policy.

It's not a complete fix for the above, because the underlying issues is
that the server is sending a HTTP 403 response and although we were
already treating 403s as "do not retry" it was deciding to retry with 2
attempts anyway. So further debugging is needed to figure out why it
wasn't going down the 403 branch by the time the request got here.

Release Notes:

- N/A
2025-07-24 11:50:04 -04:00
Danilo Leal
f6f7762f32 ai onboarding: Add overall fixes to the whole flow (#34996)
Closes https://github.com/zed-industries/zed/issues/34979

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Ben Kunkle <Ben.kunkle@gmail.com>
2025-07-24 11:20:26 -04:00
Oleksiy Syvokon
c015ef64dc
linux: Fix ctrl-0..9, ctrl-[, ctrl-^ (#35028)
There were two different underlying reasons for the issues with
ctrl-number and ctrl-punctuation:

1. Some keys in the ctrl-0..9 range send codes in the `\1b`..`\1f`
range. For example, `ctrl-2` sends keycode for `ctrl-[` (0x1b), but we
want to map it to `2`, not to `[`.

2. `ctrl-[` and four other ctrl-punctuation were incorrectly mapped,
since the expected conversion is by adding 0x40

Closes #35012

Release Notes:

- N/A
2025-07-24 09:45:57 -04:00
Joseph T. Lyons
d3b2f604a9 Differentiate between file and selection diff events (#35014)
Release Notes:

- N/A
2025-07-24 04:43:56 -04:00
Joseph T. Lyons
b8849d83e6 Fix some bugs with editor: diff clipboard with selection (#34999)
Improves testing around `editor: diff clipboard with selection` as well.

Release Notes:

- Fixed some bugs with `editor: diff clipboard with selection`
2025-07-24 02:53:01 -04:00
versecafe
77dda2eca8
ollama: Add Magistral to Ollama (#35000)
See also: #34983

Release Notes:

- Added magistral support to ollama
2025-07-24 00:19:11 -04:00
Peter Tripp
ece9dd2c43
mistral: Add support for magistral-small and magistral-medium (#34983)
Release Notes:

- mistral: Added support for magistral-small and magistral-medium
2025-07-23 23:14:37 -04:00
Renato Lochetti
c60f37a044
mistral: Add support for Mistral Devstral Medium (#34888)
Mistral released their new DevstralMedium model to be used via API:
https://mistral.ai/news/devstral-2507

Release Notes:

- Add support for Mistral Devstral Medium
2025-07-23 23:13:16 -04:00
Joseph T. Lyons
ca646e2951 zed 0.197.1 2025-07-23 17:26:52 -04:00
Umesh Yadav
b5433a9a54 agent_ui: Show keybindings for NewThread and NewTextThread in new thread button (#34967)
I believe in this PR: #34829 we moved to context menu entry from action
but the side effect of that was we also removed the Keybindings from
showing it in the new thread button dropdown. This PR fixes that. cc
@danilo-leal

| Before | After |
|--------|--------|
| <img width="900" height="1962" alt="CleanShot 2025-07-23 at 23 36
28@2x"
src="https://github.com/user-attachments/assets/760cbe75-09b9-404b-9d33-1db73785234f"
/> | <img width="850" height="1964" alt="CleanShot 2025-07-23 at 23 37
17@2x"
src="https://github.com/user-attachments/assets/24a7e871-aebc-475c-845f-b76f02527b8f"
/> |

Release Notes:

- N/A
2025-07-23 17:19:04 -04:00
gcp-cherry-pick-bot[bot]
4727ae35d2
Fix telemetry event type names (cherry-pick #34974) (#34975)
Cherry-picked Fix telemetry event type names (#34974)

Release Notes:

- N/A

Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
2025-07-23 17:09:42 -04:00
Danilo Leal
d61db1fae7 agent: Fix follow button disabled state (#34978)
Release Notes:

- N/A
2025-07-23 17:09:15 -04:00
Joseph T. Lyons
45d211a555 v0.197.x preview 2025-07-23 13:48:22 -04:00
83 changed files with 2491 additions and 1036 deletions

171
Cargo.lock generated
View file

@ -4276,41 +4276,6 @@ dependencies = [
"workspace-hack", "workspace-hack",
] ]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.101",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn 2.0.101",
]
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "5.5.3" version = "5.5.3"
@ -4526,37 +4491,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn 2.0.101",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.19" version = "0.99.19"
@ -4966,6 +4900,7 @@ dependencies = [
"text", "text",
"theme", "theme",
"time", "time",
"tree-sitter-c",
"tree-sitter-html", "tree-sitter-html",
"tree-sitter-python", "tree-sitter-python",
"tree-sitter-rust", "tree-sitter-rust",
@ -5926,7 +5861,7 @@ dependencies = [
"ignore", "ignore",
"libc", "libc",
"log", "log",
"notify", "notify 8.0.0",
"objc", "objc",
"parking_lot", "parking_lot",
"paths", "paths",
@ -7483,18 +7418,16 @@ dependencies = [
[[package]] [[package]]
name = "handlebars" name = "handlebars"
version = "6.3.2" version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b"
dependencies = [ dependencies = [
"derive_builder",
"log", "log",
"num-order",
"pest", "pest",
"pest_derive", "pest_derive",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.12", "thiserror 1.0.69",
] ]
[[package]] [[package]]
@ -8165,12 +8098,6 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "1.0.3" version = "1.0.3"
@ -8389,6 +8316,17 @@ dependencies = [
"zeta", "zeta",
] ]
[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]] [[package]]
name = "inotify" name = "inotify"
version = "0.11.0" version = "0.11.0"
@ -8542,7 +8480,7 @@ dependencies = [
"fnv", "fnv",
"lazy_static", "lazy_static",
"libc", "libc",
"mio", "mio 1.0.3",
"rand 0.8.5", "rand 0.8.5",
"serde", "serde",
"tempfile", "tempfile",
@ -9981,9 +9919,9 @@ dependencies = [
[[package]] [[package]]
name = "mdbook" name = "mdbook"
version = "0.4.48" version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6fbb4ac2d9fd7aa987c3510309ea3c80004a968d063c42f0d34fea070817c1" checksum = "b45a38e19bd200220ef07c892b0157ad3d2365e5b5a267ca01ad12182491eea5"
dependencies = [ dependencies = [
"ammonia", "ammonia",
"anyhow", "anyhow",
@ -9993,12 +9931,11 @@ dependencies = [
"elasticlunr-rs", "elasticlunr-rs",
"env_logger 0.11.8", "env_logger 0.11.8",
"futures-util", "futures-util",
"handlebars 6.3.2", "handlebars 5.1.2",
"hex",
"ignore", "ignore",
"log", "log",
"memchr", "memchr",
"notify", "notify 6.1.1",
"notify-debouncer-mini", "notify-debouncer-mini",
"once_cell", "once_cell",
"opener", "opener",
@ -10007,7 +9944,6 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",
"sha2",
"shlex", "shlex",
"tempfile", "tempfile",
"tokio", "tokio",
@ -10150,6 +10086,18 @@ version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.3" version = "1.0.3"
@ -10519,6 +10467,25 @@ dependencies = [
"zed_actions", "zed_actions",
] ]
[[package]]
name = "notify"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.9.0",
"crossbeam-channel",
"filetime",
"fsevent-sys 4.1.0",
"inotify 0.9.6",
"kqueue",
"libc",
"log",
"mio 0.8.11",
"walkdir",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "notify" name = "notify"
version = "8.0.0" version = "8.0.0"
@ -10527,11 +10494,11 @@ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"filetime", "filetime",
"fsevent-sys 4.1.0", "fsevent-sys 4.1.0",
"inotify", "inotify 0.11.0",
"kqueue", "kqueue",
"libc", "libc",
"log", "log",
"mio", "mio 1.0.3",
"notify-types", "notify-types",
"walkdir", "walkdir",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@ -10539,14 +10506,13 @@ dependencies = [
[[package]] [[package]]
name = "notify-debouncer-mini" name = "notify-debouncer-mini"
version = "0.6.0" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a689eb4262184d9a1727f9087cd03883ea716682ab03ed24efec57d7716dccb8" checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43"
dependencies = [ dependencies = [
"crossbeam-channel",
"log", "log",
"notify", "notify 6.1.1",
"notify-types",
"tempfile",
] ]
[[package]] [[package]]
@ -10686,21 +10652,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-modular"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f"
[[package]]
name = "num-order"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6"
dependencies = [
"num-modular",
]
[[package]] [[package]]
name = "num-rational" name = "num-rational"
version = "0.4.2" version = "0.4.2"
@ -16549,7 +16500,7 @@ dependencies = [
"backtrace", "backtrace",
"bytes 1.10.1", "bytes 1.10.1",
"libc", "libc",
"mio", "mio 1.0.3",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
@ -19726,7 +19677,7 @@ dependencies = [
"md-5", "md-5",
"memchr", "memchr",
"miniz_oxide", "miniz_oxide",
"mio", "mio 1.0.3",
"naga", "naga",
"nix 0.29.0", "nix 0.29.0",
"nom", "nom",
@ -20170,7 +20121,7 @@ dependencies = [
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.197.0" version = "0.197.5"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"agent", "agent",

View file

@ -872,8 +872,6 @@
"tab": "git_panel::FocusEditor", "tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor", "shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus", "escape": "git_panel::ToggleFocus",
"ctrl-enter": "git::Commit",
"ctrl-shift-enter": "git::Amend",
"alt-enter": "menu::SecondaryConfirm", "alt-enter": "menu::SecondaryConfirm",
"delete": ["git::RestoreFile", { "skip_prompt": false }], "delete": ["git::RestoreFile", { "skip_prompt": false }],
"backspace": ["git::RestoreFile", { "skip_prompt": false }], "backspace": ["git::RestoreFile", { "skip_prompt": false }],
@ -910,7 +908,9 @@
"ctrl-g backspace": "git::RestoreTrackedFiles", "ctrl-g backspace": "git::RestoreTrackedFiles",
"ctrl-g shift-backspace": "git::TrashUntrackedFiles", "ctrl-g shift-backspace": "git::TrashUntrackedFiles",
"ctrl-space": "git::StageAll", "ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll" "ctrl-shift-space": "git::UnstageAll",
"ctrl-enter": "git::Commit",
"ctrl-shift-enter": "git::Amend"
} }
}, },
{ {

View file

@ -950,8 +950,6 @@
"tab": "git_panel::FocusEditor", "tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor", "shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus", "escape": "git_panel::ToggleFocus",
"cmd-enter": "git::Commit",
"cmd-shift-enter": "git::Amend",
"backspace": ["git::RestoreFile", { "skip_prompt": false }], "backspace": ["git::RestoreFile", { "skip_prompt": false }],
"delete": ["git::RestoreFile", { "skip_prompt": false }], "delete": ["git::RestoreFile", { "skip_prompt": false }],
"cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }], "cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }],
@ -1001,7 +999,9 @@
"ctrl-g backspace": "git::RestoreTrackedFiles", "ctrl-g backspace": "git::RestoreTrackedFiles",
"ctrl-g shift-backspace": "git::TrashUntrackedFiles", "ctrl-g shift-backspace": "git::TrashUntrackedFiles",
"cmd-ctrl-y": "git::StageAll", "cmd-ctrl-y": "git::StageAll",
"cmd-ctrl-shift-y": "git::UnstageAll" "cmd-ctrl-shift-y": "git::UnstageAll",
"cmd-enter": "git::Commit",
"cmd-shift-enter": "git::Amend"
} }
}, },
{ {

View file

@ -94,7 +94,7 @@
"ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }], "ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
"alt-shift-f10": "task::Spawn", "alt-shift-f10": "task::Spawn",
"ctrl-e": "file_finder::Toggle", "ctrl-e": "file_finder::Toggle",
"ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor // "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
"ctrl-shift-n": "file_finder::Toggle", "ctrl-shift-n": "file_finder::Toggle",
"ctrl-shift-a": "command_palette::Toggle", "ctrl-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle", "shift shift": "command_palette::Toggle",
@ -150,7 +150,7 @@
{ "context": "Diagnostics > Editor", "bindings": { "alt-6": "pane::CloseActiveItem" } }, { "context": "Diagnostics > Editor", "bindings": { "alt-6": "pane::CloseActiveItem" } },
{ "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } }, { "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } },
{ {
"context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", "context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
"bindings": { "escape": "editor::ToggleFocus" } "bindings": { "escape": "editor::ToggleFocus" }
} }
] ]

View file

@ -96,7 +96,7 @@
"cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }], "cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-alt-r": "task::Spawn", "ctrl-alt-r": "task::Spawn",
"cmd-e": "file_finder::Toggle", "cmd-e": "file_finder::Toggle",
"cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor // "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
"cmd-shift-o": "file_finder::Toggle", "cmd-shift-o": "file_finder::Toggle",
"cmd-shift-a": "command_palette::Toggle", "cmd-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle", "shift shift": "command_palette::Toggle",
@ -150,7 +150,7 @@
{ "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } }, { "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } },
{ "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } }, { "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } },
{ {
"context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", "context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
"bindings": { "escape": "editor::ToggleFocus" } "bindings": { "escape": "editor::ToggleFocus" }
} }
] ]

View file

@ -308,7 +308,12 @@ mod tests {
unimplemented!() unimplemented!()
} }
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool { fn needs_confirmation(
&self,
_input: &serde_json::Value,
_project: &Entity<Project>,
_cx: &App,
) -> bool {
unimplemented!() unimplemented!()
} }

View file

@ -47,7 +47,7 @@ impl Tool for ContextServerTool {
} }
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
true true
} }

View file

@ -942,7 +942,7 @@ impl Thread {
} }
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> { pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
self.tool_use.tool_uses_for_message(id, cx) self.tool_use.tool_uses_for_message(id, &self.project, cx)
} }
pub fn tool_results_for_message( pub fn tool_results_for_message(
@ -2037,6 +2037,12 @@ impl Thread {
if let Some(retry_strategy) = if let Some(retry_strategy) =
Thread::get_retry_strategy(completion_error) Thread::get_retry_strategy(completion_error)
{ {
log::info!(
"Retrying with {:?} for language model completion error {:?}",
retry_strategy,
completion_error
);
retry_scheduled = thread retry_scheduled = thread
.handle_retryable_error_with_delay( .handle_retryable_error_with_delay(
&completion_error, &completion_error,
@ -2246,15 +2252,14 @@ impl Thread {
.. ..
} }
| AuthenticationError { .. } | AuthenticationError { .. }
| PermissionError { .. } => None, | PermissionError { .. }
// These errors might be transient, so retry them | NoApiKey { .. }
SerializeRequest { .. }
| BuildRequestBody { .. }
| PromptTooLarge { .. }
| ApiEndpointNotFound { .. } | ApiEndpointNotFound { .. }
| NoApiKey { .. } => Some(RetryStrategy::Fixed { | PromptTooLarge { .. } => None,
// These errors might be transient, so retry them
SerializeRequest { .. } | BuildRequestBody { .. } => Some(RetryStrategy::Fixed {
delay: BASE_RETRY_DELAY, delay: BASE_RETRY_DELAY,
max_attempts: 2, max_attempts: 1,
}), }),
// Retry all other 4xx and 5xx errors once. // Retry all other 4xx and 5xx errors once.
HttpResponseError { status_code, .. } HttpResponseError { status_code, .. }
@ -2552,7 +2557,7 @@ impl Thread {
return self.handle_hallucinated_tool_use(tool_use.id, tool_use.name, window, cx); return self.handle_hallucinated_tool_use(tool_use.id, tool_use.name, window, cx);
} }
if tool.needs_confirmation(&tool_use.input, cx) if tool.needs_confirmation(&tool_use.input, &self.project, cx)
&& !AgentSettings::get_global(cx).always_allow_tool_actions && !AgentSettings::get_global(cx).always_allow_tool_actions
{ {
self.tool_use.confirm_tool_use( self.tool_use.confirm_tool_use(

View file

@ -41,6 +41,9 @@ use std::{
}; };
use util::ResultExt as _; use util::ResultExt as _;
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataType { pub enum DataType {
#[serde(rename = "json")] #[serde(rename = "json")]
@ -874,7 +877,11 @@ impl ThreadsDatabase {
let needs_migration_from_heed = mdb_path.exists(); let needs_migration_from_heed = mdb_path.exists();
let connection = Connection::open_file(&sqlite_path.to_string_lossy()); let connection = if *ZED_STATELESS {
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
} else {
Connection::open_file(&sqlite_path.to_string_lossy())
};
connection.exec(indoc! {" connection.exec(indoc! {"
CREATE TABLE IF NOT EXISTS threads ( CREATE TABLE IF NOT EXISTS threads (

View file

@ -165,7 +165,12 @@ impl ToolUseState {
self.pending_tool_uses_by_id.values().collect() self.pending_tool_uses_by_id.values().collect()
} }
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> { pub fn tool_uses_for_message(
&self,
id: MessageId,
project: &Entity<Project>,
cx: &App,
) -> Vec<ToolUse> {
let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else { let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else {
return Vec::new(); return Vec::new();
}; };
@ -211,7 +216,10 @@ impl ToolUseState {
let (icon, needs_confirmation) = let (icon, needs_confirmation) =
if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) { if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
(tool.icon(), tool.needs_confirmation(&tool_use.input, cx)) (
tool.icon(),
tool.needs_confirmation(&tool_use.input, project, cx),
)
} else { } else {
(IconName::Cog, false) (IconName::Cog, false)
}; };

View file

@ -185,6 +185,13 @@ impl AgentConfiguration {
None None
}; };
let is_signed_in = self
.workspace
.read_with(cx, |workspace, _| {
workspace.client().status().borrow().is_connected()
})
.unwrap_or(false);
v_flex() v_flex()
.when(is_expanded, |this| this.mb_2()) .when(is_expanded, |this| this.mb_2())
.child( .child(
@ -230,8 +237,8 @@ impl AgentConfiguration {
.size(LabelSize::Large), .size(LabelSize::Large),
) )
.map(|this| { .map(|this| {
if is_zed_provider { if is_zed_provider && is_signed_in {
this.gap_2().child( this.child(
self.render_zed_plan_info(current_plan, cx), self.render_zed_plan_info(current_plan, cx),
) )
} else { } else {

View file

@ -564,6 +564,17 @@ impl AgentPanel {
let inline_assist_context_store = let inline_assist_context_store =
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade()))); cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
let thread_id = thread.read(cx).id().clone();
let history_store = cx.new(|cx| {
HistoryStore::new(
thread_store.clone(),
context_store.clone(),
[HistoryEntryId::Thread(thread_id)],
cx,
)
});
let message_editor = cx.new(|cx| { let message_editor = cx.new(|cx| {
MessageEditor::new( MessageEditor::new(
fs.clone(), fs.clone(),
@ -573,22 +584,13 @@ impl AgentPanel {
prompt_store.clone(), prompt_store.clone(),
thread_store.downgrade(), thread_store.downgrade(),
context_store.downgrade(), context_store.downgrade(),
Some(history_store.downgrade()),
thread.clone(), thread.clone(),
window, window,
cx, cx,
) )
}); });
let thread_id = thread.read(cx).id().clone();
let history_store = cx.new(|cx| {
HistoryStore::new(
thread_store.clone(),
context_store.clone(),
[HistoryEntryId::Thread(thread_id)],
cx,
)
});
cx.observe(&history_store, |_, _, cx| cx.notify()).detach(); cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
let active_thread = cx.new(|cx| { let active_thread = cx.new(|cx| {
@ -851,6 +853,7 @@ impl AgentPanel {
self.prompt_store.clone(), self.prompt_store.clone(),
self.thread_store.downgrade(), self.thread_store.downgrade(),
self.context_store.downgrade(), self.context_store.downgrade(),
Some(self.history_store.downgrade()),
thread.clone(), thread.clone(),
window, window,
cx, cx,
@ -1124,6 +1127,7 @@ impl AgentPanel {
self.prompt_store.clone(), self.prompt_store.clone(),
self.thread_store.downgrade(), self.thread_store.downgrade(),
self.context_store.downgrade(), self.context_store.downgrade(),
Some(self.history_store.downgrade()),
thread.clone(), thread.clone(),
window, window,
cx, cx,
@ -1901,85 +1905,96 @@ impl AgentPanel {
) )
.anchor(Corner::TopRight) .anchor(Corner::TopRight)
.with_handle(self.new_thread_menu_handle.clone()) .with_handle(self.new_thread_menu_handle.clone())
.menu(move |window, cx| { .menu({
let active_thread = active_thread.clone(); let focus_handle = focus_handle.clone();
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { move |window, cx| {
menu = menu let active_thread = active_thread.clone();
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| { Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
this.header("Zed Agent") menu = menu
}) .context(focus_handle.clone())
.item( .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
ContextMenuEntry::new("New Thread") this.header("Zed Agent")
.icon(IconName::NewThread) })
.icon_color(Color::Muted) .item(
.handler(move |window, cx| { ContextMenuEntry::new("New Thread")
window.dispatch_action(NewThread::default().boxed_clone(), cx); .icon(IconName::NewThread)
}), .icon_color(Color::Muted)
) .action(NewThread::default().boxed_clone())
.item( .handler(move |window, cx| {
ContextMenuEntry::new("New Text Thread") window.dispatch_action(
.icon(IconName::NewTextThread) NewThread::default().boxed_clone(),
.icon_color(Color::Muted) cx,
.handler(move |window, cx| { );
window.dispatch_action(NewTextThread.boxed_clone(), cx); }),
}), )
) .item(
.when_some(active_thread, |this, active_thread| { ContextMenuEntry::new("New Text Thread")
let thread = active_thread.read(cx); .icon(IconName::NewTextThread)
.icon_color(Color::Muted)
.action(NewTextThread.boxed_clone())
.handler(move |window, cx| {
window.dispatch_action(NewTextThread.boxed_clone(), cx);
}),
)
.when_some(active_thread, |this, active_thread| {
let thread = active_thread.read(cx);
if !thread.is_empty() { if !thread.is_empty() {
let thread_id = thread.id().clone(); let thread_id = thread.id().clone();
this.item( this.item(
ContextMenuEntry::new("New From Summary") ContextMenuEntry::new("New From Summary")
.icon(IconName::NewFromSummary) .icon(IconName::NewFromSummary)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.handler(move |window, cx| { .handler(move |window, cx| {
window.dispatch_action( window.dispatch_action(
Box::new(NewThread { Box::new(NewThread {
from_thread_id: Some(thread_id.clone()), from_thread_id: Some(thread_id.clone()),
}), }),
cx, cx,
); );
}), }),
) )
} else { } else {
this this
} }
}) })
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| { .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
this.separator() this.separator()
.header("External Agents") .header("External Agents")
.item( .item(
ContextMenuEntry::new("New Gemini Thread") ContextMenuEntry::new("New Gemini Thread")
.icon(IconName::AiGemini) .icon(IconName::AiGemini)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.handler(move |window, cx| { .handler(move |window, cx| {
window.dispatch_action( window.dispatch_action(
NewExternalAgentThread { NewExternalAgentThread {
agent: Some(crate::ExternalAgent::Gemini), agent: Some(crate::ExternalAgent::Gemini),
} }
.boxed_clone(), .boxed_clone(),
cx, cx,
); );
}), }),
) )
.item( .item(
ContextMenuEntry::new("New Claude Code Thread") ContextMenuEntry::new("New Claude Code Thread")
.icon(IconName::AiClaude) .icon(IconName::AiClaude)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.handler(move |window, cx| { .handler(move |window, cx| {
window.dispatch_action( window.dispatch_action(
NewExternalAgentThread { NewExternalAgentThread {
agent: Some(crate::ExternalAgent::ClaudeCode), agent: Some(
} crate::ExternalAgent::ClaudeCode,
.boxed_clone(), ),
cx, }
); .boxed_clone(),
}), cx,
) );
}); }),
menu )
})) });
menu
}))
}
}); });
let agent_panel_menu = PopoverMenu::new("agent-options-menu") let agent_panel_menu = PopoverMenu::new("agent-options-menu")
@ -2272,20 +2287,21 @@ impl AgentPanel {
} }
match &self.active_view { match &self.active_view {
ActiveView::Thread { thread, .. } => thread ActiveView::Thread { .. } | ActiveView::TextThread { .. } => {
.read(cx) let history_is_empty = self
.thread() .history_store
.read(cx) .update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
.configured_model()
.map_or(true, |model| { let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID .providers()
}), .iter()
ActiveView::TextThread { .. } => LanguageModelRegistry::global(cx) .any(|provider| {
.read(cx) provider.is_authenticated(cx)
.default_model() && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
.map_or(true, |model| { });
model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID
}), history_is_empty || !has_configured_non_zed_providers
}
ActiveView::ExternalAgentThread { .. } ActiveView::ExternalAgentThread { .. }
| ActiveView::History | ActiveView::History
| ActiveView::Configuration => false, | ActiveView::Configuration => false,
@ -2306,9 +2322,8 @@ impl AgentPanel {
Some( Some(
div() div()
.size_full()
.when(thread_view, |this| { .when(thread_view, |this| {
this.bg(cx.theme().colors().panel_background) this.size_full().bg(cx.theme().colors().panel_background)
}) })
.when(text_thread_view, |this| { .when(text_thread_view, |this| {
this.bg(cx.theme().colors().editor_background) this.bg(cx.theme().colors().editor_background)

View file

@ -262,6 +262,8 @@ fn update_command_palette_filter(cx: &mut App) {
if disable_ai { if disable_ai {
filter.hide_namespace("agent"); filter.hide_namespace("agent");
filter.hide_namespace("assistant"); filter.hide_namespace("assistant");
filter.hide_namespace("copilot");
filter.hide_namespace("supermaven");
filter.hide_namespace("zed_predict_onboarding"); filter.hide_namespace("zed_predict_onboarding");
filter.hide_namespace("edit_prediction"); filter.hide_namespace("edit_prediction");

View file

@ -9,6 +9,7 @@ use crate::ui::{
MaxModeTooltip, MaxModeTooltip,
preview::{AgentPreview, UsageCallout}, preview::{AgentPreview, UsageCallout},
}; };
use agent::history_store::HistoryStore;
use agent::{ use agent::{
context::{AgentContextKey, ContextLoadResult, load_context}, context::{AgentContextKey, ContextLoadResult, load_context},
context_store::ContextStoreEvent, context_store::ContextStoreEvent,
@ -29,8 +30,9 @@ use fs::Fs;
use futures::future::Shared; use futures::future::Shared;
use futures::{FutureExt as _, future}; use futures::{FutureExt as _, future};
use gpui::{ use gpui::{
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, KeyContext, Subscription, Task, Animation, AnimationExt, App, Entity, EventEmitter, Focusable, IntoElement, KeyContext,
TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between, Subscription, Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point,
pulsating_between,
}; };
use language::{Buffer, Language, Point}; use language::{Buffer, Language, Point};
use language_model::{ use language_model::{
@ -80,6 +82,7 @@ pub struct MessageEditor {
user_store: Entity<UserStore>, user_store: Entity<UserStore>,
context_store: Entity<ContextStore>, context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
history_store: Option<WeakEntity<HistoryStore>>,
context_strip: Entity<ContextStrip>, context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>, context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AgentModelSelector>, model_selector: Entity<AgentModelSelector>,
@ -161,6 +164,7 @@ impl MessageEditor {
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
thread_store: WeakEntity<ThreadStore>, thread_store: WeakEntity<ThreadStore>,
text_thread_store: WeakEntity<TextThreadStore>, text_thread_store: WeakEntity<TextThreadStore>,
history_store: Option<WeakEntity<HistoryStore>>,
thread: Entity<Thread>, thread: Entity<Thread>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@ -233,6 +237,7 @@ impl MessageEditor {
workspace, workspace,
context_store, context_store,
prompt_store, prompt_store,
history_store,
context_strip, context_strip,
context_picker_menu_handle, context_picker_menu_handle,
load_context_task: None, load_context_task: None,
@ -625,7 +630,7 @@ impl MessageEditor {
.unwrap_or(false); .unwrap_or(false);
IconButton::new("follow-agent", IconName::Crosshair) IconButton::new("follow-agent", IconName::Crosshair)
.disabled(is_model_selected) .disabled(!is_model_selected)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.toggle_state(following) .toggle_state(following)
@ -1661,32 +1666,36 @@ impl Render for MessageEditor {
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5; let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
let in_pro_trial = matches!( let has_configured_providers = LanguageModelRegistry::read_global(cx)
self.user_store.read(cx).current_plan(), .providers()
Some(proto::Plan::ZedProTrial) .iter()
); .filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
.count()
> 0;
let pro_user = matches!( let is_signed_out = self
self.user_store.read(cx).current_plan(), .workspace
Some(proto::Plan::ZedPro) .read_with(cx, |workspace, _| {
); workspace.client().status().borrow().is_signed_out()
})
.unwrap_or(true);
let configured_providers: Vec<(IconName, SharedString)> = let has_history = self
LanguageModelRegistry::read_global(cx) .history_store
.providers() .as_ref()
.iter() .and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok())
.filter(|provider| { .unwrap_or(false)
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID || self
}) .thread
.map(|provider| (provider.icon(), provider.name().0.clone())) .read_with(cx, |thread, _| thread.messages().len() > 0);
.collect();
let has_existing_providers = configured_providers.len() > 0;
v_flex() v_flex()
.size_full() .size_full()
.bg(cx.theme().colors().panel_background) .bg(cx.theme().colors().panel_background)
.when( .when(
has_existing_providers && !in_pro_trial && !pro_user, !has_history && is_signed_out && has_configured_providers,
|this| this.child(cx.new(ApiKeysWithProviders::new)), |this| this.child(cx.new(ApiKeysWithProviders::new)),
) )
.when(changed_buffers.len() > 0, |parent| { .when(changed_buffers.len() > 0, |parent| {
@ -1778,6 +1787,7 @@ impl AgentPreview for MessageEditor {
None, None,
thread_store.downgrade(), thread_store.downgrade(),
text_thread_store.downgrade(), text_thread_store.downgrade(),
None,
thread, thread,
window, window,
cx, cx,

View file

@ -5,7 +5,6 @@ mod end_trial_upsell;
mod new_thread_button; mod new_thread_button;
mod onboarding_modal; mod onboarding_modal;
pub mod preview; pub mod preview;
mod upsell;
pub use agent_notification::*; pub use agent_notification::*;
pub use burn_mode_tooltip::*; pub use burn_mode_tooltip::*;

View file

@ -3,7 +3,7 @@ use std::sync::Arc;
use ai_onboarding::{AgentPanelOnboardingCard, BulletItem}; use ai_onboarding::{AgentPanelOnboardingCard, BulletItem};
use client::zed_urls; use client::zed_urls;
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use ui::{Divider, List, prelude::*}; use ui::{Divider, List, Tooltip, prelude::*};
#[derive(IntoElement, RegisterComponent)] #[derive(IntoElement, RegisterComponent)]
pub struct EndTrialUpsell { pub struct EndTrialUpsell {
@ -33,14 +33,19 @@ impl RenderOnce for EndTrialUpsell {
) )
.child( .child(
List::new() List::new()
.child(BulletItem::new("500 prompts per month with Claude models")) .child(BulletItem::new("500 prompts with Claude models"))
.child(BulletItem::new("Unlimited edit predictions")), .child(BulletItem::new(
"Unlimited edit predictions with Zeta, our open-source model",
)),
) )
.child( .child(
Button::new("cta-button", "Upgrade to Zed Pro") Button::new("cta-button", "Upgrade to Zed Pro")
.full_width() .full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent)) .style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))), .on_click(move |_, _window, cx| {
telemetry::event!("Upgrade To Pro Clicked", state = "end-of-trial");
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
}),
); );
let free_section = v_flex() let free_section = v_flex()
@ -55,37 +60,43 @@ impl RenderOnce for EndTrialUpsell {
.color(Color::Muted) .color(Color::Muted)
.buffer_font(cx), .buffer_font(cx),
) )
.child(
Label::new("(Current Plan)")
.size(LabelSize::Small)
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
.buffer_font(cx),
)
.child(Divider::horizontal()), .child(Divider::horizontal()),
) )
.child( .child(
List::new() List::new()
.child(BulletItem::new( .child(BulletItem::new("50 prompts with the Claude models"))
"50 prompts per month with the Claude models", .child(BulletItem::new("2,000 accepted edit predictions")),
))
.child(BulletItem::new(
"2000 accepted edit predictions using our open-source Zeta model",
)),
)
.child(
Button::new("dismiss-button", "Stay on Free")
.full_width()
.style(ButtonStyle::Outlined)
.on_click({
let callback = self.dismiss_upsell.clone();
move |_, window, cx| callback(window, cx)
}),
); );
AgentPanelOnboardingCard::new() AgentPanelOnboardingCard::new()
.child(Headline::new("Your Zed Pro trial has expired.")) .child(Headline::new("Your Zed Pro Trial has expired"))
.child( .child(
Label::new("You've been automatically reset to the Free plan.") Label::new("You've been automatically reset to the Free plan.")
.size(LabelSize::Small)
.color(Color::Muted) .color(Color::Muted)
.mb_1(), .mb_2(),
) )
.child(pro_section) .child(pro_section)
.child(free_section) .child(free_section)
.child(
h_flex().absolute().top_4().right_4().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click({
let callback = self.dismiss_upsell.clone();
move |_, window, cx| {
telemetry::event!("Banner Dismissed", source = "AI Onboarding");
callback(window, cx)
}
}),
),
)
} }
} }

View file

@ -1,163 +0,0 @@
use component::{Component, ComponentScope, single_example};
use gpui::{
AnyElement, App, ClickEvent, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
Window,
};
use theme::ActiveTheme;
use ui::{
Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, Color, Label, LabelCommon,
RegisterComponent, ToggleState, h_flex, v_flex,
};
/// A component that displays an upsell message with a call-to-action button
///
/// # Example
/// ```
/// let upsell = Upsell::new(
/// "Upgrade to Zed Pro",
/// "Get access to advanced AI features and more",
/// "Upgrade Now",
/// Box::new(|_, _window, cx| {
/// cx.open_url("https://zed.dev/pricing");
/// }),
/// Box::new(|_, _window, cx| {
/// // Handle dismiss
/// }),
/// Box::new(|checked, window, cx| {
/// // Handle don't show again
/// }),
/// );
/// ```
#[derive(IntoElement, RegisterComponent)]
pub struct Upsell {
title: SharedString,
message: SharedString,
cta_text: SharedString,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App) + 'static>,
}
impl Upsell {
/// Create a new upsell component
pub fn new(
title: impl Into<SharedString>,
message: impl Into<SharedString>,
cta_text: impl Into<SharedString>,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App) + 'static>,
) -> Self {
Self {
title: title.into(),
message: message.into(),
cta_text: cta_text.into(),
on_click,
on_dismiss,
on_dont_show_again,
}
}
}
impl RenderOnce for Upsell {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.w_full()
.p_4()
.gap_3()
.bg(cx.theme().colors().surface_background)
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
.gap_1()
.child(
Label::new(self.title)
.size(ui::LabelSize::Large)
.weight(gpui::FontWeight::BOLD),
)
.child(Label::new(self.message).color(Color::Muted)),
)
.child(
h_flex()
.w_full()
.justify_between()
.items_center()
.child(
h_flex()
.items_center()
.gap_1()
.child(
Checkbox::new("dont-show-again", ToggleState::Unselected).on_click(
move |_, window, cx| {
(self.on_dont_show_again)(true, window, cx);
},
),
)
.child(
Label::new("Don't show again")
.color(Color::Muted)
.size(ui::LabelSize::Small),
),
)
.child(
h_flex()
.gap_2()
.child(
Button::new("dismiss-button", "No Thanks")
.style(ButtonStyle::Subtle)
.on_click(self.on_dismiss),
)
.child(
Button::new("cta-button", self.cta_text)
.style(ButtonStyle::Filled)
.on_click(self.on_click),
),
),
)
}
}
impl Component for Upsell {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn name() -> &'static str {
"Upsell"
}
fn description() -> Option<&'static str> {
Some("A promotional component that displays a message with a call-to-action.")
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let examples = vec![
single_example(
"Default",
Upsell::new(
"Upgrade to Zed Pro",
"Get unlimited access to AI features and more with Zed Pro. Unlock advanced AI capabilities and other premium features.",
"Upgrade Now",
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
).render(window, cx).into_any_element(),
),
single_example(
"Short Message",
Upsell::new(
"Try Zed Pro for free",
"Start your 7-day trial today.",
"Start Trial",
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
).render(window, cx).into_any_element(),
),
];
Some(v_flex().gap_4().children(examples).into_any_element())
}
}

View file

@ -61,6 +61,11 @@ impl Render for AgentPanelOnboarding {
Some(proto::Plan::ZedProTrial) Some(proto::Plan::ZedProTrial)
); );
let is_pro_user = matches!(
self.user_store.read(cx).current_plan(),
Some(proto::Plan::ZedPro)
);
AgentPanelOnboardingCard::new() AgentPanelOnboardingCard::new()
.child( .child(
ZedAiOnboarding::new( ZedAiOnboarding::new(
@ -75,7 +80,7 @@ impl Render for AgentPanelOnboarding {
}), }),
) )
.map(|this| { .map(|this| {
if enrolled_in_trial || self.configured_providers.len() >= 1 { if enrolled_in_trial || is_pro_user || self.configured_providers.len() >= 1 {
this this
} else { } else {
this.child(ApiKeysWithoutProviders::new()) this.child(ApiKeysWithoutProviders::new())

View file

@ -16,6 +16,7 @@ use client::{Client, UserStore, zed_urls};
use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString}; use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*}; use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
#[derive(IntoElement)]
pub struct BulletItem { pub struct BulletItem {
label: SharedString, label: SharedString,
} }
@ -28,18 +29,27 @@ impl BulletItem {
} }
} }
impl IntoElement for BulletItem { impl RenderOnce for BulletItem {
type Element = AnyElement; fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
let line_height = 0.85 * window.line_height();
fn into_element(self) -> Self::Element {
ListItem::new("list-item") ListItem::new("list-item")
.selectable(false) .selectable(false)
.start_slot( .child(
Icon::new(IconName::Dash) h_flex()
.size(IconSize::XSmall) .w_full()
.color(Color::Hidden), .min_w_0()
.gap_1()
.items_start()
.child(
h_flex().h(line_height).justify_center().child(
Icon::new(IconName::Dash)
.size(IconSize::XSmall)
.color(Color::Hidden),
),
)
.child(div().w_full().min_w_0().child(Label::new(self.label))),
) )
.child(div().w_full().child(Label::new(self.label)))
.into_any_element() .into_any_element()
} }
} }
@ -237,7 +247,7 @@ impl ZedAiOnboarding {
.icon_color(Color::Muted) .icon_color(Color::Muted)
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.on_click(move |_, _window, cx| { .on_click(move |_, _window, cx| {
telemetry::event!("Review Terms of Service Click"); telemetry::event!("Review Terms of Service Clicked");
cx.open_url(&zed_urls::terms_of_service(cx)) cx.open_url(&zed_urls::terms_of_service(cx))
}), }),
) )
@ -248,7 +258,7 @@ impl ZedAiOnboarding {
.on_click({ .on_click({
let callback = self.accept_terms_of_service.clone(); let callback = self.accept_terms_of_service.clone();
move |_, window, cx| { move |_, window, cx| {
telemetry::event!("Accepted Terms of Service"); telemetry::event!("Terms of Service Accepted");
(callback)(window, cx)} (callback)(window, cx)}
}), }),
) )
@ -373,7 +383,9 @@ impl ZedAiOnboarding {
.child( .child(
List::new() List::new()
.child(BulletItem::new("500 prompts with Claude models")) .child(BulletItem::new("500 prompts with Claude models"))
.child(BulletItem::new("Unlimited edit predictions")), .child(BulletItem::new(
"Unlimited edit predictions with Zeta, our open-source model",
)),
) )
.child( .child(
Button::new("pro", "Continue with Zed Pro") Button::new("pro", "Continue with Zed Pro")

View file

@ -767,6 +767,11 @@ impl ContextStore {
fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> { fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let fs = self.fs.clone(); let fs = self.fs.clone();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
pub static ZED_STATELESS: LazyLock<bool> =
LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
if *ZED_STATELESS {
return Ok(());
}
fs.create_dir(contexts_dir()).await?; fs.create_dir(contexts_dir()).await?;
let mut paths = fs.read_dir(contexts_dir()).await?; let mut paths = fs.read_dir(contexts_dir()).await?;

View file

@ -216,7 +216,12 @@ pub trait Tool: 'static + Send + Sync {
/// Returns true if the tool needs the users's confirmation /// Returns true if the tool needs the users's confirmation
/// before having permission to run. /// before having permission to run.
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool; fn needs_confirmation(
&self,
input: &serde_json::Value,
project: &Entity<Project>,
cx: &App,
) -> bool;
/// Returns true if the tool may perform edits. /// Returns true if the tool may perform edits.
fn may_perform_edits(&self) -> bool; fn may_perform_edits(&self) -> bool;

View file

@ -375,7 +375,12 @@ mod tests {
false false
} }
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool { fn needs_confirmation(
&self,
_input: &serde_json::Value,
_project: &Entity<Project>,
_cx: &App,
) -> bool {
true true
} }

View file

@ -44,7 +44,7 @@ impl Tool for CopyPathTool {
"copy_path".into() "copy_path".into()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false false
} }

View file

@ -37,7 +37,7 @@ impl Tool for CreateDirectoryTool {
include_str!("./create_directory_tool/description.md").into() include_str!("./create_directory_tool/description.md").into()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false false
} }

View file

@ -33,7 +33,7 @@ impl Tool for DeletePathTool {
"delete_path".into() "delete_path".into()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false false
} }

View file

@ -46,7 +46,7 @@ impl Tool for DiagnosticsTool {
"diagnostics".into() "diagnostics".into()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false false
} }

View file

@ -25,6 +25,7 @@ use language::{
}; };
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use paths;
use project::{ use project::{
Project, ProjectPath, Project, ProjectPath,
lsp_store::{FormatTrigger, LspFormatTarget}, lsp_store::{FormatTrigger, LspFormatTarget},
@ -126,8 +127,47 @@ impl Tool for EditFileTool {
"edit_file".into() "edit_file".into()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(
false &self,
input: &serde_json::Value,
project: &Entity<Project>,
cx: &App,
) -> bool {
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
return false;
}
let Ok(input) = serde_json::from_value::<EditFileToolInput>(input.clone()) else {
// If it's not valid JSON, it's going to error and confirming won't do anything.
return false;
};
// If any path component matches the local settings folder, then this could affect
// the editor in ways beyond the project source, so prompt.
let local_settings_folder = paths::local_settings_folder_relative_path();
let path = Path::new(&input.path);
if path
.components()
.any(|component| component.as_os_str() == local_settings_folder.as_os_str())
{
return true;
}
// It's also possible that the global config dir is configured to be inside the project,
// so check for that edge case too.
if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
if canonical_path.starts_with(paths::config_dir()) {
return true;
}
}
// Check if path is inside the global config directory
// First check if it's already inside project - if not, try to canonicalize
let project_path = project.read(cx).find_project_path(&input.path, cx);
// If the path is inside the project, and it's not one of the above edge cases,
// then no confirmation is necessary. Otherwise, confirmation is necessary.
project_path.is_none()
} }
fn may_perform_edits(&self) -> bool { fn may_perform_edits(&self) -> bool {
@ -148,7 +188,25 @@ impl Tool for EditFileTool {
fn ui_text(&self, input: &serde_json::Value) -> String { fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<EditFileToolInput>(input.clone()) { match serde_json::from_value::<EditFileToolInput>(input.clone()) {
Ok(input) => input.display_description, Ok(input) => {
let path = Path::new(&input.path);
let mut description = input.display_description.clone();
// Add context about why confirmation may be needed
let local_settings_folder = paths::local_settings_folder_relative_path();
if path
.components()
.any(|c| c.as_os_str() == local_settings_folder.as_os_str())
{
description.push_str(" (local settings)");
} else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
if canonical_path.starts_with(paths::config_dir()) {
description.push_str(" (global settings)");
}
}
description
}
Err(_) => "Editing file".to_string(), Err(_) => "Editing file".to_string(),
} }
} }
@ -1175,19 +1233,20 @@ async fn build_buffer_diff(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use ::fs::Fs;
use client::TelemetrySettings; use client::TelemetrySettings;
use fs::{FakeFs, Fs};
use gpui::{TestAppContext, UpdateGlobal}; use gpui::{TestAppContext, UpdateGlobal};
use language_model::fake_provider::FakeLanguageModel; use language_model::fake_provider::FakeLanguageModel;
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use std::fs;
use util::path; use util::path;
#[gpui::test] #[gpui::test]
async fn test_edit_nonexistent_file(cx: &mut TestAppContext) { async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({})).await; fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
@ -1277,7 +1336,7 @@ mod tests {
) -> anyhow::Result<ProjectPath> { ) -> anyhow::Result<ProjectPath> {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
json!({ json!({
@ -1384,6 +1443,21 @@ mod tests {
cx.set_global(settings_store); cx.set_global(settings_store);
language::init(cx); language::init(cx);
TelemetrySettings::register(cx); TelemetrySettings::register(cx);
agent_settings::AgentSettings::register(cx);
Project::init_settings(cx);
});
}
fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) {
cx.update(|cx| {
// Set custom data directory (config will be under data_dir/config)
paths::set_custom_data_dir(data_dir.to_str().unwrap());
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
TelemetrySettings::register(cx);
agent_settings::AgentSettings::register(cx);
Project::init_settings(cx); Project::init_settings(cx);
}); });
} }
@ -1392,7 +1466,7 @@ mod tests {
async fn test_format_on_save(cx: &mut TestAppContext) { async fn test_format_on_save(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({"src": {}})).await; fs.insert_tree("/root", json!({"src": {}})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
@ -1591,7 +1665,7 @@ mod tests {
async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) { async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({"src": {}})).await; fs.insert_tree("/root", json!({"src": {}})).await;
// Create a simple file with trailing whitespace // Create a simple file with trailing whitespace
@ -1723,4 +1797,641 @@ mod tests {
"Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled" "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
); );
} }
#[gpui::test]
async fn test_needs_confirmation(cx: &mut TestAppContext) {
init_test(cx);
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({})).await;
// Test 1: Path with .zed component should require confirmation
let input_with_zed = json!({
"display_description": "Edit settings",
"path": ".zed/settings.json",
"mode": "edit"
});
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
cx.update(|cx| {
assert!(
tool.needs_confirmation(&input_with_zed, &project, cx),
"Path with .zed component should require confirmation"
);
});
// Test 2: Absolute path should require confirmation
let input_absolute = json!({
"display_description": "Edit file",
"path": "/etc/hosts",
"mode": "edit"
});
cx.update(|cx| {
assert!(
tool.needs_confirmation(&input_absolute, &project, cx),
"Absolute path should require confirmation"
);
});
// Test 3: Relative path without .zed should not require confirmation
let input_relative = json!({
"display_description": "Edit file",
"path": "root/src/main.rs",
"mode": "edit"
});
cx.update(|cx| {
assert!(
!tool.needs_confirmation(&input_relative, &project, cx),
"Relative path without .zed should not require confirmation"
);
});
// Test 4: Path with .zed in the middle should require confirmation
let input_zed_middle = json!({
"display_description": "Edit settings",
"path": "root/.zed/tasks.json",
"mode": "edit"
});
cx.update(|cx| {
assert!(
tool.needs_confirmation(&input_zed_middle, &project, cx),
"Path with .zed in any component should require confirmation"
);
});
// Test 5: When always_allow_tool_actions is enabled, no confirmation needed
cx.update(|cx| {
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
settings.always_allow_tool_actions = true;
agent_settings::AgentSettings::override_global(settings, cx);
assert!(
!tool.needs_confirmation(&input_with_zed, &project, cx),
"When always_allow_tool_actions is true, no confirmation should be needed"
);
assert!(
!tool.needs_confirmation(&input_absolute, &project, cx),
"When always_allow_tool_actions is true, no confirmation should be needed for absolute paths"
);
});
}
#[gpui::test]
async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) {
// Set up a custom config directory for testing
let temp_dir = tempfile::tempdir().unwrap();
init_test_with_config(cx, temp_dir.path());
let tool = Arc::new(EditFileTool);
// Test ui_text shows context for various paths
let test_cases = vec![
(
json!({
"display_description": "Update config",
"path": ".zed/settings.json",
"mode": "edit"
}),
"Update config (local settings)",
".zed path should show local settings context",
),
(
json!({
"display_description": "Fix bug",
"path": "src/.zed/local.json",
"mode": "edit"
}),
"Fix bug (local settings)",
"Nested .zed path should show local settings context",
),
(
json!({
"display_description": "Update readme",
"path": "README.md",
"mode": "edit"
}),
"Update readme",
"Normal path should not show additional context",
),
(
json!({
"display_description": "Edit config",
"path": "config.zed",
"mode": "edit"
}),
"Edit config",
".zed as extension should not show context",
),
];
for (input, expected_text, description) in test_cases {
cx.update(|_cx| {
let ui_text = tool.ui_text(&input);
assert_eq!(ui_text, expected_text, "Failed for case: {}", description);
});
}
}
#[gpui::test]
async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) {
init_test(cx);
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
// Create a project in /project directory
fs.insert_tree("/project", json!({})).await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
// Test file outside project requires confirmation
let input_outside = json!({
"display_description": "Edit file",
"path": "/outside/file.txt",
"mode": "edit"
});
cx.update(|cx| {
assert!(
tool.needs_confirmation(&input_outside, &project, cx),
"File outside project should require confirmation"
);
});
// Test file inside project doesn't require confirmation
let input_inside = json!({
"display_description": "Edit file",
"path": "project/file.txt",
"mode": "edit"
});
cx.update(|cx| {
assert!(
!tool.needs_confirmation(&input_inside, &project, cx),
"File inside project should not require confirmation"
);
});
}
#[gpui::test]
async fn test_needs_confirmation_config_paths(cx: &mut TestAppContext) {
// Set up a custom data directory for testing
let temp_dir = tempfile::tempdir().unwrap();
init_test_with_config(cx, temp_dir.path());
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/home/user/myproject", json!({})).await;
let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await;
// Get the actual local settings folder name
let local_settings_folder = paths::local_settings_folder_relative_path();
// Test various config path patterns
let test_cases = vec![
(
format!("{}/settings.json", local_settings_folder.display()),
true,
"Top-level local settings file".to_string(),
),
(
format!(
"myproject/{}/settings.json",
local_settings_folder.display()
),
true,
"Local settings in project path".to_string(),
),
(
format!("src/{}/config.toml", local_settings_folder.display()),
true,
"Local settings in subdirectory".to_string(),
),
(
".zed.backup/file.txt".to_string(),
true,
".zed.backup is outside project".to_string(),
),
(
"my.zed/file.txt".to_string(),
true,
"my.zed is outside project".to_string(),
),
(
"myproject/src/file.zed".to_string(),
false,
".zed as file extension".to_string(),
),
(
"myproject/normal/path/file.rs".to_string(),
false,
"Normal file without config paths".to_string(),
),
];
for (path, should_confirm, description) in test_cases {
let input = json!({
"display_description": "Edit file",
"path": path,
"mode": "edit"
});
cx.update(|cx| {
assert_eq!(
tool.needs_confirmation(&input, &project, cx),
should_confirm,
"Failed for case: {} - path: {}",
description,
path
);
});
}
}
#[gpui::test]
async fn test_needs_confirmation_global_config(cx: &mut TestAppContext) {
// Set up a custom data directory for testing
let temp_dir = tempfile::tempdir().unwrap();
init_test_with_config(cx, temp_dir.path());
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
// Create test files in the global config directory
let global_config_dir = paths::config_dir();
fs::create_dir_all(&global_config_dir).unwrap();
let global_settings_path = global_config_dir.join("settings.json");
fs::write(&global_settings_path, "{}").unwrap();
fs.insert_tree("/project", json!({})).await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
// Test global config paths
let test_cases = vec![
(
global_settings_path.to_str().unwrap().to_string(),
true,
"Global settings file should require confirmation",
),
(
global_config_dir
.join("keymap.json")
.to_str()
.unwrap()
.to_string(),
true,
"Global keymap file should require confirmation",
),
(
"project/normal_file.rs".to_string(),
false,
"Normal project file should not require confirmation",
),
];
for (path, should_confirm, description) in test_cases {
let input = json!({
"display_description": "Edit file",
"path": path,
"mode": "edit"
});
cx.update(|cx| {
assert_eq!(
tool.needs_confirmation(&input, &project, cx),
should_confirm,
"Failed for case: {}",
description
);
});
}
}
#[gpui::test]
async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
init_test(cx);
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
// Create multiple worktree directories
fs.insert_tree(
"/workspace/frontend",
json!({
"src": {
"main.js": "console.log('frontend');"
}
}),
)
.await;
fs.insert_tree(
"/workspace/backend",
json!({
"src": {
"main.rs": "fn main() {}"
}
}),
)
.await;
fs.insert_tree(
"/workspace/shared",
json!({
".zed": {
"settings.json": "{}"
}
}),
)
.await;
// Create project with multiple worktrees
let project = Project::test(
fs.clone(),
[
path!("/workspace/frontend").as_ref(),
path!("/workspace/backend").as_ref(),
path!("/workspace/shared").as_ref(),
],
cx,
)
.await;
// Test files in different worktrees
let test_cases = vec![
("frontend/src/main.js", false, "File in first worktree"),
("backend/src/main.rs", false, "File in second worktree"),
(
"shared/.zed/settings.json",
true,
".zed file in third worktree",
),
("/etc/hosts", true, "Absolute path outside all worktrees"),
(
"../outside/file.txt",
true,
"Relative path outside worktrees",
),
];
for (path, should_confirm, description) in test_cases {
let input = json!({
"display_description": "Edit file",
"path": path,
"mode": "edit"
});
cx.update(|cx| {
assert_eq!(
tool.needs_confirmation(&input, &project, cx),
should_confirm,
"Failed for case: {} - path: {}",
description,
path
);
});
}
}
#[gpui::test]
async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
init_test(cx);
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/project",
json!({
".zed": {
"settings.json": "{}"
},
"src": {
".zed": {
"local.json": "{}"
}
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
// Test edge cases
let test_cases = vec![
// Empty path - find_project_path returns Some for empty paths
("", false, "Empty path is treated as project root"),
// Root directory
("/", true, "Root directory should be outside project"),
// Parent directory references - find_project_path resolves these
(
"project/../other",
false,
"Path with .. is resolved by find_project_path",
),
(
"project/./src/file.rs",
false,
"Path with . should work normally",
),
// Windows-style paths (if on Windows)
#[cfg(target_os = "windows")]
("C:\\Windows\\System32\\hosts", true, "Windows system path"),
#[cfg(target_os = "windows")]
("project\\src\\main.rs", false, "Windows-style project path"),
];
for (path, should_confirm, description) in test_cases {
let input = json!({
"display_description": "Edit file",
"path": path,
"mode": "edit"
});
cx.update(|cx| {
assert_eq!(
tool.needs_confirmation(&input, &project, cx),
should_confirm,
"Failed for case: {} - path: {}",
description,
path
);
});
}
}
#[gpui::test]
async fn test_ui_text_with_all_path_types(cx: &mut TestAppContext) {
init_test(cx);
let tool = Arc::new(EditFileTool);
// Test UI text for various scenarios
let test_cases = vec![
(
json!({
"display_description": "Update config",
"path": ".zed/settings.json",
"mode": "edit"
}),
"Update config (local settings)",
".zed path should show local settings context",
),
(
json!({
"display_description": "Fix bug",
"path": "src/.zed/local.json",
"mode": "edit"
}),
"Fix bug (local settings)",
"Nested .zed path should show local settings context",
),
(
json!({
"display_description": "Update readme",
"path": "README.md",
"mode": "edit"
}),
"Update readme",
"Normal path should not show additional context",
),
(
json!({
"display_description": "Edit config",
"path": "config.zed",
"mode": "edit"
}),
"Edit config",
".zed as extension should not show context",
),
];
for (input, expected_text, description) in test_cases {
cx.update(|_cx| {
let ui_text = tool.ui_text(&input);
assert_eq!(ui_text, expected_text, "Failed for case: {}", description);
});
}
}
#[gpui::test]
async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
init_test(cx);
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/project",
json!({
"existing.txt": "content",
".zed": {
"settings.json": "{}"
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
// Test different EditFileMode values
let modes = vec![
EditFileMode::Edit,
EditFileMode::Create,
EditFileMode::Overwrite,
];
for mode in modes {
// Test .zed path with different modes
let input_zed = json!({
"display_description": "Edit settings",
"path": "project/.zed/settings.json",
"mode": mode
});
cx.update(|cx| {
assert!(
tool.needs_confirmation(&input_zed, &project, cx),
".zed path should require confirmation regardless of mode: {:?}",
mode
);
});
// Test outside path with different modes
let input_outside = json!({
"display_description": "Edit file",
"path": "/outside/file.txt",
"mode": mode
});
cx.update(|cx| {
assert!(
tool.needs_confirmation(&input_outside, &project, cx),
"Outside path should require confirmation regardless of mode: {:?}",
mode
);
});
// Test normal path with different modes
let input_normal = json!({
"display_description": "Edit file",
"path": "project/normal.txt",
"mode": mode
});
cx.update(|cx| {
assert!(
!tool.needs_confirmation(&input_normal, &project, cx),
"Normal path should not require confirmation regardless of mode: {:?}",
mode
);
});
}
}
#[gpui::test]
async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) {
// Set up with custom directories for deterministic testing
let temp_dir = tempfile::tempdir().unwrap();
init_test_with_config(cx, temp_dir.path());
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/project", json!({})).await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
// Enable always_allow_tool_actions
cx.update(|cx| {
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
settings.always_allow_tool_actions = true;
agent_settings::AgentSettings::override_global(settings, cx);
});
// Test that all paths that normally require confirmation are bypassed
let global_settings_path = paths::config_dir().join("settings.json");
fs::create_dir_all(paths::config_dir()).unwrap();
fs::write(&global_settings_path, "{}").unwrap();
let test_cases = vec![
".zed/settings.json",
"project/.zed/config.toml",
global_settings_path.to_str().unwrap(),
"/etc/hosts",
"/absolute/path/file.txt",
"../outside/project.txt",
];
for path in test_cases {
let input = json!({
"display_description": "Edit file",
"path": path,
"mode": "edit"
});
cx.update(|cx| {
assert!(
!tool.needs_confirmation(&input, &project, cx),
"Path {} should not require confirmation when always_allow_tool_actions is true",
path
);
});
}
// Disable always_allow_tool_actions and verify confirmation is required again
cx.update(|cx| {
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
settings.always_allow_tool_actions = false;
agent_settings::AgentSettings::override_global(settings, cx);
});
// Verify .zed path requires confirmation again
let input = json!({
"display_description": "Edit file",
"path": ".zed/settings.json",
"mode": "edit"
});
cx.update(|cx| {
assert!(
tool.needs_confirmation(&input, &project, cx),
".zed path should require confirmation when always_allow_tool_actions is false"
);
});
}
} }

View file

@ -116,7 +116,7 @@ impl Tool for FetchTool {
"fetch".to_string() "fetch".to_string()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false false
} }

View file

@ -55,7 +55,7 @@ impl Tool for FindPathTool {
"find_path".into() "find_path".into()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false false
} }

View file

@ -57,7 +57,7 @@ impl Tool for GrepTool {
"grep".into() "grep".into()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false false
} }

View file

@ -45,7 +45,7 @@ impl Tool for ListDirectoryTool {
"list_directory".into() "list_directory".into()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false false
} }

View file

@ -42,7 +42,7 @@ impl Tool for MovePathTool {
"move_path".into() "move_path".into()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false false
} }

View file

@ -33,7 +33,7 @@ impl Tool for NowTool {
"now".into() "now".into()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false false
} }

View file

@ -23,7 +23,7 @@ impl Tool for OpenTool {
"open".to_string() "open".to_string()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
true true
} }
fn may_perform_edits(&self) -> bool { fn may_perform_edits(&self) -> bool {

View file

@ -19,7 +19,7 @@ impl Tool for ProjectNotificationsTool {
"project_notifications".to_string() "project_notifications".to_string()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false false
} }
fn may_perform_edits(&self) -> bool { fn may_perform_edits(&self) -> bool {

View file

@ -54,7 +54,7 @@ impl Tool for ReadFileTool {
"read_file".into() "read_file".into()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false false
} }

View file

@ -77,7 +77,7 @@ impl Tool for TerminalTool {
Self::NAME.to_string() Self::NAME.to_string()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
true true
} }

View file

@ -24,7 +24,7 @@ impl Tool for ThinkingTool {
"thinking".to_string() "thinking".to_string()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false false
} }

View file

@ -32,7 +32,7 @@ impl Tool for WebSearchTool {
"web_search".into() "web_search".into()
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false false
} }

View file

@ -21,7 +21,7 @@ use futures::{
channel::oneshot, future::BoxFuture, channel::oneshot, future::BoxFuture,
}; };
use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions}; use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use http_client::{AsyncBody, HttpClient, HttpClientWithUrl, http};
use parking_lot::RwLock; use parking_lot::RwLock;
use postage::watch; use postage::watch;
use proxy::connect_proxy_stream; use proxy::connect_proxy_stream;
@ -31,7 +31,6 @@ use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources}; use settings::{Settings, SettingsSources};
use std::pin::Pin;
use std::{ use std::{
any::TypeId, any::TypeId,
convert::TryFrom, convert::TryFrom,
@ -45,6 +44,7 @@ use std::{
}, },
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use std::{cmp, pin::Pin};
use telemetry::Telemetry; use telemetry::Telemetry;
use thiserror::Error; use thiserror::Error;
use tokio::net::TcpStream; use tokio::net::TcpStream;
@ -78,7 +78,7 @@ pub static ZED_ALWAYS_ACTIVE: LazyLock<bool> =
LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty())); LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty()));
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500); pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500);
pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(10); pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(30);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20); pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20);
actions!( actions!(
@ -727,11 +727,10 @@ impl Client {
}, },
&cx, &cx,
); );
cx.background_executor().timer(delay).await; let jitter =
delay = delay Duration::from_millis(rng.gen_range(0..delay.as_millis() as u64));
.mul_f32(rng.gen_range(0.5..=2.5)) cx.background_executor().timer(delay + jitter).await;
.max(INITIAL_RECONNECTION_DELAY) delay = cmp::min(delay * 2, MAX_RECONNECTION_DELAY);
.min(MAX_RECONNECTION_DELAY);
} else { } else {
break; break;
} }
@ -1158,6 +1157,7 @@ impl Client {
let http = self.http.clone(); let http = self.http.clone();
let proxy = http.proxy().cloned(); let proxy = http.proxy().cloned();
let user_agent = http.user_agent().cloned();
let credentials = credentials.clone(); let credentials = credentials.clone();
let rpc_url = self.rpc_url(http, release_channel); let rpc_url = self.rpc_url(http, release_channel);
let system_id = self.telemetry.system_id(); let system_id = self.telemetry.system_id();
@ -1209,7 +1209,7 @@ impl Client {
// We then modify the request to add our desired headers. // We then modify the request to add our desired headers.
let request_headers = request.headers_mut(); let request_headers = request.headers_mut();
request_headers.insert( request_headers.insert(
"Authorization", http::header::AUTHORIZATION,
HeaderValue::from_str(&credentials.authorization_header())?, HeaderValue::from_str(&credentials.authorization_header())?,
); );
request_headers.insert( request_headers.insert(
@ -1221,6 +1221,9 @@ impl Client {
"x-zed-release-channel", "x-zed-release-channel",
HeaderValue::from_str(release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?, HeaderValue::from_str(release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?,
); );
if let Some(user_agent) = user_agent {
request_headers.insert(http::header::USER_AGENT, user_agent);
}
if let Some(system_id) = system_id { if let Some(system_id) = system_id {
request_headers.insert("x-zed-system-id", HeaderValue::from_str(&system_id)?); request_headers.insert("x-zed-system-id", HeaderValue::from_str(&system_id)?);
} }

View file

@ -765,12 +765,14 @@ impl UserStore {
pub fn current_plan(&self) -> Option<proto::Plan> { pub fn current_plan(&self) -> Option<proto::Plan> {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
if let Ok(plan) = std::env::var("ZED_SIMULATE_ZED_PRO_PLAN").as_ref() { if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {
return match plan.as_str() { return match plan.as_str() {
"free" => Some(proto::Plan::Free), "free" => Some(proto::Plan::Free),
"trial" => Some(proto::Plan::ZedProTrial), "trial" => Some(proto::Plan::ZedProTrial),
"pro" => Some(proto::Plan::ZedPro), "pro" => Some(proto::Plan::ZedPro),
_ => None, _ => {
panic!("ZED_SIMULATE_PLAN must be one of 'free', 'trial', or 'pro'");
}
}; };
} }

View file

@ -842,7 +842,7 @@ async fn test_client_disconnecting_from_room(
// Allow user A to reconnect to the server. // Allow user A to reconnect to the server.
server.allow_connections(); server.allow_connections();
executor.advance_clock(RECEIVE_TIMEOUT); executor.advance_clock(RECONNECT_TIMEOUT);
// Call user B again from client A. // Call user B again from client A.
active_call_a active_call_a
@ -1358,7 +1358,7 @@ async fn test_calls_on_multiple_connections(
// User A reconnects automatically, then calls user B again. // User A reconnects automatically, then calls user B again.
server.allow_connections(); server.allow_connections();
executor.advance_clock(RECEIVE_TIMEOUT); executor.advance_clock(RECONNECT_TIMEOUT);
active_call_a active_call_a
.update(cx_a, |call, cx| { .update(cx_a, |call, cx| {
call.invite(client_b1.user_id().unwrap(), None, cx) call.invite(client_b1.user_id().unwrap(), None, cx)

View file

@ -144,6 +144,7 @@ impl Client {
pub fn stdio( pub fn stdio(
server_id: ContextServerId, server_id: ContextServerId,
binary: ModelContextServerBinary, binary: ModelContextServerBinary,
working_directory: &Option<PathBuf>,
cx: AsyncApp, cx: AsyncApp,
) -> Result<Self> { ) -> Result<Self> {
log::info!( log::info!(
@ -158,7 +159,7 @@ impl Client {
.map(|name| name.to_string_lossy().to_string()) .map(|name| name.to_string_lossy().to_string())
.unwrap_or_else(String::new); .unwrap_or_else(String::new);
let transport = Arc::new(StdioTransport::new(binary, &cx)?); let transport = Arc::new(StdioTransport::new(binary, working_directory, &cx)?);
Self::new(server_id, server_name.into(), transport, cx) Self::new(server_id, server_name.into(), transport, cx)
} }

View file

@ -53,7 +53,7 @@ impl std::fmt::Debug for ContextServerCommand {
} }
enum ContextServerTransport { enum ContextServerTransport {
Stdio(ContextServerCommand), Stdio(ContextServerCommand, Option<PathBuf>),
Custom(Arc<dyn crate::transport::Transport>), Custom(Arc<dyn crate::transport::Transport>),
} }
@ -64,11 +64,18 @@ pub struct ContextServer {
} }
impl ContextServer { impl ContextServer {
pub fn stdio(id: ContextServerId, command: ContextServerCommand) -> Self { pub fn stdio(
id: ContextServerId,
command: ContextServerCommand,
working_directory: Option<Arc<Path>>,
) -> Self {
Self { Self {
id, id,
client: RwLock::new(None), client: RwLock::new(None),
configuration: ContextServerTransport::Stdio(command), configuration: ContextServerTransport::Stdio(
command,
working_directory.map(|directory| directory.to_path_buf()),
),
} }
} }
@ -90,13 +97,14 @@ impl ContextServer {
pub async fn start(self: Arc<Self>, cx: &AsyncApp) -> Result<()> { pub async fn start(self: Arc<Self>, cx: &AsyncApp) -> Result<()> {
let client = match &self.configuration { let client = match &self.configuration {
ContextServerTransport::Stdio(command) => Client::stdio( ContextServerTransport::Stdio(command, working_directory) => Client::stdio(
client::ContextServerId(self.id.0.clone()), client::ContextServerId(self.id.0.clone()),
client::ModelContextServerBinary { client::ModelContextServerBinary {
executable: Path::new(&command.path).to_path_buf(), executable: Path::new(&command.path).to_path_buf(),
args: command.args.clone(), args: command.args.clone(),
env: command.env.clone(), env: command.env.clone(),
}, },
working_directory,
cx.clone(), cx.clone(),
)?, )?,
ContextServerTransport::Custom(transport) => Client::new( ContextServerTransport::Custom(transport) => Client::new(

View file

@ -1,3 +1,4 @@
use std::path::PathBuf;
use std::pin::Pin; use std::pin::Pin;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
@ -22,7 +23,11 @@ pub struct StdioTransport {
} }
impl StdioTransport { impl StdioTransport {
pub fn new(binary: ModelContextServerBinary, cx: &AsyncApp) -> Result<Self> { pub fn new(
binary: ModelContextServerBinary,
working_directory: &Option<PathBuf>,
cx: &AsyncApp,
) -> Result<Self> {
let mut command = util::command::new_smol_command(&binary.executable); let mut command = util::command::new_smol_command(&binary.executable);
command command
.args(&binary.args) .args(&binary.args)
@ -32,6 +37,10 @@ impl StdioTransport {
.stderr(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped())
.kill_on_drop(true); .kill_on_drop(true);
if let Some(working_directory) = working_directory {
command.current_dir(working_directory);
}
let mut server = command.spawn().with_context(|| { let mut server = command.spawn().with_context(|| {
format!( format!(
"failed to spawn command. (path={:?}, args={:?})", "failed to spawn command. (path={:?}, args={:?})",

View file

@ -295,7 +295,7 @@ mod tests {
request: dap_types::StartDebuggingRequestArgumentsRequest::Launch, request: dap_types::StartDebuggingRequestArgumentsRequest::Launch,
}, },
}, },
Box::new(|_| panic!("Did not expect to hit this code path")), Box::new(|_| {}),
&mut cx.to_async(), &mut cx.to_async(),
) )
.await .await

View file

@ -883,6 +883,7 @@ impl FakeTransport {
break Err(anyhow!("exit in response to request")); break Err(anyhow!("exit in response to request"));
} }
}; };
let success = response.success;
let message = let message =
serde_json::to_string(&Message::Response(response)).unwrap(); serde_json::to_string(&Message::Response(response)).unwrap();
@ -893,6 +894,25 @@ impl FakeTransport {
) )
.await .await
.unwrap(); .unwrap();
if request.command == dap_types::requests::Initialize::COMMAND
&& success
{
let message = serde_json::to_string(&Message::Event(Box::new(
dap_types::messages::Events::Initialized(Some(
Default::default(),
)),
)))
.unwrap();
writer
.write_all(
TransportDelegate::build_rpc_message(message)
.as_bytes(),
)
.await
.unwrap();
}
writer.flush().await.unwrap(); writer.flush().await.unwrap();
} }
} }

View file

@ -22,6 +22,7 @@ test-support = [
"theme/test-support", "theme/test-support",
"util/test-support", "util/test-support",
"workspace/test-support", "workspace/test-support",
"tree-sitter-c",
"tree-sitter-rust", "tree-sitter-rust",
"tree-sitter-typescript", "tree-sitter-typescript",
"tree-sitter-html", "tree-sitter-html",
@ -76,6 +77,7 @@ telemetry.workspace = true
text.workspace = true text.workspace = true
time.workspace = true time.workspace = true
theme.workspace = true theme.workspace = true
tree-sitter-c = { workspace = true, optional = true }
tree-sitter-html = { workspace = true, optional = true } tree-sitter-html = { workspace = true, optional = true }
tree-sitter-rust = { workspace = true, optional = true } tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true }
@ -106,6 +108,7 @@ settings = { workspace = true, features = ["test-support"] }
tempfile.workspace = true tempfile.workspace = true
text = { workspace = true, features = ["test-support"] } text = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] } theme = { workspace = true, features = ["test-support"] }
tree-sitter-c.workspace = true
tree-sitter-html.workspace = true tree-sitter-html.workspace = true
tree-sitter-rust.workspace = true tree-sitter-rust.workspace = true
tree-sitter-typescript.workspace = true tree-sitter-typescript.workspace = true

View file

@ -56,7 +56,7 @@ use aho_corasick::AhoCorasick;
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use blink_manager::BlinkManager; use blink_manager::BlinkManager;
use buffer_diff::DiffHunkStatus; use buffer_diff::DiffHunkStatus;
use client::{Collaborator, ParticipantIndex}; use client::{Collaborator, DisableAiSettings, ParticipantIndex};
use clock::{AGENT_REPLICA_ID, ReplicaId}; use clock::{AGENT_REPLICA_ID, ReplicaId};
use collections::{BTreeMap, HashMap, HashSet, VecDeque}; use collections::{BTreeMap, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing}; use convert_case::{Case, Casing};
@ -1305,6 +1305,7 @@ impl Default for SelectionHistoryMode {
/// ///
/// Similarly, you might want to disable scrolling if you don't want the viewport to /// Similarly, you might want to disable scrolling if you don't want the viewport to
/// move. /// move.
#[derive(Clone)]
pub struct SelectionEffects { pub struct SelectionEffects {
nav_history: Option<bool>, nav_history: Option<bool>,
completions: bool, completions: bool,
@ -1774,7 +1775,7 @@ impl Editor {
) -> Self { ) -> Self {
debug_assert!( debug_assert!(
display_map.is_none() || mode.is_minimap(), display_map.is_none() || mode.is_minimap(),
"Providing a display map for a new editor is only intended for the minimap and might have unindended side effects otherwise!" "Providing a display map for a new editor is only intended for the minimap and might have unintended side effects otherwise!"
); );
let full_mode = mode.is_full(); let full_mode = mode.is_full();
@ -2944,10 +2945,12 @@ impl Editor {
} }
} }
let selection_anchors = self.selections.disjoint_anchors();
if self.focus_handle.is_focused(window) && self.leader_id.is_none() { if self.focus_handle.is_focused(window) && self.leader_id.is_none() {
self.buffer.update(cx, |buffer, cx| { self.buffer.update(cx, |buffer, cx| {
buffer.set_active_selections( buffer.set_active_selections(
&self.selections.disjoint_anchors(), &selection_anchors,
self.selections.line_mode, self.selections.line_mode,
self.cursor_shape, self.cursor_shape,
cx, cx,
@ -2964,9 +2967,8 @@ impl Editor {
self.select_next_state = None; self.select_next_state = None;
self.select_prev_state = None; self.select_prev_state = None;
self.select_syntax_node_history.try_clear(); self.select_syntax_node_history.try_clear();
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); self.invalidate_autoclose_regions(&selection_anchors, buffer);
self.snippet_stack self.snippet_stack.invalidate(&selection_anchors, buffer);
.invalidate(&self.selections.disjoint_anchors(), buffer);
self.take_rename(false, window, cx); self.take_rename(false, window, cx);
let newest_selection = self.selections.newest_anchor(); let newest_selection = self.selections.newest_anchor();
@ -4047,7 +4049,8 @@ impl Editor {
// then don't insert that closing bracket again; just move the selection // then don't insert that closing bracket again; just move the selection
// past the closing bracket. // past the closing bracket.
let should_skip = selection.end == region.range.end.to_point(&snapshot) let should_skip = selection.end == region.range.end.to_point(&snapshot)
&& text.as_ref() == region.pair.end.as_str(); && text.as_ref() == region.pair.end.as_str()
&& snapshot.contains_str_at(region.range.end, text.as_ref());
if should_skip { if should_skip {
let anchor = snapshot.anchor_after(selection.end); let anchor = snapshot.anchor_after(selection.end);
new_selections new_selections
@ -4973,13 +4976,17 @@ impl Editor {
}) })
} }
/// Remove any autoclose regions that no longer contain their selection. /// Remove any autoclose regions that no longer contain their selection or have invalid anchors in ranges.
fn invalidate_autoclose_regions( fn invalidate_autoclose_regions(
&mut self, &mut self,
mut selections: &[Selection<Anchor>], mut selections: &[Selection<Anchor>],
buffer: &MultiBufferSnapshot, buffer: &MultiBufferSnapshot,
) { ) {
self.autoclose_regions.retain(|state| { self.autoclose_regions.retain(|state| {
if !state.range.start.is_valid(buffer) || !state.range.end.is_valid(buffer) {
return false;
}
let mut i = 0; let mut i = 0;
while let Some(selection) = selections.get(i) { while let Some(selection) = selections.get(i) {
if selection.end.cmp(&state.range.start, buffer).is_lt() { if selection.end.cmp(&state.range.start, buffer).is_lt() {
@ -5891,18 +5898,20 @@ impl Editor {
text: new_text[common_prefix_len..].into(), text: new_text[common_prefix_len..].into(),
}); });
self.transact(window, cx, |this, window, cx| { self.transact(window, cx, |editor, window, cx| {
if let Some(mut snippet) = snippet { if let Some(mut snippet) = snippet {
snippet.text = new_text.to_string(); snippet.text = new_text.to_string();
this.insert_snippet(&ranges, snippet, window, cx).log_err(); editor
.insert_snippet(&ranges, snippet, window, cx)
.log_err();
} else { } else {
this.buffer.update(cx, |buffer, cx| { editor.buffer.update(cx, |multi_buffer, cx| {
let auto_indent = match completion.insert_text_mode { let auto_indent = match completion.insert_text_mode {
Some(InsertTextMode::AS_IS) => None, Some(InsertTextMode::AS_IS) => None,
_ => this.autoindent_mode.clone(), _ => editor.autoindent_mode.clone(),
}; };
let edits = ranges.into_iter().map(|range| (range, new_text.as_str())); let edits = ranges.into_iter().map(|range| (range, new_text.as_str()));
buffer.edit(edits, auto_indent, cx); multi_buffer.edit(edits, auto_indent, cx);
}); });
} }
for (buffer, edits) in linked_edits { for (buffer, edits) in linked_edits {
@ -5921,8 +5930,9 @@ impl Editor {
}) })
} }
this.refresh_inline_completion(true, false, window, cx); editor.refresh_inline_completion(true, false, window, cx);
}); });
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), &snapshot);
let show_new_completions_on_confirm = completion let show_new_completions_on_confirm = completion
.confirm .confirm
@ -7048,7 +7058,7 @@ impl Editor {
} }
pub fn update_edit_prediction_settings(&mut self, cx: &mut Context<Self>) { pub fn update_edit_prediction_settings(&mut self, cx: &mut Context<Self>) {
if self.edit_prediction_provider.is_none() { if self.edit_prediction_provider.is_none() || DisableAiSettings::get_global(cx).disable_ai {
self.edit_prediction_settings = EditPredictionSettings::Disabled; self.edit_prediction_settings = EditPredictionSettings::Disabled;
} else { } else {
let selection = self.selections.newest_anchor(); let selection = self.selections.newest_anchor();
@ -8235,8 +8245,7 @@ impl Editor {
return; return;
}; };
// Try to find a closest, enclosing node using tree-sitter that has a // Try to find a closest, enclosing node using tree-sitter that has a task
// task
let Some((buffer, buffer_row, tasks)) = self let Some((buffer, buffer_row, tasks)) = self
.find_enclosing_node_task(cx) .find_enclosing_node_task(cx)
// Or find the task that's closest in row-distance. // Or find the task that's closest in row-distance.
@ -9563,27 +9572,46 @@ impl Editor {
// Check whether the just-entered snippet ends with an auto-closable bracket. // Check whether the just-entered snippet ends with an auto-closable bracket.
if self.autoclose_regions.is_empty() { if self.autoclose_regions.is_empty() {
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
for selection in &mut self.selections.all::<Point>(cx) { let mut all_selections = self.selections.all::<Point>(cx);
for selection in &mut all_selections {
let selection_head = selection.head(); let selection_head = selection.head();
let Some(scope) = snapshot.language_scope_at(selection_head) else { let Some(scope) = snapshot.language_scope_at(selection_head) else {
continue; continue;
}; };
let mut bracket_pair = None; let mut bracket_pair = None;
let next_chars = snapshot.chars_at(selection_head).collect::<String>(); let max_lookup_length = scope
let prev_chars = snapshot .brackets()
.reversed_chars_at(selection_head) .map(|(pair, _)| {
.collect::<String>(); pair.start
for (pair, enabled) in scope.brackets() { .as_str()
if enabled .chars()
&& pair.close .count()
&& prev_chars.starts_with(pair.start.as_str()) .max(pair.end.as_str().chars().count())
&& next_chars.starts_with(pair.end.as_str()) })
{ .max();
bracket_pair = Some(pair.clone()); if let Some(max_lookup_length) = max_lookup_length {
break; let next_text = snapshot
.chars_at(selection_head)
.take(max_lookup_length)
.collect::<String>();
let prev_text = snapshot
.reversed_chars_at(selection_head)
.take(max_lookup_length)
.collect::<String>();
for (pair, enabled) in scope.brackets() {
if enabled
&& pair.close
&& prev_text.starts_with(pair.start.as_str())
&& next_text.starts_with(pair.end.as_str())
{
bracket_pair = Some(pair.clone());
break;
}
} }
} }
if let Some(pair) = bracket_pair { if let Some(pair) = bracket_pair {
let snapshot_settings = snapshot.language_settings_at(selection_head, cx); let snapshot_settings = snapshot.language_settings_at(selection_head, cx);
let autoclose_enabled = let autoclose_enabled =
@ -21812,11 +21840,11 @@ impl CodeActionProvider for Entity<Project> {
cx: &mut App, cx: &mut App,
) -> Task<Result<Vec<CodeAction>>> { ) -> Task<Result<Vec<CodeAction>>> {
self.update(cx, |project, cx| { self.update(cx, |project, cx| {
let code_lens = project.code_lens(buffer, range.clone(), cx); let code_lens_actions = project.code_lens_actions(buffer, range.clone(), cx);
let code_actions = project.code_actions(buffer, range, None, cx); let code_actions = project.code_actions(buffer, range, None, cx);
cx.background_spawn(async move { cx.background_spawn(async move {
let (code_lens, code_actions) = join(code_lens, code_actions).await; let (code_lens_actions, code_actions) = join(code_lens_actions, code_actions).await;
Ok(code_lens Ok(code_lens_actions
.context("code lens fetch")? .context("code lens fetch")?
.into_iter() .into_iter()
.chain(code_actions.context("code action fetch")?) .chain(code_actions.context("code action fetch")?)

View file

@ -10028,8 +10028,14 @@ async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
); );
} }
#[gpui::test] async fn setup_range_format_test(
async fn test_range_format_during_save(cx: &mut TestAppContext) { cx: &mut TestAppContext,
) -> (
Entity<Project>,
Entity<Editor>,
&mut gpui::VisualTestContext,
lsp::FakeLanguageServer,
) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
@ -10044,9 +10050,9 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
FakeLspAdapter { FakeLspAdapter {
capabilities: lsp::ServerCapabilities { capabilities: lsp::ServerCapabilities {
document_range_formatting_provider: Some(lsp::OneOf::Left(true)), document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
..Default::default() ..lsp::ServerCapabilities::default()
}, },
..Default::default() ..FakeLspAdapter::default()
}, },
); );
@ -10061,14 +10067,22 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
let (editor, cx) = cx.add_window_view(|window, cx| { let (editor, cx) = cx.add_window_view(|window, cx| {
build_editor_with_project(project.clone(), buffer, window, cx) build_editor_with_project(project.clone(), buffer, window, cx)
}); });
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
(project, editor, cx, fake_server)
}
#[gpui::test]
async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
editor.update_in(cx, |editor, window, cx| { editor.update_in(cx, |editor, window, cx| {
editor.set_text("one\ntwo\nthree\n", window, cx) editor.set_text("one\ntwo\nthree\n", window, cx)
}); });
assert!(cx.read(|cx| editor.is_dirty(cx))); assert!(cx.read(|cx| editor.is_dirty(cx)));
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let save = editor let save = editor
.update_in(cx, |editor, window, cx| { .update_in(cx, |editor, window, cx| {
editor.save( editor.save(
@ -10103,13 +10117,18 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
"one, two\nthree\n" "one, two\nthree\n"
); );
assert!(!cx.read(|cx| editor.is_dirty(cx))); assert!(!cx.read(|cx| editor.is_dirty(cx)));
}
#[gpui::test]
async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
editor.update_in(cx, |editor, window, cx| { editor.update_in(cx, |editor, window, cx| {
editor.set_text("one\ntwo\nthree\n", window, cx) editor.set_text("one\ntwo\nthree\n", window, cx)
}); });
assert!(cx.read(|cx| editor.is_dirty(cx))); assert!(cx.read(|cx| editor.is_dirty(cx)));
// Ensure we can still save even if formatting hangs. // Test that save still works when formatting hangs
fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>( fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
move |params, _| async move { move |params, _| async move {
assert_eq!( assert_eq!(
@ -10141,8 +10160,13 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
"one\ntwo\nthree\n" "one\ntwo\nthree\n"
); );
assert!(!cx.read(|cx| editor.is_dirty(cx))); assert!(!cx.read(|cx| editor.is_dirty(cx)));
}
// For non-dirty buffer, no formatting request should be sent #[gpui::test]
async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
// Buffer starts clean, no formatting should be requested
let save = editor let save = editor
.update_in(cx, |editor, window, cx| { .update_in(cx, |editor, window, cx| {
editor.save( editor.save(
@ -10163,6 +10187,12 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
.next(); .next();
cx.executor().start_waiting(); cx.executor().start_waiting();
save.await; save.await;
cx.run_until_parked();
}
#[gpui::test]
async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
// Set Rust language override and assert overridden tabsize is sent to language server // Set Rust language override and assert overridden tabsize is sent to language server
update_test_language_settings(cx, |settings| { update_test_language_settings(cx, |settings| {
@ -10176,7 +10206,7 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
}); });
editor.update_in(cx, |editor, window, cx| { editor.update_in(cx, |editor, window, cx| {
editor.set_text("somehting_new\n", window, cx) editor.set_text("something_new\n", window, cx)
}); });
assert!(cx.read(|cx| editor.is_dirty(cx))); assert!(cx.read(|cx| editor.is_dirty(cx)));
let save = editor let save = editor
@ -13326,6 +13356,178 @@ async fn test_as_is_completions(cx: &mut TestAppContext) {
cx.assert_editor_state("fn a() {}\n unsafeˇ"); cx.assert_editor_state("fn a() {}\n unsafeˇ");
} }
#[gpui::test]
async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let language =
Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
let mut cx = EditorLspTestContext::new(
language,
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
..lsp::CompletionOptions::default()
}),
..lsp::ServerCapabilities::default()
},
cx,
)
.await;
cx.set_state(
"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
ˇ",
);
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.handle_input("#", window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.handle_input("i", window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.handle_input("n", window, cx);
});
cx.executor().run_until_parked();
cx.assert_editor_state(
"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
#inˇ",
);
cx.lsp
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: false,
item_defaults: None,
items: vec![lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::SNIPPET),
label_details: Some(lsp::CompletionItemLabelDetails {
detail: Some("header".to_string()),
description: None,
}),
label: " include".to_string(),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 8,
character: 1,
},
end: lsp::Position {
line: 8,
character: 1,
},
},
new_text: "include \"$0\"".to_string(),
})),
sort_text: Some("40b67681include".to_string()),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
filter_text: Some("include".to_string()),
insert_text: Some("include \"$0\"".to_string()),
..lsp::CompletionItem::default()
}],
})))
});
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
});
cx.executor().run_until_parked();
cx.assert_editor_state(
"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
#include \"ˇ\"",
);
cx.lsp
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: true,
item_defaults: None,
items: vec![lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FILE),
label: "AGL/".to_string(),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 8,
character: 10,
},
end: lsp::Position {
line: 8,
character: 11,
},
},
new_text: "AGL/".to_string(),
})),
sort_text: Some("40b67681AGL/".to_string()),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
filter_text: Some("AGL/".to_string()),
insert_text: Some("AGL/".to_string()),
..lsp::CompletionItem::default()
}],
})))
});
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
});
cx.executor().run_until_parked();
cx.assert_editor_state(
r##"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
#include "AGL/ˇ"##,
);
cx.update_editor(|editor, window, cx| {
editor.handle_input("\"", window, cx);
});
cx.executor().run_until_parked();
cx.assert_editor_state(
r##"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
#include "AGL/"ˇ"##,
);
}
#[gpui::test] #[gpui::test]
async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) { async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
@ -16837,7 +17039,7 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
} }
#[gpui::test] #[gpui::test]
async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) { async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let cols = 4; let cols = 4;
@ -21266,16 +21468,32 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex
}, },
); );
let (buffer, _handle) = project let editor = workspace
.update(cx, |p, cx| { .update(cx, |workspace, window, cx| {
p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx) workspace.open_abs_path(
PathBuf::from(path!("/dir/a.ts")),
OpenOptions::default(),
window,
cx,
)
}) })
.unwrap()
.await .await
.unwrap()
.downcast::<Editor>()
.unwrap(); .unwrap();
cx.executor().run_until_parked(); cx.executor().run_until_parked();
let fake_server = fake_language_servers.next().await.unwrap(); let fake_server = fake_language_servers.next().await.unwrap();
let buffer = editor.update(cx, |editor, cx| {
editor
.buffer()
.read(cx)
.as_singleton()
.expect("have opened a single file by path")
});
let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left); let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
drop(buffer_snapshot); drop(buffer_snapshot);
@ -21333,7 +21551,7 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex
assert_eq!( assert_eq!(
actions.len(), actions.len(),
1, 1,
"Should have only one valid action for the 0..0 range" "Should have only one valid action for the 0..0 range, got: {actions:#?}"
); );
let action = actions[0].clone(); let action = actions[0].clone();
let apply = project.update(cx, |project, cx| { let apply = project.update(cx, |project, cx| {
@ -21379,7 +21597,7 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex
.into_iter() .into_iter()
.collect(), .collect(),
), ),
..Default::default() ..lsp::WorkspaceEdit::default()
}, },
}, },
) )
@ -21402,6 +21620,38 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex
buffer.undo(cx); buffer.undo(cx);
assert_eq!(buffer.text(), "a"); assert_eq!(buffer.text(), "a");
}); });
let actions_after_edits = cx
.update_window(*workspace, |_, window, cx| {
project.code_actions(&buffer, anchor..anchor, window, cx)
})
.unwrap()
.await
.unwrap();
assert_eq!(
actions, actions_after_edits,
"For the same selection, same code lens actions should be returned"
);
let _responses =
fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
panic!("No more code lens requests are expected");
});
editor.update_in(cx, |editor, window, cx| {
editor.select_all(&SelectAll, window, cx);
});
cx.executor().run_until_parked();
let new_actions = cx
.update_window(*workspace, |_, window, cx| {
project.code_actions(&buffer, anchor..anchor, window, cx)
})
.unwrap()
.await
.unwrap();
assert_eq!(
actions, new_actions,
"Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
);
} }
#[gpui::test] #[gpui::test]

View file

@ -6,7 +6,7 @@ use gpui::{Hsla, Rgba};
use itertools::Itertools; use itertools::Itertools;
use language::point_from_lsp; use language::point_from_lsp;
use multi_buffer::Anchor; use multi_buffer::Anchor;
use project::{DocumentColor, lsp_store::ColorFetchStrategy}; use project::{DocumentColor, lsp_store::LspFetchStrategy};
use settings::Settings as _; use settings::Settings as _;
use text::{Bias, BufferId, OffsetRangeExt as _}; use text::{Bias, BufferId, OffsetRangeExt as _};
use ui::{App, Context, Window}; use ui::{App, Context, Window};
@ -180,9 +180,9 @@ impl Editor {
.filter_map(|buffer| { .filter_map(|buffer| {
let buffer_id = buffer.read(cx).remote_id(); let buffer_id = buffer.read(cx).remote_id();
let fetch_strategy = if ignore_cache { let fetch_strategy = if ignore_cache {
ColorFetchStrategy::IgnoreCache LspFetchStrategy::IgnoreCache
} else { } else {
ColorFetchStrategy::UseCache { LspFetchStrategy::UseCache {
known_cache_version: self.colors.as_ref().and_then(|colors| { known_cache_version: self.colors.as_ref().and_then(|colors| {
Some(colors.buffer_colors.get(&buffer_id)?.cache_version_used) Some(colors.buffer_colors.get(&buffer_id)?.cache_version_used)
}), }),

View file

@ -102,7 +102,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn language_server_initialization_options( async fn language_server_initialization_options(
@ -127,7 +127,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn language_server_workspace_configuration( async fn language_server_workspace_configuration(
@ -150,7 +150,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn language_server_additional_initialization_options( async fn language_server_additional_initialization_options(
@ -175,7 +175,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn language_server_additional_workspace_configuration( async fn language_server_additional_workspace_configuration(
@ -200,7 +200,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn labels_for_completions( async fn labels_for_completions(
@ -226,7 +226,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn labels_for_symbols( async fn labels_for_symbols(
@ -252,7 +252,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn complete_slash_command_argument( async fn complete_slash_command_argument(
@ -271,7 +271,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn run_slash_command( async fn run_slash_command(
@ -297,7 +297,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn context_server_command( async fn context_server_command(
@ -316,7 +316,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn context_server_configuration( async fn context_server_configuration(
@ -343,7 +343,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>> { async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>> {
@ -358,7 +358,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn index_docs( async fn index_docs(
@ -384,7 +384,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn get_dap_binary( async fn get_dap_binary(
@ -406,7 +406,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn dap_request_kind( async fn dap_request_kind(
&self, &self,
@ -423,7 +423,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn dap_config_to_scenario(&self, config: ZedDebugConfig) -> Result<DebugScenario> { async fn dap_config_to_scenario(&self, config: ZedDebugConfig) -> Result<DebugScenario> {
@ -437,7 +437,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn dap_locator_create_scenario( async fn dap_locator_create_scenario(
@ -461,7 +461,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
async fn run_dap_locator( async fn run_dap_locator(
&self, &self,
@ -477,7 +477,7 @@ impl extension::Extension for WasmExtension {
} }
.boxed() .boxed()
}) })
.await .await?
} }
} }
@ -739,7 +739,7 @@ impl WasmExtension {
.with_context(|| format!("failed to load wasm extension {}", manifest.id)) .with_context(|| format!("failed to load wasm extension {}", manifest.id))
} }
pub async fn call<T, Fn>(&self, f: Fn) -> T pub async fn call<T, Fn>(&self, f: Fn) -> Result<T>
where where
T: 'static + Send, T: 'static + Send,
Fn: 'static Fn: 'static
@ -755,8 +755,19 @@ impl WasmExtension {
} }
.boxed() .boxed()
})) }))
.expect("wasm extension channel should not be closed yet"); .map_err(|_| {
return_rx.await.expect("wasm extension channel") anyhow!(
"wasm extension channel should not be closed yet, extension {} (id {})",
self.manifest.name,
self.manifest.id,
)
})?;
return_rx.await.with_context(|| {
format!(
"wasm extension channel, extension {} (id {})",
self.manifest.name, self.manifest.id,
)
})
} }
} }
@ -777,8 +788,19 @@ impl WasmState {
} }
.boxed_local() .boxed_local()
})) }))
.expect("main thread message channel should not be closed yet"); .unwrap_or_else(|_| {
async move { return_rx.await.expect("main thread message channel") } panic!(
"main thread message channel should not be closed yet, extension {} (id {})",
self.manifest.name, self.manifest.id,
)
});
let name = self.manifest.name.clone();
let id = self.manifest.id.clone();
async move {
return_rx.await.unwrap_or_else(|_| {
panic!("main thread message channel, extension {name} (id {id})")
})
}
} }
fn work_dir(&self) -> PathBuf { fn work_dir(&self) -> PathBuf {

View file

@ -295,11 +295,13 @@ impl CommitModal {
IconPosition::Start, IconPosition::Start,
Some(Box::new(Amend)), Some(Box::new(Amend)),
{ {
let git_panel = git_panel_entity.clone(); let git_panel = git_panel_entity.downgrade();
move |window, cx| { move |_, cx| {
git_panel.update(cx, |git_panel, cx| { git_panel
git_panel.toggle_amend_pending(&Amend, window, cx); .update(cx, |git_panel, cx| {
}) git_panel.toggle_amend_pending(cx);
})
.ok();
} }
}, },
) )

View file

@ -3054,6 +3054,7 @@ impl GitPanel {
), ),
) )
.menu({ .menu({
let git_panel = cx.entity();
let has_previous_commit = self.head_commit(cx).is_some(); let has_previous_commit = self.head_commit(cx).is_some();
let amend = self.amend_pending(); let amend = self.amend_pending();
let signoff = self.signoff_enabled; let signoff = self.signoff_enabled;
@ -3070,7 +3071,16 @@ impl GitPanel {
amend, amend,
IconPosition::Start, IconPosition::Start,
Some(Box::new(Amend)), Some(Box::new(Amend)),
move |window, cx| window.dispatch_action(Box::new(Amend), cx), {
let git_panel = git_panel.downgrade();
move |_, cx| {
git_panel
.update(cx, |git_panel, cx| {
git_panel.toggle_amend_pending(cx);
})
.ok();
}
},
) )
}) })
.toggleable_entry( .toggleable_entry(
@ -3441,9 +3451,11 @@ impl GitPanel {
.truncate(), .truncate(),
), ),
) )
.child(panel_button("Cancel").size(ButtonSize::Default).on_click( .child(
cx.listener(|this, _, window, cx| this.toggle_amend_pending(&Amend, window, cx)), panel_button("Cancel")
)) .size(ButtonSize::Default)
.on_click(cx.listener(|this, _, _, cx| this.set_amend_pending(false, cx))),
)
} }
fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> { fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
@ -4204,17 +4216,8 @@ impl GitPanel {
pub fn set_amend_pending(&mut self, value: bool, cx: &mut Context<Self>) { pub fn set_amend_pending(&mut self, value: bool, cx: &mut Context<Self>) {
self.amend_pending = value; self.amend_pending = value;
cx.notify();
}
pub fn toggle_amend_pending(
&mut self,
_: &Amend,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.set_amend_pending(!self.amend_pending, cx);
self.serialize(cx); self.serialize(cx);
cx.notify();
} }
pub fn signoff_enabled(&self) -> bool { pub fn signoff_enabled(&self) -> bool {
@ -4308,6 +4311,13 @@ impl GitPanel {
anchor: path, anchor: path,
}); });
} }
pub(crate) fn toggle_amend_pending(&mut self, cx: &mut Context<Self>) {
self.set_amend_pending(!self.amend_pending, cx);
if self.amend_pending {
self.load_last_commit_message_if_empty(cx);
}
}
} }
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> { fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
@ -4352,7 +4362,6 @@ impl Render for GitPanel {
.on_action(cx.listener(Self::stage_range)) .on_action(cx.listener(Self::stage_range))
.on_action(cx.listener(GitPanel::commit)) .on_action(cx.listener(GitPanel::commit))
.on_action(cx.listener(GitPanel::amend)) .on_action(cx.listener(GitPanel::amend))
.on_action(cx.listener(GitPanel::toggle_amend_pending))
.on_action(cx.listener(GitPanel::toggle_signoff_enabled)) .on_action(cx.listener(GitPanel::toggle_signoff_enabled))
.on_action(cx.listener(Self::stage_all)) .on_action(cx.listener(Self::stage_all))
.on_action(cx.listener(Self::unstage_all)) .on_action(cx.listener(Self::unstage_all))

View file

@ -12,6 +12,7 @@ use language::{self, Buffer, Point};
use project::Project; use project::Project;
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
cmp,
ops::Range, ops::Range,
pin::pin, pin::pin,
sync::Arc, sync::Arc,
@ -45,38 +46,60 @@ impl TextDiffView {
) -> Option<Task<Result<Entity<Self>>>> { ) -> Option<Task<Result<Entity<Self>>>> {
let source_editor = diff_data.editor.clone(); let source_editor = diff_data.editor.clone();
let source_editor_buffer_and_range = source_editor.update(cx, |editor, cx| { let selection_data = source_editor.update(cx, |editor, cx| {
let multibuffer = editor.buffer().read(cx); let multibuffer = editor.buffer().read(cx);
let source_buffer = multibuffer.as_singleton()?.clone(); let source_buffer = multibuffer.as_singleton()?.clone();
let selections = editor.selections.all::<Point>(cx); let selections = editor.selections.all::<Point>(cx);
let buffer_snapshot = source_buffer.read(cx); let buffer_snapshot = source_buffer.read(cx);
let first_selection = selections.first()?; let first_selection = selections.first()?;
let selection_range = if first_selection.is_empty() { let max_point = buffer_snapshot.max_point();
Point::new(0, 0)..buffer_snapshot.max_point()
} else {
first_selection.start..first_selection.end
};
Some((source_buffer, selection_range)) if first_selection.is_empty() {
let full_range = Point::new(0, 0)..max_point;
return Some((source_buffer, full_range));
}
let start = first_selection.start;
let end = first_selection.end;
let expanded_start = Point::new(start.row, 0);
let expanded_end = if end.column > 0 {
let next_row = end.row + 1;
cmp::min(max_point, Point::new(next_row, 0))
} else {
end
};
Some((source_buffer, expanded_start..expanded_end))
}); });
let Some((source_buffer, selected_range)) = source_editor_buffer_and_range else { let Some((source_buffer, expanded_selection_range)) = selection_data else {
log::warn!("There should always be at least one selection in Zed. This is a bug."); log::warn!("There should always be at least one selection in Zed. This is a bug.");
return None; return None;
}; };
let clipboard_text = diff_data.clipboard_text.clone(); source_editor.update(cx, |source_editor, cx| {
source_editor.change_selections(Default::default(), window, cx, |s| {
let workspace = workspace.weak_handle(); s.select_ranges(vec![
expanded_selection_range.start..expanded_selection_range.end,
let diff_buffer = cx.new(|cx| { ]);
let source_buffer_snapshot = source_buffer.read(cx).snapshot(); })
let diff = BufferDiff::new(&source_buffer_snapshot.text, cx);
diff
}); });
let clipboard_buffer = let source_buffer_snapshot = source_buffer.read(cx).snapshot();
build_clipboard_buffer(clipboard_text, &source_buffer, selected_range.clone(), cx); let mut clipboard_text = diff_data.clipboard_text.clone();
if !clipboard_text.ends_with("\n") {
clipboard_text.push_str("\n");
}
let workspace = workspace.weak_handle();
let diff_buffer = cx.new(|cx| BufferDiff::new(&source_buffer_snapshot.text, cx));
let clipboard_buffer = build_clipboard_buffer(
clipboard_text,
&source_buffer,
expanded_selection_range.clone(),
cx,
);
let task = window.spawn(cx, async move |cx| { let task = window.spawn(cx, async move |cx| {
let project = workspace.update(cx, |workspace, _| workspace.project().clone())?; let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
@ -89,7 +112,7 @@ impl TextDiffView {
clipboard_buffer, clipboard_buffer,
source_editor, source_editor,
source_buffer, source_buffer,
selected_range, expanded_selection_range,
diff_buffer, diff_buffer,
project, project,
window, window,
@ -208,9 +231,9 @@ impl TextDiffView {
} }
fn build_clipboard_buffer( fn build_clipboard_buffer(
clipboard_text: String, text: String,
source_buffer: &Entity<Buffer>, source_buffer: &Entity<Buffer>,
selected_range: Range<Point>, replacement_range: Range<Point>,
cx: &mut App, cx: &mut App,
) -> Entity<Buffer> { ) -> Entity<Buffer> {
let source_buffer_snapshot = source_buffer.read(cx).snapshot(); let source_buffer_snapshot = source_buffer.read(cx).snapshot();
@ -219,9 +242,9 @@ fn build_clipboard_buffer(
let language = source_buffer.read(cx).language().cloned(); let language = source_buffer.read(cx).language().cloned();
buffer.set_language(language, cx); buffer.set_language(language, cx);
let range_start = source_buffer_snapshot.point_to_offset(selected_range.start); let range_start = source_buffer_snapshot.point_to_offset(replacement_range.start);
let range_end = source_buffer_snapshot.point_to_offset(selected_range.end); let range_end = source_buffer_snapshot.point_to_offset(replacement_range.end);
buffer.edit([(range_start..range_end, clipboard_text)], None, cx); buffer.edit([(range_start..range_end, text)], None, cx);
buffer buffer
}) })
@ -293,7 +316,7 @@ impl Item for TextDiffView {
} }
fn telemetry_event_text(&self) -> Option<&'static str> { fn telemetry_event_text(&self) -> Option<&'static str> {
Some("Diff View Opened") Some("Selection Diff View Opened")
} }
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@ -395,21 +418,13 @@ pub fn selection_location_text(editor: &Editor, cx: &App) -> Option<String> {
let buffer_snapshot = buffer.snapshot(cx); let buffer_snapshot = buffer.snapshot(cx);
let first_selection = editor.selections.disjoint.first()?; let first_selection = editor.selections.disjoint.first()?;
let (start_row, start_column, end_row, end_column) = let selection_start = first_selection.start.to_point(&buffer_snapshot);
if first_selection.start == first_selection.end { let selection_end = first_selection.end.to_point(&buffer_snapshot);
let max_point = buffer_snapshot.max_point();
(0, 0, max_point.row, max_point.column)
} else {
let selection_start = first_selection.start.to_point(&buffer_snapshot);
let selection_end = first_selection.end.to_point(&buffer_snapshot);
( let start_row = selection_start.row;
selection_start.row, let start_column = selection_start.column;
selection_start.column, let end_row = selection_end.row;
selection_end.row, let end_column = selection_end.column;
selection_end.column,
)
};
let range_text = if start_row == end_row { let range_text = if start_row == end_row {
format!("L{}:{}-{}", start_row + 1, start_column + 1, end_column + 1) format!("L{}:{}-{}", start_row + 1, start_column + 1, end_column + 1)
@ -435,14 +450,13 @@ impl Render for TextDiffView {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use editor::test::editor_test_context::assert_state_with_diff;
use editor::{actions, test::editor_test_context::assert_state_with_diff};
use gpui::{TestAppContext, VisualContext}; use gpui::{TestAppContext, VisualContext};
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use serde_json::json; use serde_json::json;
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use unindent::unindent; use unindent::unindent;
use util::path; use util::{path, test::marked_text_ranges};
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| { cx.update(|cx| {
@ -457,52 +471,236 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
async fn test_diffing_clipboard_against_specific_selection(cx: &mut TestAppContext) { async fn test_diffing_clipboard_against_empty_selection_uses_full_buffer_selection(
base_test(true, cx).await; cx: &mut TestAppContext,
) {
base_test(
path!("/test"),
path!("/test/text.txt"),
"def process_incoming_inventory(items, warehouse_id):\n pass\n",
"def process_outgoing_inventory(items, warehouse_id):\n passˇ\n",
&unindent(
"
- def process_incoming_inventory(items, warehouse_id):
+ ˇdef process_outgoing_inventory(items, warehouse_id):
pass
",
),
"Clipboard ↔ text.txt @ L1:1-L3:1",
&format!("Clipboard ↔ {} @ L1:1-L3:1", path!("test/text.txt")),
cx,
)
.await;
} }
#[gpui::test] #[gpui::test]
async fn test_diffing_clipboard_against_empty_selection_uses_full_buffer( async fn test_diffing_clipboard_against_multiline_selection_expands_to_full_lines(
cx: &mut TestAppContext, cx: &mut TestAppContext,
) { ) {
base_test(false, cx).await; base_test(
path!("/test"),
path!("/test/text.txt"),
"def process_incoming_inventory(items, warehouse_id):\n pass\n",
"«def process_outgoing_inventory(items, warehouse_id):\n passˇ»\n",
&unindent(
"
- def process_incoming_inventory(items, warehouse_id):
+ ˇdef process_outgoing_inventory(items, warehouse_id):
pass
",
),
"Clipboard ↔ text.txt @ L1:1-L3:1",
&format!("Clipboard ↔ {} @ L1:1-L3:1", path!("test/text.txt")),
cx,
)
.await;
} }
async fn base_test(select_all_text: bool, cx: &mut TestAppContext) { #[gpui::test]
async fn test_diffing_clipboard_against_single_line_selection(cx: &mut TestAppContext) {
base_test(
path!("/test"),
path!("/test/text.txt"),
"a",
"«bbˇ»",
&unindent(
"
- a
+ ˇbb",
),
"Clipboard ↔ text.txt @ L1:1-3",
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
cx,
)
.await;
}
#[gpui::test]
async fn test_diffing_clipboard_with_leading_whitespace_against_line(cx: &mut TestAppContext) {
base_test(
path!("/test"),
path!("/test/text.txt"),
" a",
"«bbˇ»",
&unindent(
"
- a
+ ˇbb",
),
"Clipboard ↔ text.txt @ L1:1-3",
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
cx,
)
.await;
}
#[gpui::test]
async fn test_diffing_clipboard_against_line_with_leading_whitespace(cx: &mut TestAppContext) {
base_test(
path!("/test"),
path!("/test/text.txt"),
"a",
" «bbˇ»",
&unindent(
"
- a
+ ˇ bb",
),
"Clipboard ↔ text.txt @ L1:1-7",
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
cx,
)
.await;
}
#[gpui::test]
async fn test_diffing_clipboard_against_line_with_leading_whitespace_included_in_selection(
cx: &mut TestAppContext,
) {
base_test(
path!("/test"),
path!("/test/text.txt"),
"a",
"« bbˇ»",
&unindent(
"
- a
+ ˇ bb",
),
"Clipboard ↔ text.txt @ L1:1-7",
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
cx,
)
.await;
}
#[gpui::test]
async fn test_diffing_clipboard_with_leading_whitespace_against_line_with_leading_whitespace(
cx: &mut TestAppContext,
) {
base_test(
path!("/test"),
path!("/test/text.txt"),
" a",
" «bbˇ»",
&unindent(
"
- a
+ ˇ bb",
),
"Clipboard ↔ text.txt @ L1:1-7",
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
cx,
)
.await;
}
#[gpui::test]
async fn test_diffing_clipboard_with_leading_whitespace_against_line_with_leading_whitespace_included_in_selection(
cx: &mut TestAppContext,
) {
base_test(
path!("/test"),
path!("/test/text.txt"),
" a",
"« bbˇ»",
&unindent(
"
- a
+ ˇ bb",
),
"Clipboard ↔ text.txt @ L1:1-7",
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
cx,
)
.await;
}
#[gpui::test]
async fn test_diffing_clipboard_against_partial_selection_expands_to_include_trailing_characters(
cx: &mut TestAppContext,
) {
base_test(
path!("/test"),
path!("/test/text.txt"),
"a",
"«bˇ»b",
&unindent(
"
- a
+ ˇbb",
),
"Clipboard ↔ text.txt @ L1:1-3",
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
cx,
)
.await;
}
async fn base_test(
project_root: &str,
file_path: &str,
clipboard_text: &str,
editor_text: &str,
expected_diff: &str,
expected_tab_title: &str,
expected_tab_tooltip: &str,
cx: &mut TestAppContext,
) {
init_test(cx); init_test(cx);
let file_name = std::path::Path::new(file_path)
.file_name()
.unwrap()
.to_str()
.unwrap();
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
path!("/test"), project_root,
json!({ json!({
"a": { file_name: editor_text
"b": {
"text.txt": "new line 1\nline 2\nnew line 3\nline 4"
}
}
}), }),
) )
.await; .await;
let project = Project::test(fs, [path!("/test").as_ref()], cx).await; let project = Project::test(fs, [project_root.as_ref()], cx).await;
let (workspace, mut cx) = let (workspace, mut cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let buffer = project let buffer = project
.update(cx, |project, cx| { .update(cx, |project, cx| project.open_local_buffer(file_path, cx))
project.open_local_buffer(path!("/test/a/b/text.txt"), cx)
})
.await .await
.unwrap(); .unwrap();
let editor = cx.new_window_entity(|window, cx| { let editor = cx.new_window_entity(|window, cx| {
let mut editor = Editor::for_buffer(buffer, None, window, cx); let mut editor = Editor::for_buffer(buffer, None, window, cx);
editor.set_text("new line 1\nline 2\nnew line 3\nline 4\n", window, cx); let (unmarked_text, selection_ranges) = marked_text_ranges(editor_text, false);
editor.set_text(unmarked_text, window, cx);
if select_all_text { editor.change_selections(Default::default(), window, cx, |s| {
editor.select_all(&actions::SelectAll, window, cx); s.select_ranges(selection_ranges)
} });
editor editor
}); });
@ -511,7 +709,7 @@ mod tests {
.update_in(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
TextDiffView::open( TextDiffView::open(
&DiffClipboardWithSelectionData { &DiffClipboardWithSelectionData {
clipboard_text: "old line 1\nline 2\nold line 3\nline 4\n".to_string(), clipboard_text: clipboard_text.to_string(),
editor, editor,
}, },
workspace, workspace,
@ -528,26 +726,14 @@ mod tests {
assert_state_with_diff( assert_state_with_diff(
&diff_view.read_with(cx, |diff_view, _| diff_view.diff_editor.clone()), &diff_view.read_with(cx, |diff_view, _| diff_view.diff_editor.clone()),
&mut cx, &mut cx,
&unindent( expected_diff,
"
- old line 1
+ ˇnew line 1
line 2
- old line 3
+ new line 3
line 4
",
),
); );
diff_view.read_with(cx, |diff_view, cx| { diff_view.read_with(cx, |diff_view, cx| {
assert_eq!( assert_eq!(diff_view.tab_content_text(0, cx), expected_tab_title);
diff_view.tab_content_text(0, cx),
"Clipboard ↔ text.txt @ L1:1-L5:1"
);
assert_eq!( assert_eq!(
diff_view.tab_tooltip_text(cx).unwrap(), diff_view.tab_tooltip_text(cx).unwrap(),
format!("Clipboard ↔ {}", path!("test/a/b/text.txt @ L1:1-L5:1")) expected_tab_tooltip
); );
}); });
} }

View file

@ -2023,6 +2023,10 @@ impl HttpClient for NullHttpClient {
.boxed() .boxed()
} }
fn user_agent(&self) -> Option<&http_client::http::HeaderValue> {
None
}
fn proxy(&self) -> Option<&Url> { fn proxy(&self) -> Option<&Url> {
None None
} }

View file

@ -88,15 +88,24 @@ pub enum ScrollStrategy {
/// May not be possible if there's not enough list items above the item scrolled to: /// May not be possible if there's not enough list items above the item scrolled to:
/// in this case, the element will be placed at the closest possible position. /// in this case, the element will be placed at the closest possible position.
Center, Center,
/// Scrolls the element to be at the given item index from the top of the viewport. }
ToPosition(usize),
#[derive(Clone, Copy, Debug)]
#[allow(missing_docs)]
pub struct DeferredScrollToItem {
/// The item index to scroll to
pub item_index: usize,
/// The scroll strategy to use
pub strategy: ScrollStrategy,
/// The offset in number of items
pub offset: usize,
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
#[allow(missing_docs)] #[allow(missing_docs)]
pub struct UniformListScrollState { pub struct UniformListScrollState {
pub base_handle: ScrollHandle, pub base_handle: ScrollHandle,
pub deferred_scroll_to_item: Option<(usize, ScrollStrategy)>, pub deferred_scroll_to_item: Option<DeferredScrollToItem>,
/// Size of the item, captured during last layout. /// Size of the item, captured during last layout.
pub last_item_size: Option<ItemSize>, pub last_item_size: Option<ItemSize>,
/// Whether the list was vertically flipped during last layout. /// Whether the list was vertically flipped during last layout.
@ -126,7 +135,24 @@ impl UniformListScrollHandle {
/// Scroll the list to the given item index. /// Scroll the list to the given item index.
pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) { pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
self.0.borrow_mut().deferred_scroll_to_item = Some((ix, strategy)); self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
strategy,
offset: 0,
});
}
/// Scroll the list to the given item index with an offset.
///
/// For ScrollStrategy::Top, the item will be placed at the offset position from the top.
///
/// For ScrollStrategy::Center, the item will be centered between offset and the last visible item.
pub fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
strategy,
offset,
});
} }
/// Check if the list is flipped vertically. /// Check if the list is flipped vertically.
@ -139,7 +165,8 @@ impl UniformListScrollHandle {
pub fn logical_scroll_top_index(&self) -> usize { pub fn logical_scroll_top_index(&self) -> usize {
let this = self.0.borrow(); let this = self.0.borrow();
this.deferred_scroll_to_item this.deferred_scroll_to_item
.map(|(ix, _)| ix) .as_ref()
.map(|deferred| deferred.item_index)
.unwrap_or_else(|| this.base_handle.logical_scroll_top().0) .unwrap_or_else(|| this.base_handle.logical_scroll_top().0)
} }
@ -321,7 +348,8 @@ impl Element for UniformList {
scroll_offset.x = Pixels::ZERO; scroll_offset.x = Pixels::ZERO;
} }
if let Some((mut ix, scroll_strategy)) = shared_scroll_to_item { if let Some(deferred_scroll) = shared_scroll_to_item {
let mut ix = deferred_scroll.item_index;
if y_flipped { if y_flipped {
ix = self.item_count.saturating_sub(ix + 1); ix = self.item_count.saturating_sub(ix + 1);
} }
@ -330,23 +358,28 @@ impl Element for UniformList {
let item_top = item_height * ix + padding.top; let item_top = item_height * ix + padding.top;
let item_bottom = item_top + item_height; let item_bottom = item_top + item_height;
let scroll_top = -updated_scroll_offset.y; let scroll_top = -updated_scroll_offset.y;
let offset_pixels = item_height * deferred_scroll.offset;
let mut scrolled_to_top = false; let mut scrolled_to_top = false;
if item_top < scroll_top + padding.top {
if item_top < scroll_top + padding.top + offset_pixels {
scrolled_to_top = true; scrolled_to_top = true;
updated_scroll_offset.y = -(item_top) + padding.top; updated_scroll_offset.y = -(item_top) + padding.top + offset_pixels;
} else if item_bottom > scroll_top + list_height - padding.bottom { } else if item_bottom > scroll_top + list_height - padding.bottom {
scrolled_to_top = true; scrolled_to_top = true;
updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom; updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
} }
match scroll_strategy { match deferred_scroll.strategy {
ScrollStrategy::Top => {} ScrollStrategy::Top => {}
ScrollStrategy::Center => { ScrollStrategy::Center => {
if scrolled_to_top { if scrolled_to_top {
let item_center = item_top + item_height / 2.0; let item_center = item_top + item_height / 2.0;
let target_scroll_top = item_center - list_height / 2.0;
if item_top < scroll_top let viewport_height = list_height - offset_pixels;
let viewport_center = offset_pixels + viewport_height / 2.0;
let target_scroll_top = item_center - viewport_center;
if item_top < scroll_top + offset_pixels
|| item_bottom > scroll_top + list_height || item_bottom > scroll_top + list_height
{ {
updated_scroll_offset.y = -target_scroll_top updated_scroll_offset.y = -target_scroll_top
@ -356,15 +389,6 @@ impl Element for UniformList {
} }
} }
} }
ScrollStrategy::ToPosition(sticky_index) => {
let target_y_in_viewport = item_height * sticky_index;
let target_scroll_top = item_top - target_y_in_viewport;
let max_scroll_top =
(content_height - list_height).max(Pixels::ZERO);
let new_scroll_top =
target_scroll_top.clamp(Pixels::ZERO, max_scroll_top);
updated_scroll_offset.y = -new_scroll_top;
}
} }
scroll_offset = *updated_scroll_offset scroll_offset = *updated_scroll_offset
} }

View file

@ -417,17 +417,6 @@ impl Modifiers {
self.control || self.alt || self.shift || self.platform || self.function self.control || self.alt || self.shift || self.platform || self.function
} }
/// Returns the XOR of two modifier sets
pub fn xor(&self, other: &Modifiers) -> Modifiers {
Modifiers {
control: self.control ^ other.control,
alt: self.alt ^ other.alt,
shift: self.shift ^ other.shift,
platform: self.platform ^ other.platform,
function: self.function ^ other.function,
}
}
/// Whether the semantically 'secondary' modifier key is pressed. /// Whether the semantically 'secondary' modifier key is pressed.
/// ///
/// On macOS, this is the command key. /// On macOS, this is the command key.
@ -545,11 +534,62 @@ impl Modifiers {
/// Checks if this [`Modifiers`] is a subset of another [`Modifiers`]. /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
pub fn is_subset_of(&self, other: &Modifiers) -> bool { pub fn is_subset_of(&self, other: &Modifiers) -> bool {
(other.control || !self.control) (*other & *self) == *self
&& (other.alt || !self.alt) }
&& (other.shift || !self.shift) }
&& (other.platform || !self.platform)
&& (other.function || !self.function) impl std::ops::BitOr for Modifiers {
type Output = Self;
fn bitor(mut self, other: Self) -> Self::Output {
self |= other;
self
}
}
impl std::ops::BitOrAssign for Modifiers {
fn bitor_assign(&mut self, other: Self) {
self.control |= other.control;
self.alt |= other.alt;
self.shift |= other.shift;
self.platform |= other.platform;
self.function |= other.function;
}
}
impl std::ops::BitXor for Modifiers {
type Output = Self;
fn bitxor(mut self, rhs: Self) -> Self::Output {
self ^= rhs;
self
}
}
impl std::ops::BitXorAssign for Modifiers {
fn bitxor_assign(&mut self, other: Self) {
self.control ^= other.control;
self.alt ^= other.alt;
self.shift ^= other.shift;
self.platform ^= other.platform;
self.function ^= other.function;
}
}
impl std::ops::BitAnd for Modifiers {
type Output = Self;
fn bitand(mut self, rhs: Self) -> Self::Output {
self &= rhs;
self
}
}
impl std::ops::BitAndAssign for Modifiers {
fn bitand_assign(&mut self, other: Self) {
self.control &= other.control;
self.alt &= other.alt;
self.shift &= other.shift;
self.platform &= other.platform;
self.function &= other.function;
} }
} }

View file

@ -845,9 +845,15 @@ impl crate::Keystroke {
{ {
if key.is_ascii_graphic() { if key.is_ascii_graphic() {
key_utf8.to_lowercase() key_utf8.to_lowercase()
// map ctrl-a to a // map ctrl-a to `a`
} else if key_utf32 <= 0x1f { // ctrl-0..9 may emit control codes like ctrl-[, but
((key_utf32 as u8 + 0x60) as char).to_string() // we don't want to map them to `[`
} else if key_utf32 <= 0x1f
&& !name.chars().next().is_some_and(|c| c.is_ascii_digit())
{
((key_utf32 as u8 + 0x40) as char)
.to_ascii_lowercase()
.to_string()
} else { } else {
name name
} }

View file

@ -1004,12 +1004,13 @@ impl X11Client {
let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
let keysym = state.xkb.key_get_one_sym(code); let keysym = state.xkb.key_get_one_sym(code);
// should be called after key_get_one_sym
state.xkb.update_key(code, xkbc::KeyDirection::Down);
if keysym.is_modifier_key() { if keysym.is_modifier_key() {
return Some(()); return Some(());
} }
// should be called after key_get_one_sym
state.xkb.update_key(code, xkbc::KeyDirection::Down);
if let Some(mut compose_state) = state.compose_state.take() { if let Some(mut compose_state) = state.compose_state.take() {
compose_state.feed(keysym); compose_state.feed(keysym);
match compose_state.status() { match compose_state.status() {
@ -1067,12 +1068,13 @@ impl X11Client {
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
let keysym = state.xkb.key_get_one_sym(code); let keysym = state.xkb.key_get_one_sym(code);
// should be called after key_get_one_sym
state.xkb.update_key(code, xkbc::KeyDirection::Up);
if keysym.is_modifier_key() { if keysym.is_modifier_key() {
return Some(()); return Some(());
} }
// should be called after key_get_one_sym
state.xkb.update_key(code, xkbc::KeyDirection::Up);
keystroke keystroke
}; };
drop(state); drop(state);

View file

@ -4,6 +4,7 @@ pub mod github;
pub use anyhow::{Result, anyhow}; pub use anyhow::{Result, anyhow};
pub use async_body::{AsyncBody, Inner}; pub use async_body::{AsyncBody, Inner};
use derive_more::Deref; use derive_more::Deref;
use http::HeaderValue;
pub use http::{self, Method, Request, Response, StatusCode, Uri}; pub use http::{self, Method, Request, Response, StatusCode, Uri};
use futures::future::BoxFuture; use futures::future::BoxFuture;
@ -39,6 +40,8 @@ impl HttpRequestExt for http::request::Builder {
pub trait HttpClient: 'static + Send + Sync { pub trait HttpClient: 'static + Send + Sync {
fn type_name(&self) -> &'static str; fn type_name(&self) -> &'static str;
fn user_agent(&self) -> Option<&HeaderValue>;
fn send( fn send(
&self, &self,
req: http::Request<AsyncBody>, req: http::Request<AsyncBody>,
@ -118,6 +121,10 @@ impl HttpClient for HttpClientWithProxy {
self.client.send(req) self.client.send(req)
} }
fn user_agent(&self) -> Option<&HeaderValue> {
self.client.user_agent()
}
fn proxy(&self) -> Option<&Url> { fn proxy(&self) -> Option<&Url> {
self.proxy.as_ref() self.proxy.as_ref()
} }
@ -135,6 +142,10 @@ impl HttpClient for Arc<HttpClientWithProxy> {
self.client.send(req) self.client.send(req)
} }
fn user_agent(&self) -> Option<&HeaderValue> {
self.client.user_agent()
}
fn proxy(&self) -> Option<&Url> { fn proxy(&self) -> Option<&Url> {
self.proxy.as_ref() self.proxy.as_ref()
} }
@ -250,6 +261,10 @@ impl HttpClient for Arc<HttpClientWithUrl> {
self.client.send(req) self.client.send(req)
} }
fn user_agent(&self) -> Option<&HeaderValue> {
self.client.user_agent()
}
fn proxy(&self) -> Option<&Url> { fn proxy(&self) -> Option<&Url> {
self.client.proxy.as_ref() self.client.proxy.as_ref()
} }
@ -267,6 +282,10 @@ impl HttpClient for HttpClientWithUrl {
self.client.send(req) self.client.send(req)
} }
fn user_agent(&self) -> Option<&HeaderValue> {
self.client.user_agent()
}
fn proxy(&self) -> Option<&Url> { fn proxy(&self) -> Option<&Url> {
self.client.proxy.as_ref() self.client.proxy.as_ref()
} }
@ -314,6 +333,10 @@ impl HttpClient for BlockedHttpClient {
}) })
} }
fn user_agent(&self) -> Option<&HeaderValue> {
None
}
fn proxy(&self) -> Option<&Url> { fn proxy(&self) -> Option<&Url> {
None None
} }
@ -334,6 +357,7 @@ type FakeHttpHandler = Box<
#[cfg(feature = "test-support")] #[cfg(feature = "test-support")]
pub struct FakeHttpClient { pub struct FakeHttpClient {
handler: FakeHttpHandler, handler: FakeHttpHandler,
user_agent: HeaderValue,
} }
#[cfg(feature = "test-support")] #[cfg(feature = "test-support")]
@ -348,6 +372,7 @@ impl FakeHttpClient {
client: HttpClientWithProxy { client: HttpClientWithProxy {
client: Arc::new(Self { client: Arc::new(Self {
handler: Box::new(move |req| Box::pin(handler(req))), handler: Box::new(move |req| Box::pin(handler(req))),
user_agent: HeaderValue::from_static(type_name::<Self>()),
}), }),
proxy: None, proxy: None,
}, },
@ -390,6 +415,10 @@ impl HttpClient for FakeHttpClient {
future future
} }
fn user_agent(&self) -> Option<&HeaderValue> {
Some(&self.user_agent)
}
fn proxy(&self) -> Option<&Url> { fn proxy(&self) -> Option<&Url> {
None None
} }

View file

@ -1159,19 +1159,20 @@ impl RenderOnce for ZedAiConfiguration {
let manage_subscription_buttons = if is_pro { let manage_subscription_buttons = if is_pro {
Button::new("manage_settings", "Manage Subscription") Button::new("manage_settings", "Manage Subscription")
.full_width()
.style(ButtonStyle::Tinted(TintColor::Accent)) .style(ButtonStyle::Tinted(TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))) .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))
.into_any_element() .into_any_element()
} else if self.plan.is_none() || self.eligible_for_trial { } else if self.plan.is_none() || self.eligible_for_trial {
Button::new("start_trial", "Start 14-day Free Pro Trial") Button::new("start_trial", "Start 14-day Free Pro Trial")
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width() .full_width()
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx))) .on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx)))
.into_any_element() .into_any_element()
} else { } else {
Button::new("upgrade", "Upgrade to Pro") Button::new("upgrade", "Upgrade to Pro")
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width() .full_width()
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))) .on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)))
.into_any_element() .into_any_element()
}; };

View file

@ -1,4 +1,4 @@
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result, anyhow};
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, http}; use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, http};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -275,11 +275,16 @@ impl Capabilities {
} }
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct LmStudioError {
pub message: String,
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)] #[serde(untagged)]
pub enum ResponseStreamResult { pub enum ResponseStreamResult {
Ok(ResponseStreamEvent), Ok(ResponseStreamEvent),
Err { error: String }, Err { error: LmStudioError },
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -392,7 +397,6 @@ pub async fn stream_chat_completion(
let mut response = client.send(request).await?; let mut response = client.send(request).await?;
if response.status().is_success() { if response.status().is_success() {
let reader = BufReader::new(response.into_body()); let reader = BufReader::new(response.into_body());
Ok(reader Ok(reader
.lines() .lines()
.filter_map(|line| async move { .filter_map(|line| async move {
@ -402,18 +406,16 @@ pub async fn stream_chat_completion(
if line == "[DONE]" { if line == "[DONE]" {
None None
} else { } else {
let result = serde_json::from_str(&line) match serde_json::from_str(line) {
.context("Unable to parse chat completions response"); Ok(ResponseStreamResult::Ok(response)) => Some(Ok(response)),
if let Err(ref e) = result { Ok(ResponseStreamResult::Err { error, .. }) => {
eprintln!("Error parsing line: {e}\nLine content: '{line}'"); Some(Err(anyhow!(error.message)))
}
Err(error) => Some(Err(anyhow!(error))),
} }
Some(result)
} }
} }
Err(e) => { Err(error) => Some(Err(anyhow!(error))),
eprintln!("Error reading line: {e}");
Some(Err(e.into()))
}
} }
}) })
.boxed()) .boxed())

View file

@ -48,18 +48,29 @@ pub enum Model {
#[serde(rename = "codestral-latest", alias = "codestral-latest")] #[serde(rename = "codestral-latest", alias = "codestral-latest")]
#[default] #[default]
CodestralLatest, CodestralLatest,
#[serde(rename = "mistral-large-latest", alias = "mistral-large-latest")] #[serde(rename = "mistral-large-latest", alias = "mistral-large-latest")]
MistralLargeLatest, MistralLargeLatest,
#[serde(rename = "mistral-medium-latest", alias = "mistral-medium-latest")] #[serde(rename = "mistral-medium-latest", alias = "mistral-medium-latest")]
MistralMediumLatest, MistralMediumLatest,
#[serde(rename = "mistral-small-latest", alias = "mistral-small-latest")] #[serde(rename = "mistral-small-latest", alias = "mistral-small-latest")]
MistralSmallLatest, MistralSmallLatest,
#[serde(rename = "magistral-medium-latest", alias = "magistral-medium-latest")]
MagistralMediumLatest,
#[serde(rename = "magistral-small-latest", alias = "magistral-small-latest")]
MagistralSmallLatest,
#[serde(rename = "open-mistral-nemo", alias = "open-mistral-nemo")] #[serde(rename = "open-mistral-nemo", alias = "open-mistral-nemo")]
OpenMistralNemo, OpenMistralNemo,
#[serde(rename = "open-codestral-mamba", alias = "open-codestral-mamba")] #[serde(rename = "open-codestral-mamba", alias = "open-codestral-mamba")]
OpenCodestralMamba, OpenCodestralMamba,
#[serde(rename = "devstral-medium-latest", alias = "devstral-medium-latest")]
DevstralMediumLatest,
#[serde(rename = "devstral-small-latest", alias = "devstral-small-latest")] #[serde(rename = "devstral-small-latest", alias = "devstral-small-latest")]
DevstralSmallLatest, DevstralSmallLatest,
#[serde(rename = "pixtral-12b-latest", alias = "pixtral-12b-latest")] #[serde(rename = "pixtral-12b-latest", alias = "pixtral-12b-latest")]
Pixtral12BLatest, Pixtral12BLatest,
#[serde(rename = "pixtral-large-latest", alias = "pixtral-large-latest")] #[serde(rename = "pixtral-large-latest", alias = "pixtral-large-latest")]
@ -89,8 +100,11 @@ impl Model {
"mistral-large-latest" => Ok(Self::MistralLargeLatest), "mistral-large-latest" => Ok(Self::MistralLargeLatest),
"mistral-medium-latest" => Ok(Self::MistralMediumLatest), "mistral-medium-latest" => Ok(Self::MistralMediumLatest),
"mistral-small-latest" => Ok(Self::MistralSmallLatest), "mistral-small-latest" => Ok(Self::MistralSmallLatest),
"magistral-medium-latest" => Ok(Self::MagistralMediumLatest),
"magistral-small-latest" => Ok(Self::MagistralSmallLatest),
"open-mistral-nemo" => Ok(Self::OpenMistralNemo), "open-mistral-nemo" => Ok(Self::OpenMistralNemo),
"open-codestral-mamba" => Ok(Self::OpenCodestralMamba), "open-codestral-mamba" => Ok(Self::OpenCodestralMamba),
"devstral-medium-latest" => Ok(Self::DevstralMediumLatest),
"devstral-small-latest" => Ok(Self::DevstralSmallLatest), "devstral-small-latest" => Ok(Self::DevstralSmallLatest),
"pixtral-12b-latest" => Ok(Self::Pixtral12BLatest), "pixtral-12b-latest" => Ok(Self::Pixtral12BLatest),
"pixtral-large-latest" => Ok(Self::PixtralLargeLatest), "pixtral-large-latest" => Ok(Self::PixtralLargeLatest),
@ -104,8 +118,11 @@ impl Model {
Self::MistralLargeLatest => "mistral-large-latest", Self::MistralLargeLatest => "mistral-large-latest",
Self::MistralMediumLatest => "mistral-medium-latest", Self::MistralMediumLatest => "mistral-medium-latest",
Self::MistralSmallLatest => "mistral-small-latest", Self::MistralSmallLatest => "mistral-small-latest",
Self::MagistralMediumLatest => "magistral-medium-latest",
Self::MagistralSmallLatest => "magistral-small-latest",
Self::OpenMistralNemo => "open-mistral-nemo", Self::OpenMistralNemo => "open-mistral-nemo",
Self::OpenCodestralMamba => "open-codestral-mamba", Self::OpenCodestralMamba => "open-codestral-mamba",
Self::DevstralMediumLatest => "devstral-medium-latest",
Self::DevstralSmallLatest => "devstral-small-latest", Self::DevstralSmallLatest => "devstral-small-latest",
Self::Pixtral12BLatest => "pixtral-12b-latest", Self::Pixtral12BLatest => "pixtral-12b-latest",
Self::PixtralLargeLatest => "pixtral-large-latest", Self::PixtralLargeLatest => "pixtral-large-latest",
@ -119,8 +136,11 @@ impl Model {
Self::MistralLargeLatest => "mistral-large-latest", Self::MistralLargeLatest => "mistral-large-latest",
Self::MistralMediumLatest => "mistral-medium-latest", Self::MistralMediumLatest => "mistral-medium-latest",
Self::MistralSmallLatest => "mistral-small-latest", Self::MistralSmallLatest => "mistral-small-latest",
Self::MagistralMediumLatest => "magistral-medium-latest",
Self::MagistralSmallLatest => "magistral-small-latest",
Self::OpenMistralNemo => "open-mistral-nemo", Self::OpenMistralNemo => "open-mistral-nemo",
Self::OpenCodestralMamba => "open-codestral-mamba", Self::OpenCodestralMamba => "open-codestral-mamba",
Self::DevstralMediumLatest => "devstral-medium-latest",
Self::DevstralSmallLatest => "devstral-small-latest", Self::DevstralSmallLatest => "devstral-small-latest",
Self::Pixtral12BLatest => "pixtral-12b-latest", Self::Pixtral12BLatest => "pixtral-12b-latest",
Self::PixtralLargeLatest => "pixtral-large-latest", Self::PixtralLargeLatest => "pixtral-large-latest",
@ -136,8 +156,11 @@ impl Model {
Self::MistralLargeLatest => 131000, Self::MistralLargeLatest => 131000,
Self::MistralMediumLatest => 128000, Self::MistralMediumLatest => 128000,
Self::MistralSmallLatest => 32000, Self::MistralSmallLatest => 32000,
Self::MagistralMediumLatest => 40000,
Self::MagistralSmallLatest => 40000,
Self::OpenMistralNemo => 131000, Self::OpenMistralNemo => 131000,
Self::OpenCodestralMamba => 256000, Self::OpenCodestralMamba => 256000,
Self::DevstralMediumLatest => 128000,
Self::DevstralSmallLatest => 262144, Self::DevstralSmallLatest => 262144,
Self::Pixtral12BLatest => 128000, Self::Pixtral12BLatest => 128000,
Self::PixtralLargeLatest => 128000, Self::PixtralLargeLatest => 128000,
@ -160,8 +183,11 @@ impl Model {
| Self::MistralLargeLatest | Self::MistralLargeLatest
| Self::MistralMediumLatest | Self::MistralMediumLatest
| Self::MistralSmallLatest | Self::MistralSmallLatest
| Self::MagistralMediumLatest
| Self::MagistralSmallLatest
| Self::OpenMistralNemo | Self::OpenMistralNemo
| Self::OpenCodestralMamba | Self::OpenCodestralMamba
| Self::DevstralMediumLatest
| Self::DevstralSmallLatest | Self::DevstralSmallLatest
| Self::Pixtral12BLatest | Self::Pixtral12BLatest
| Self::PixtralLargeLatest => true, | Self::PixtralLargeLatest => true,
@ -177,8 +203,11 @@ impl Model {
| Self::MistralSmallLatest => true, | Self::MistralSmallLatest => true,
Self::CodestralLatest Self::CodestralLatest
| Self::MistralLargeLatest | Self::MistralLargeLatest
| Self::MagistralMediumLatest
| Self::MagistralSmallLatest
| Self::OpenMistralNemo | Self::OpenMistralNemo
| Self::OpenCodestralMamba | Self::OpenCodestralMamba
| Self::DevstralMediumLatest
| Self::DevstralSmallLatest => false, | Self::DevstralSmallLatest => false,
Self::Custom { Self::Custom {
supports_images, .. supports_images, ..

View file

@ -167,10 +167,10 @@ impl Anchor {
if *self == Anchor::min() || *self == Anchor::max() { if *self == Anchor::min() || *self == Anchor::max() {
true true
} else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
excerpt.contains(self) (self.text_anchor == excerpt.range.context.start
&& (self.text_anchor == excerpt.range.context.start || self.text_anchor == excerpt.range.context.end
|| self.text_anchor == excerpt.range.context.end || self.text_anchor.is_valid(&excerpt.buffer))
|| self.text_anchor.is_valid(&excerpt.buffer)) && excerpt.contains(self)
} else { } else {
false false
} }

View file

@ -55,6 +55,7 @@ fn get_max_tokens(name: &str) -> u64 {
"codellama" | "starcoder2" => 16384, "codellama" | "starcoder2" => 16384,
"mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "qwen2.5-coder" "mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "qwen2.5-coder"
| "dolphin-mixtral" => 32768, | "dolphin-mixtral" => 32768,
"magistral" => 40000,
"llama3.1" | "llama3.2" | "llama3.3" | "phi3" | "phi3.5" | "phi4" | "command-r" "llama3.1" | "llama3.2" | "llama3.3" | "phi3" | "phi3.5" | "phi4" | "command-r"
| "qwen3" | "gemma3" | "deepseek-coder-v2" | "deepseek-v3" | "deepseek-r1" | "yi-coder" | "qwen3" | "gemma3" | "deepseek-coder-v2" | "deepseek-v3" | "deepseek-r1" | "yi-coder"
| "devstral" => 128000, | "devstral" => 128000,

View file

@ -13,6 +13,7 @@ use settings::{Settings as _, SettingsStore};
use util::ResultExt as _; use util::ResultExt as _;
use crate::{ use crate::{
Project,
project_settings::{ContextServerSettings, ProjectSettings}, project_settings::{ContextServerSettings, ProjectSettings},
worktree_store::WorktreeStore, worktree_store::WorktreeStore,
}; };
@ -144,6 +145,7 @@ pub struct ContextServerStore {
context_server_settings: HashMap<Arc<str>, ContextServerSettings>, context_server_settings: HashMap<Arc<str>, ContextServerSettings>,
servers: HashMap<ContextServerId, ContextServerState>, servers: HashMap<ContextServerId, ContextServerState>,
worktree_store: Entity<WorktreeStore>, worktree_store: Entity<WorktreeStore>,
project: WeakEntity<Project>,
registry: Entity<ContextServerDescriptorRegistry>, registry: Entity<ContextServerDescriptorRegistry>,
update_servers_task: Option<Task<Result<()>>>, update_servers_task: Option<Task<Result<()>>>,
context_server_factory: Option<ContextServerFactory>, context_server_factory: Option<ContextServerFactory>,
@ -161,12 +163,17 @@ pub enum Event {
impl EventEmitter<Event> for ContextServerStore {} impl EventEmitter<Event> for ContextServerStore {}
impl ContextServerStore { impl ContextServerStore {
pub fn new(worktree_store: Entity<WorktreeStore>, cx: &mut Context<Self>) -> Self { pub fn new(
worktree_store: Entity<WorktreeStore>,
weak_project: WeakEntity<Project>,
cx: &mut Context<Self>,
) -> Self {
Self::new_internal( Self::new_internal(
true, true,
None, None,
ContextServerDescriptorRegistry::default_global(cx), ContextServerDescriptorRegistry::default_global(cx),
worktree_store, worktree_store,
weak_project,
cx, cx,
) )
} }
@ -184,9 +191,10 @@ impl ContextServerStore {
pub fn test( pub fn test(
registry: Entity<ContextServerDescriptorRegistry>, registry: Entity<ContextServerDescriptorRegistry>,
worktree_store: Entity<WorktreeStore>, worktree_store: Entity<WorktreeStore>,
weak_project: WeakEntity<Project>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
Self::new_internal(false, None, registry, worktree_store, cx) Self::new_internal(false, None, registry, worktree_store, weak_project, cx)
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -194,6 +202,7 @@ impl ContextServerStore {
context_server_factory: ContextServerFactory, context_server_factory: ContextServerFactory,
registry: Entity<ContextServerDescriptorRegistry>, registry: Entity<ContextServerDescriptorRegistry>,
worktree_store: Entity<WorktreeStore>, worktree_store: Entity<WorktreeStore>,
weak_project: WeakEntity<Project>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
Self::new_internal( Self::new_internal(
@ -201,6 +210,7 @@ impl ContextServerStore {
Some(context_server_factory), Some(context_server_factory),
registry, registry,
worktree_store, worktree_store,
weak_project,
cx, cx,
) )
} }
@ -210,6 +220,7 @@ impl ContextServerStore {
context_server_factory: Option<ContextServerFactory>, context_server_factory: Option<ContextServerFactory>,
registry: Entity<ContextServerDescriptorRegistry>, registry: Entity<ContextServerDescriptorRegistry>,
worktree_store: Entity<WorktreeStore>, worktree_store: Entity<WorktreeStore>,
weak_project: WeakEntity<Project>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let subscriptions = if maintain_server_loop { let subscriptions = if maintain_server_loop {
@ -235,6 +246,7 @@ impl ContextServerStore {
context_server_settings: Self::resolve_context_server_settings(&worktree_store, cx) context_server_settings: Self::resolve_context_server_settings(&worktree_store, cx)
.clone(), .clone(),
worktree_store, worktree_store,
project: weak_project,
registry, registry,
needs_server_update: false, needs_server_update: false,
servers: HashMap::default(), servers: HashMap::default(),
@ -360,7 +372,7 @@ impl ContextServerStore {
let configuration = state.configuration(); let configuration = state.configuration();
self.stop_server(&state.server().id(), cx)?; self.stop_server(&state.server().id(), cx)?;
let new_server = self.create_context_server(id.clone(), configuration.clone())?; let new_server = self.create_context_server(id.clone(), configuration.clone(), cx);
self.run_server(new_server, configuration, cx); self.run_server(new_server, configuration, cx);
} }
Ok(()) Ok(())
@ -449,14 +461,33 @@ impl ContextServerStore {
&self, &self,
id: ContextServerId, id: ContextServerId,
configuration: Arc<ContextServerConfiguration>, configuration: Arc<ContextServerConfiguration>,
) -> Result<Arc<ContextServer>> { cx: &mut Context<Self>,
) -> Arc<ContextServer> {
let root_path = self
.project
.read_with(cx, |project, cx| project.active_project_directory(cx))
.ok()
.flatten()
.or_else(|| {
self.worktree_store.read_with(cx, |store, cx| {
store.visible_worktrees(cx).fold(None, |acc, item| {
if acc.is_none() {
item.read(cx).root_dir()
} else {
acc
}
})
})
});
if let Some(factory) = self.context_server_factory.as_ref() { if let Some(factory) = self.context_server_factory.as_ref() {
Ok(factory(id, configuration)) factory(id, configuration)
} else { } else {
Ok(Arc::new(ContextServer::stdio( Arc::new(ContextServer::stdio(
id, id,
configuration.command().clone(), configuration.command().clone(),
))) root_path,
))
} }
} }
@ -553,7 +584,7 @@ impl ContextServerStore {
let mut servers_to_remove = HashSet::default(); let mut servers_to_remove = HashSet::default();
let mut servers_to_stop = HashSet::default(); let mut servers_to_stop = HashSet::default();
this.update(cx, |this, _cx| { this.update(cx, |this, cx| {
for server_id in this.servers.keys() { for server_id in this.servers.keys() {
// All servers that are not in desired_servers should be removed from the store. // All servers that are not in desired_servers should be removed from the store.
// This can happen if the user removed a server from the context server settings. // This can happen if the user removed a server from the context server settings.
@ -572,14 +603,10 @@ impl ContextServerStore {
let existing_config = state.as_ref().map(|state| state.configuration()); let existing_config = state.as_ref().map(|state| state.configuration());
if existing_config.as_deref() != Some(&config) || is_stopped { if existing_config.as_deref() != Some(&config) || is_stopped {
let config = Arc::new(config); let config = Arc::new(config);
if let Some(server) = this let server = this.create_context_server(id.clone(), config.clone(), cx);
.create_context_server(id.clone(), config.clone()) servers_to_start.push((server, config));
.log_err() if this.servers.contains_key(&id) {
{ servers_to_stop.insert(id);
servers_to_start.push((server, config));
if this.servers.contains_key(&id) {
servers_to_stop.insert(id);
}
} }
} }
} }
@ -630,7 +657,12 @@ mod tests {
let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
let store = cx.new(|cx| { let store = cx.new(|cx| {
ContextServerStore::test(registry.clone(), project.read(cx).worktree_store(), cx) ContextServerStore::test(
registry.clone(),
project.read(cx).worktree_store(),
project.downgrade(),
cx,
)
}); });
let server_1_id = ContextServerId(SERVER_1_ID.into()); let server_1_id = ContextServerId(SERVER_1_ID.into());
@ -705,7 +737,12 @@ mod tests {
let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
let store = cx.new(|cx| { let store = cx.new(|cx| {
ContextServerStore::test(registry.clone(), project.read(cx).worktree_store(), cx) ContextServerStore::test(
registry.clone(),
project.read(cx).worktree_store(),
project.downgrade(),
cx,
)
}); });
let server_1_id = ContextServerId(SERVER_1_ID.into()); let server_1_id = ContextServerId(SERVER_1_ID.into());
@ -758,7 +795,12 @@ mod tests {
let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
let store = cx.new(|cx| { let store = cx.new(|cx| {
ContextServerStore::test(registry.clone(), project.read(cx).worktree_store(), cx) ContextServerStore::test(
registry.clone(),
project.read(cx).worktree_store(),
project.downgrade(),
cx,
)
}); });
let server_id = ContextServerId(SERVER_1_ID.into()); let server_id = ContextServerId(SERVER_1_ID.into());
@ -842,6 +884,7 @@ mod tests {
}), }),
registry.clone(), registry.clone(),
project.read(cx).worktree_store(), project.read(cx).worktree_store(),
project.downgrade(),
cx, cx,
) )
}); });
@ -1074,6 +1117,7 @@ mod tests {
}), }),
registry.clone(), registry.clone(),
project.read(cx).worktree_store(), project.read(cx).worktree_store(),
project.downgrade(),
cx, cx,
) )
}); });

View file

@ -1,7 +1,7 @@
use std::{path::Path, sync::Arc}; use std::{path::Path, sync::Arc};
use dap::client::DebugAdapterClient; use dap::client::DebugAdapterClient;
use gpui::{App, AppContext, Subscription}; use gpui::{App, Subscription};
use super::session::{Session, SessionStateEvent}; use super::session::{Session, SessionStateEvent};
@ -19,14 +19,6 @@ pub fn intercept_debug_sessions<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
let client = session.adapter_client().unwrap(); let client = session.adapter_client().unwrap();
register_default_handlers(session, &client, cx); register_default_handlers(session, &client, cx);
configure(&client); configure(&client);
cx.background_spawn(async move {
client
.fake_event(dap::messages::Events::Initialized(
Some(Default::default()),
))
.await
})
.detach();
} }
}) })
.detach(); .detach();

View file

@ -2269,7 +2269,7 @@ impl LspCommand for GetCompletions {
// the range based on the syntax tree. // the range based on the syntax tree.
None => { None => {
if self.position != clipped_position { if self.position != clipped_position {
log::info!("completion out of expected range"); log::info!("completion out of expected range ");
return false; return false;
} }
@ -2483,7 +2483,9 @@ pub(crate) fn parse_completion_text_edit(
let start = snapshot.clip_point_utf16(range.start, Bias::Left); let start = snapshot.clip_point_utf16(range.start, Bias::Left);
let end = snapshot.clip_point_utf16(range.end, Bias::Left); let end = snapshot.clip_point_utf16(range.end, Bias::Left);
if start != range.start.0 || end != range.end.0 { if start != range.start.0 || end != range.end.0 {
log::info!("completion out of expected range"); log::info!(
"completion out of expected range, start: {start:?}, end: {end:?}, range: {range:?}"
);
return None; return None;
} }
snapshot.anchor_before(start)..snapshot.anchor_after(end) snapshot.anchor_before(start)..snapshot.anchor_after(end)

View file

@ -3551,7 +3551,8 @@ pub struct LspStore {
_maintain_buffer_languages: Task<()>, _maintain_buffer_languages: Task<()>,
diagnostic_summaries: diagnostic_summaries:
HashMap<WorktreeId, HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>>, HashMap<WorktreeId, HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>>,
lsp_data: HashMap<BufferId, DocumentColorData>, lsp_document_colors: HashMap<BufferId, DocumentColorData>,
lsp_code_lens: HashMap<BufferId, CodeLensData>,
} }
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@ -3561,6 +3562,7 @@ pub struct DocumentColors {
} }
type DocumentColorTask = Shared<Task<std::result::Result<DocumentColors, Arc<anyhow::Error>>>>; type DocumentColorTask = Shared<Task<std::result::Result<DocumentColors, Arc<anyhow::Error>>>>;
type CodeLensTask = Shared<Task<std::result::Result<Vec<CodeAction>, Arc<anyhow::Error>>>>;
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct DocumentColorData { struct DocumentColorData {
@ -3570,8 +3572,15 @@ struct DocumentColorData {
colors_update: Option<(Global, DocumentColorTask)>, colors_update: Option<(Global, DocumentColorTask)>,
} }
#[derive(Debug, Default)]
struct CodeLensData {
lens_for_version: Global,
lens: HashMap<LanguageServerId, Vec<CodeAction>>,
update: Option<(Global, CodeLensTask)>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ColorFetchStrategy { pub enum LspFetchStrategy {
IgnoreCache, IgnoreCache,
UseCache { known_cache_version: Option<usize> }, UseCache { known_cache_version: Option<usize> },
} }
@ -3804,7 +3813,8 @@ impl LspStore {
language_server_statuses: Default::default(), language_server_statuses: Default::default(),
nonce: StdRng::from_entropy().r#gen(), nonce: StdRng::from_entropy().r#gen(),
diagnostic_summaries: HashMap::default(), diagnostic_summaries: HashMap::default(),
lsp_data: HashMap::default(), lsp_document_colors: HashMap::default(),
lsp_code_lens: HashMap::default(),
active_entry: None, active_entry: None,
_maintain_workspace_config, _maintain_workspace_config,
_maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx), _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx),
@ -3861,7 +3871,8 @@ impl LspStore {
language_server_statuses: Default::default(), language_server_statuses: Default::default(),
nonce: StdRng::from_entropy().r#gen(), nonce: StdRng::from_entropy().r#gen(),
diagnostic_summaries: HashMap::default(), diagnostic_summaries: HashMap::default(),
lsp_data: HashMap::default(), lsp_document_colors: HashMap::default(),
lsp_code_lens: HashMap::default(),
active_entry: None, active_entry: None,
toolchain_store, toolchain_store,
_maintain_workspace_config, _maintain_workspace_config,
@ -4162,7 +4173,8 @@ impl LspStore {
*refcount *refcount
}; };
if refcount == 0 { if refcount == 0 {
lsp_store.lsp_data.remove(&buffer_id); lsp_store.lsp_document_colors.remove(&buffer_id);
lsp_store.lsp_code_lens.remove(&buffer_id);
let local = lsp_store.as_local_mut().unwrap(); let local = lsp_store.as_local_mut().unwrap();
local.registered_buffers.remove(&buffer_id); local.registered_buffers.remove(&buffer_id);
local.buffers_opened_in_servers.remove(&buffer_id); local.buffers_opened_in_servers.remove(&buffer_id);
@ -5702,69 +5714,168 @@ impl LspStore {
} }
} }
pub fn code_lens( pub fn code_lens_actions(
&mut self, &mut self,
buffer_handle: &Entity<Buffer>, buffer: &Entity<Buffer>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<Vec<CodeAction>>> { ) -> CodeLensTask {
let version_queried_for = buffer.read(cx).version();
let buffer_id = buffer.read(cx).remote_id();
if let Some(cached_data) = self.lsp_code_lens.get(&buffer_id) {
if !version_queried_for.changed_since(&cached_data.lens_for_version) {
let has_different_servers = self.as_local().is_some_and(|local| {
local
.buffers_opened_in_servers
.get(&buffer_id)
.cloned()
.unwrap_or_default()
!= cached_data.lens.keys().copied().collect()
});
if !has_different_servers {
return Task::ready(Ok(cached_data.lens.values().flatten().cloned().collect()))
.shared();
}
}
}
let lsp_data = self.lsp_code_lens.entry(buffer_id).or_default();
if let Some((updating_for, running_update)) = &lsp_data.update {
if !version_queried_for.changed_since(&updating_for) {
return running_update.clone();
}
}
let buffer = buffer.clone();
let query_version_queried_for = version_queried_for.clone();
let new_task = cx
.spawn(async move |lsp_store, cx| {
cx.background_executor()
.timer(Duration::from_millis(30))
.await;
let fetched_lens = lsp_store
.update(cx, |lsp_store, cx| lsp_store.fetch_code_lens(&buffer, cx))
.map_err(Arc::new)?
.await
.context("fetching code lens")
.map_err(Arc::new);
let fetched_lens = match fetched_lens {
Ok(fetched_lens) => fetched_lens,
Err(e) => {
lsp_store
.update(cx, |lsp_store, _| {
lsp_store.lsp_code_lens.entry(buffer_id).or_default().update = None;
})
.ok();
return Err(e);
}
};
lsp_store
.update(cx, |lsp_store, _| {
let lsp_data = lsp_store.lsp_code_lens.entry(buffer_id).or_default();
if lsp_data.lens_for_version == query_version_queried_for {
lsp_data.lens.extend(fetched_lens.clone());
} else if !lsp_data
.lens_for_version
.changed_since(&query_version_queried_for)
{
lsp_data.lens_for_version = query_version_queried_for;
lsp_data.lens = fetched_lens.clone();
}
lsp_data.update = None;
lsp_data.lens.values().flatten().cloned().collect()
})
.map_err(Arc::new)
})
.shared();
lsp_data.update = Some((version_queried_for, new_task.clone()));
new_task
}
fn fetch_code_lens(
&mut self,
buffer: &Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<HashMap<LanguageServerId, Vec<CodeAction>>>> {
if let Some((upstream_client, project_id)) = self.upstream_client() { if let Some((upstream_client, project_id)) = self.upstream_client() {
let request_task = upstream_client.request(proto::MultiLspQuery { let request_task = upstream_client.request(proto::MultiLspQuery {
buffer_id: buffer_handle.read(cx).remote_id().into(), buffer_id: buffer.read(cx).remote_id().into(),
version: serialize_version(&buffer_handle.read(cx).version()), version: serialize_version(&buffer.read(cx).version()),
project_id, project_id,
strategy: Some(proto::multi_lsp_query::Strategy::All( strategy: Some(proto::multi_lsp_query::Strategy::All(
proto::AllLanguageServers {}, proto::AllLanguageServers {},
)), )),
request: Some(proto::multi_lsp_query::Request::GetCodeLens( request: Some(proto::multi_lsp_query::Request::GetCodeLens(
GetCodeLens.to_proto(project_id, buffer_handle.read(cx)), GetCodeLens.to_proto(project_id, buffer.read(cx)),
)), )),
}); });
let buffer = buffer_handle.clone(); let buffer = buffer.clone();
cx.spawn(async move |weak_project, cx| { cx.spawn(async move |weak_lsp_store, cx| {
let Some(project) = weak_project.upgrade() else { let Some(lsp_store) = weak_lsp_store.upgrade() else {
return Ok(Vec::new()); return Ok(HashMap::default());
}; };
let responses = request_task.await?.responses; let responses = request_task.await?.responses;
let code_lens = join_all( let code_lens_actions = join_all(
responses responses
.into_iter() .into_iter()
.filter_map(|lsp_response| match lsp_response.response? { .filter_map(|lsp_response| {
proto::lsp_response::Response::GetCodeLensResponse(response) => { let response = match lsp_response.response? {
Some(response) proto::lsp_response::Response::GetCodeLensResponse(response) => {
} Some(response)
unexpected => { }
debug_panic!("Unexpected response: {unexpected:?}"); unexpected => {
None debug_panic!("Unexpected response: {unexpected:?}");
} None
}
}?;
let server_id = LanguageServerId::from_proto(lsp_response.server_id);
Some((server_id, response))
}) })
.map(|code_lens_response| { .map(|(server_id, code_lens_response)| {
GetCodeLens.response_from_proto( let lsp_store = lsp_store.clone();
code_lens_response, let buffer = buffer.clone();
project.clone(), let cx = cx.clone();
buffer.clone(), async move {
cx.clone(), (
) server_id,
GetCodeLens
.response_from_proto(
code_lens_response,
lsp_store,
buffer,
cx,
)
.await,
)
}
}), }),
) )
.await; .await;
Ok(code_lens let mut has_errors = false;
let code_lens_actions = code_lens_actions
.into_iter() .into_iter()
.collect::<Result<Vec<Vec<_>>>>()? .filter_map(|(server_id, code_lens)| match code_lens {
.into_iter() Ok(code_lens) => Some((server_id, code_lens)),
.flatten() Err(e) => {
.collect()) has_errors = true;
log::error!("{e:#}");
None
}
})
.collect::<HashMap<_, _>>();
anyhow::ensure!(
!has_errors || !code_lens_actions.is_empty(),
"Failed to fetch code lens"
);
Ok(code_lens_actions)
}) })
} else { } else {
let code_lens_task = let code_lens_actions_task =
self.request_multiple_lsp_locally(buffer_handle, None::<usize>, GetCodeLens, cx); self.request_multiple_lsp_locally(buffer, None::<usize>, GetCodeLens, cx);
cx.spawn(async move |_, _| { cx.background_spawn(
Ok(code_lens_task async move { Ok(code_lens_actions_task.await.into_iter().collect()) },
.await )
.into_iter()
.flat_map(|(_, code_lens)| code_lens)
.collect())
})
} }
} }
@ -6597,7 +6708,7 @@ impl LspStore {
pub fn document_colors( pub fn document_colors(
&mut self, &mut self,
fetch_strategy: ColorFetchStrategy, fetch_strategy: LspFetchStrategy,
buffer: Entity<Buffer>, buffer: Entity<Buffer>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Option<DocumentColorTask> { ) -> Option<DocumentColorTask> {
@ -6605,11 +6716,11 @@ impl LspStore {
let buffer_id = buffer.read(cx).remote_id(); let buffer_id = buffer.read(cx).remote_id();
match fetch_strategy { match fetch_strategy {
ColorFetchStrategy::IgnoreCache => {} LspFetchStrategy::IgnoreCache => {}
ColorFetchStrategy::UseCache { LspFetchStrategy::UseCache {
known_cache_version, known_cache_version,
} => { } => {
if let Some(cached_data) = self.lsp_data.get(&buffer_id) { if let Some(cached_data) = self.lsp_document_colors.get(&buffer_id) {
if !version_queried_for.changed_since(&cached_data.colors_for_version) { if !version_queried_for.changed_since(&cached_data.colors_for_version) {
let has_different_servers = self.as_local().is_some_and(|local| { let has_different_servers = self.as_local().is_some_and(|local| {
local local
@ -6642,7 +6753,7 @@ impl LspStore {
} }
} }
let lsp_data = self.lsp_data.entry(buffer_id).or_default(); let lsp_data = self.lsp_document_colors.entry(buffer_id).or_default();
if let Some((updating_for, running_update)) = &lsp_data.colors_update { if let Some((updating_for, running_update)) = &lsp_data.colors_update {
if !version_queried_for.changed_since(&updating_for) { if !version_queried_for.changed_since(&updating_for) {
return Some(running_update.clone()); return Some(running_update.clone());
@ -6656,14 +6767,14 @@ impl LspStore {
.await; .await;
let fetched_colors = lsp_store let fetched_colors = lsp_store
.update(cx, |lsp_store, cx| { .update(cx, |lsp_store, cx| {
lsp_store.fetch_document_colors_for_buffer(buffer.clone(), cx) lsp_store.fetch_document_colors_for_buffer(&buffer, cx)
})? })?
.await .await
.context("fetching document colors") .context("fetching document colors")
.map_err(Arc::new); .map_err(Arc::new);
let fetched_colors = match fetched_colors { let fetched_colors = match fetched_colors {
Ok(fetched_colors) => { Ok(fetched_colors) => {
if fetch_strategy != ColorFetchStrategy::IgnoreCache if fetch_strategy != LspFetchStrategy::IgnoreCache
&& Some(true) && Some(true)
== buffer == buffer
.update(cx, |buffer, _| { .update(cx, |buffer, _| {
@ -6679,7 +6790,7 @@ impl LspStore {
lsp_store lsp_store
.update(cx, |lsp_store, _| { .update(cx, |lsp_store, _| {
lsp_store lsp_store
.lsp_data .lsp_document_colors
.entry(buffer_id) .entry(buffer_id)
.or_default() .or_default()
.colors_update = None; .colors_update = None;
@ -6691,7 +6802,7 @@ impl LspStore {
lsp_store lsp_store
.update(cx, |lsp_store, _| { .update(cx, |lsp_store, _| {
let lsp_data = lsp_store.lsp_data.entry(buffer_id).or_default(); let lsp_data = lsp_store.lsp_document_colors.entry(buffer_id).or_default();
if lsp_data.colors_for_version == query_version_queried_for { if lsp_data.colors_for_version == query_version_queried_for {
lsp_data.colors.extend(fetched_colors.clone()); lsp_data.colors.extend(fetched_colors.clone());
@ -6725,7 +6836,7 @@ impl LspStore {
fn fetch_document_colors_for_buffer( fn fetch_document_colors_for_buffer(
&mut self, &mut self,
buffer: Entity<Buffer>, buffer: &Entity<Buffer>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<anyhow::Result<HashMap<LanguageServerId, HashSet<DocumentColor>>>> { ) -> Task<anyhow::Result<HashMap<LanguageServerId, HashSet<DocumentColor>>>> {
if let Some((client, project_id)) = self.upstream_client() { if let Some((client, project_id)) = self.upstream_client() {
@ -6740,6 +6851,7 @@ impl LspStore {
GetDocumentColor {}.to_proto(project_id, buffer.read(cx)), GetDocumentColor {}.to_proto(project_id, buffer.read(cx)),
)), )),
}); });
let buffer = buffer.clone();
cx.spawn(async move |project, cx| { cx.spawn(async move |project, cx| {
let Some(project) = project.upgrade() else { let Some(project) = project.upgrade() else {
return Ok(HashMap::default()); return Ok(HashMap::default());
@ -6785,7 +6897,7 @@ impl LspStore {
}) })
} else { } else {
let document_colors_task = let document_colors_task =
self.request_multiple_lsp_locally(&buffer, None::<usize>, GetDocumentColor, cx); self.request_multiple_lsp_locally(buffer, None::<usize>, GetDocumentColor, cx);
cx.spawn(async move |_, _| { cx.spawn(async move |_, _| {
Ok(document_colors_task Ok(document_colors_task
.await .await
@ -7325,21 +7437,23 @@ impl LspStore {
} }
pub(crate) async fn refresh_workspace_configurations( pub(crate) async fn refresh_workspace_configurations(
this: &WeakEntity<Self>, lsp_store: &WeakEntity<Self>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
cx: &mut AsyncApp, cx: &mut AsyncApp,
) { ) {
maybe!(async move { maybe!(async move {
let servers = this let mut refreshed_servers = HashSet::default();
.update(cx, |this, cx| { let servers = lsp_store
let Some(local) = this.as_local() else { .update(cx, |lsp_store, cx| {
let toolchain_store = lsp_store.toolchain_store(cx);
let Some(local) = lsp_store.as_local() else {
return Vec::default(); return Vec::default();
}; };
local local
.language_server_ids .language_server_ids
.iter() .iter()
.flat_map(|((worktree_id, _), server_ids)| { .flat_map(|((worktree_id, _), server_ids)| {
let worktree = this let worktree = lsp_store
.worktree_store .worktree_store
.read(cx) .read(cx)
.worktree_for_id(*worktree_id, cx); .worktree_for_id(*worktree_id, cx);
@ -7355,43 +7469,54 @@ impl LspStore {
) )
}); });
server_ids.iter().filter_map(move |server_id| { let fs = fs.clone();
let toolchain_store = toolchain_store.clone();
server_ids.iter().filter_map(|server_id| {
let delegate = delegate.clone()? as Arc<dyn LspAdapterDelegate>;
let states = local.language_servers.get(server_id)?; let states = local.language_servers.get(server_id)?;
match states { match states {
LanguageServerState::Starting { .. } => None, LanguageServerState::Starting { .. } => None,
LanguageServerState::Running { LanguageServerState::Running {
adapter, server, .. adapter, server, ..
} => Some(( } => {
adapter.adapter.clone(), let fs = fs.clone();
server.clone(), let toolchain_store = toolchain_store.clone();
delegate.clone()? as Arc<dyn LspAdapterDelegate>, let adapter = adapter.clone();
)), let server = server.clone();
refreshed_servers.insert(server.name());
Some(cx.spawn(async move |_, cx| {
let settings =
LocalLspStore::workspace_configuration_for_adapter(
adapter.adapter.clone(),
fs.as_ref(),
&delegate,
toolchain_store,
cx,
)
.await
.ok()?;
server
.notify::<lsp::notification::DidChangeConfiguration>(
&lsp::DidChangeConfigurationParams { settings },
)
.ok()?;
Some(())
}))
}
} }
}) }).collect::<Vec<_>>()
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
}) })
.ok()?; .ok()?;
let toolchain_store = this.update(cx, |this, cx| this.toolchain_store(cx)).ok()?; log::info!("Refreshing workspace configurations for servers {refreshed_servers:?}");
for (adapter, server, delegate) in servers { // TODO this asynchronous job runs concurrently with extension (de)registration and may take enough time for a certain extension
let settings = LocalLspStore::workspace_configuration_for_adapter( // to stop and unregister its language server wrapper.
adapter, // This is racy : an extension might have already removed all `local.language_servers` state, but here we `.clone()` and hold onto it anyway.
fs.as_ref(), // This now causes errors in the logs, we should find a way to remove such servers from the processing everywhere.
&delegate, let _: Vec<Option<()>> = join_all(servers).await;
toolchain_store.clone(),
cx,
)
.await
.ok()?;
server
.notify::<lsp::notification::DidChangeConfiguration>(
&lsp::DidChangeConfigurationParams { settings },
)
.ok();
}
Some(()) Some(())
}) })
.await; .await;
@ -11278,9 +11403,12 @@ impl LspStore {
} }
fn cleanup_lsp_data(&mut self, for_server: LanguageServerId) { fn cleanup_lsp_data(&mut self, for_server: LanguageServerId) {
for buffer_lsp_data in self.lsp_data.values_mut() { for buffer_colors in self.lsp_document_colors.values_mut() {
buffer_lsp_data.colors.remove(&for_server); buffer_colors.colors.remove(&for_server);
buffer_lsp_data.cache_version += 1; buffer_colors.cache_version += 1;
}
for buffer_lens in self.lsp_code_lens.values_mut() {
buffer_lens.lens.remove(&for_server);
} }
if let Some(local) = self.as_local_mut() { if let Some(local) = self.as_local_mut() {
local.buffer_pull_diagnostics_result_ids.remove(&for_server); local.buffer_pull_diagnostics_result_ids.remove(&for_server);

View file

@ -113,7 +113,7 @@ use std::{
use task_store::TaskStore; use task_store::TaskStore;
use terminals::Terminals; use terminals::Terminals;
use text::{Anchor, BufferId, Point}; use text::{Anchor, BufferId, OffsetRangeExt, Point};
use toolchain_store::EmptyToolchainStore; use toolchain_store::EmptyToolchainStore;
use util::{ use util::{
ResultExt as _, ResultExt as _,
@ -590,7 +590,7 @@ pub(crate) struct CoreCompletion {
} }
/// A code action provided by a language server. /// A code action provided by a language server.
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq)]
pub struct CodeAction { pub struct CodeAction {
/// The id of the language server that produced this code action. /// The id of the language server that produced this code action.
pub server_id: LanguageServerId, pub server_id: LanguageServerId,
@ -604,7 +604,7 @@ pub struct CodeAction {
} }
/// An action sent back by a language server. /// An action sent back by a language server.
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq)]
pub enum LspAction { pub enum LspAction {
/// An action with the full data, may have a command or may not. /// An action with the full data, may have a command or may not.
/// May require resolving. /// May require resolving.
@ -998,8 +998,9 @@ impl Project {
cx.subscribe(&worktree_store, Self::on_worktree_store_event) cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach(); .detach();
let weak_self = cx.weak_entity();
let context_server_store = let context_server_store =
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), cx)); cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx));
let environment = cx.new(|_| ProjectEnvironment::new(env)); let environment = cx.new(|_| ProjectEnvironment::new(env));
let manifest_tree = ManifestTree::new(worktree_store.clone(), cx); let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
@ -1167,8 +1168,9 @@ impl Project {
cx.subscribe(&worktree_store, Self::on_worktree_store_event) cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach(); .detach();
let weak_self = cx.weak_entity();
let context_server_store = let context_server_store =
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), cx)); cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx));
let buffer_store = cx.new(|cx| { let buffer_store = cx.new(|cx| {
BufferStore::remote( BufferStore::remote(
@ -1428,8 +1430,6 @@ impl Project {
let image_store = cx.new(|cx| { let image_store = cx.new(|cx| {
ImageStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx) ImageStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx)
})?; })?;
let context_server_store =
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), cx))?;
let environment = cx.new(|_| ProjectEnvironment::new(None))?; let environment = cx.new(|_| ProjectEnvironment::new(None))?;
@ -1496,6 +1496,10 @@ impl Project {
let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx); let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx);
let weak_self = cx.weak_entity();
let context_server_store =
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx));
let mut worktrees = Vec::new(); let mut worktrees = Vec::new();
for worktree in response.payload.worktrees { for worktree in response.payload.worktrees {
let worktree = let worktree =
@ -3607,20 +3611,29 @@ impl Project {
}) })
} }
pub fn code_lens<T: Clone + ToOffset>( pub fn code_lens_actions<T: Clone + ToOffset>(
&mut self, &mut self,
buffer_handle: &Entity<Buffer>, buffer: &Entity<Buffer>,
range: Range<T>, range: Range<T>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<Vec<CodeAction>>> { ) -> Task<Result<Vec<CodeAction>>> {
let snapshot = buffer_handle.read(cx).snapshot(); let snapshot = buffer.read(cx).snapshot();
let range = snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end); let range = range.clone().to_owned().to_point(&snapshot);
let range_start = snapshot.anchor_before(range.start);
let range_end = if range.start == range.end {
range_start
} else {
snapshot.anchor_after(range.end)
};
let range = range_start..range_end;
let code_lens_actions = self let code_lens_actions = self
.lsp_store .lsp_store
.update(cx, |lsp_store, cx| lsp_store.code_lens(buffer_handle, cx)); .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(buffer, cx));
cx.background_spawn(async move { cx.background_spawn(async move {
let mut code_lens_actions = code_lens_actions.await?; let mut code_lens_actions = code_lens_actions
.await
.map_err(|e| anyhow!("code lens fetch failed: {e:#}"))?;
code_lens_actions.retain(|code_lens_action| { code_lens_actions.retain(|code_lens_action| {
range range
.start .start

View file

@ -114,6 +114,7 @@ pub struct ProjectPanel {
mouse_down: bool, mouse_down: bool,
hover_expand_task: Option<Task<()>>, hover_expand_task: Option<Task<()>>,
previous_drag_position: Option<Point<Pixels>>, previous_drag_position: Option<Point<Pixels>>,
sticky_items_count: usize,
} }
struct DragTargetEntry { struct DragTargetEntry {
@ -322,6 +323,7 @@ pub fn init(cx: &mut App) {
}); });
workspace.register_action(|workspace, action: &Rename, window, cx| { workspace.register_action(|workspace, action: &Rename, window, cx| {
workspace.open_panel::<ProjectPanel>(window, cx);
if let Some(panel) = workspace.panel::<ProjectPanel>(cx) { if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
if let Some(first_marked) = panel.marked_entries.first() { if let Some(first_marked) = panel.marked_entries.first() {
@ -335,6 +337,7 @@ pub fn init(cx: &mut App) {
}); });
workspace.register_action(|workspace, action: &Duplicate, window, cx| { workspace.register_action(|workspace, action: &Duplicate, window, cx| {
workspace.open_panel::<ProjectPanel>(window, cx);
if let Some(panel) = workspace.panel::<ProjectPanel>(cx) { if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.duplicate(action, window, cx); panel.duplicate(action, window, cx);
@ -570,6 +573,9 @@ impl ProjectPanel {
if project_panel_settings.hide_root != new_settings.hide_root { if project_panel_settings.hide_root != new_settings.hide_root {
this.update_visible_entries(None, cx); this.update_visible_entries(None, cx);
} }
if project_panel_settings.sticky_scroll && !new_settings.sticky_scroll {
this.sticky_items_count = 0;
}
project_panel_settings = new_settings; project_panel_settings = new_settings;
this.update_diagnostics(cx); this.update_diagnostics(cx);
cx.notify(); cx.notify();
@ -613,6 +619,7 @@ impl ProjectPanel {
mouse_down: false, mouse_down: false,
hover_expand_task: None, hover_expand_task: None,
previous_drag_position: None, previous_drag_position: None,
sticky_items_count: 0,
}; };
this.update_visible_entries(None, cx); this.update_visible_entries(None, cx);
@ -2265,8 +2272,11 @@ impl ProjectPanel {
fn autoscroll(&mut self, cx: &mut Context<Self>) { fn autoscroll(&mut self, cx: &mut Context<Self>) {
if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) { if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
self.scroll_handle self.scroll_handle.scroll_to_item_with_offset(
.scroll_to_item(index, ScrollStrategy::Center); index,
ScrollStrategy::Center,
self.sticky_items_count,
);
cx.notify(); cx.notify();
} }
} }
@ -4224,10 +4234,7 @@ impl ProjectPanel {
this.marked_entries.clear(); this.marked_entries.clear();
if is_sticky { if is_sticky {
if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) { if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) {
let strategy = sticky_index this.scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, sticky_index.unwrap_or(0));
.map(ScrollStrategy::ToPosition)
.unwrap_or(ScrollStrategy::Top);
this.scroll_handle.scroll_to_item(index, strategy);
cx.notify(); cx.notify();
// move down by 1px so that clicked item // move down by 1px so that clicked item
// don't count as sticky anymore // don't count as sticky anymore
@ -5364,7 +5371,10 @@ impl Render for ProjectPanel {
items items
}, },
|this, marker_entry, window, cx| { |this, marker_entry, window, cx| {
this.render_sticky_entries(marker_entry, window, cx) let sticky_entries =
this.render_sticky_entries(marker_entry, window, cx);
this.sticky_items_count = sticky_entries.len();
sticky_entries
}, },
); );
list.with_decoration(if show_indent_guides { list.with_decoration(if show_indent_guides {

View file

@ -20,6 +20,7 @@ static REDACT_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"key=[^&]+")
pub struct ReqwestClient { pub struct ReqwestClient {
client: reqwest::Client, client: reqwest::Client,
proxy: Option<Url>, proxy: Option<Url>,
user_agent: Option<HeaderValue>,
handle: tokio::runtime::Handle, handle: tokio::runtime::Handle,
} }
@ -44,9 +45,11 @@ impl ReqwestClient {
Ok(client.into()) Ok(client.into())
} }
pub fn proxy_and_user_agent(proxy: Option<Url>, agent: &str) -> anyhow::Result<Self> { pub fn proxy_and_user_agent(proxy: Option<Url>, user_agent: &str) -> anyhow::Result<Self> {
let user_agent = HeaderValue::from_str(user_agent)?;
let mut map = HeaderMap::new(); let mut map = HeaderMap::new();
map.insert(http::header::USER_AGENT, HeaderValue::from_str(agent)?); map.insert(http::header::USER_AGENT, user_agent.clone());
let mut client = Self::builder().default_headers(map); let mut client = Self::builder().default_headers(map);
let client_has_proxy; let client_has_proxy;
@ -73,6 +76,7 @@ impl ReqwestClient {
.build()?; .build()?;
let mut client: ReqwestClient = client.into(); let mut client: ReqwestClient = client.into();
client.proxy = client_has_proxy.then_some(proxy).flatten(); client.proxy = client_has_proxy.then_some(proxy).flatten();
client.user_agent = Some(user_agent);
Ok(client) Ok(client)
} }
} }
@ -96,6 +100,7 @@ impl From<reqwest::Client> for ReqwestClient {
client, client,
handle, handle,
proxy: None, proxy: None,
user_agent: None,
} }
} }
} }
@ -216,6 +221,10 @@ impl http_client::HttpClient for ReqwestClient {
type_name::<Self>() type_name::<Self>()
} }
fn user_agent(&self) -> Option<&HeaderValue> {
self.user_agent.as_ref()
}
fn send( fn send(
&self, &self,
req: http::Request<http_client::AsyncBody>, req: http::Request<http_client::AsyncBody>,

View file

@ -566,24 +566,40 @@ impl KeymapEditor {
&& query.modifiers == keystroke.modifiers && query.modifiers == keystroke.modifiers
}, },
) )
} else if keystroke_query.len() > keystrokes.len() {
return false;
} else { } else {
let key_press_query = for keystroke_offset in 0..keystrokes.len() {
KeyPressIterator::new(keystroke_query.as_slice()); let mut found_count = 0;
let mut last_match_idx = 0; let mut query_cursor = 0;
let mut keystroke_cursor = keystroke_offset;
while query_cursor < keystroke_query.len()
&& keystroke_cursor < keystrokes.len()
{
let query = &keystroke_query[query_cursor];
let keystroke = &keystrokes[keystroke_cursor];
let matches =
query.modifiers.is_subset_of(&keystroke.modifiers)
&& ((query.key.is_empty()
|| query.key == keystroke.key)
&& query
.key_char
.as_ref()
.map_or(true, |q_kc| {
q_kc == &keystroke.key
}));
if matches {
found_count += 1;
query_cursor += 1;
}
keystroke_cursor += 1;
}
key_press_query.into_iter().all(|key| { if found_count == keystroke_query.len() {
let key_presses = KeyPressIterator::new(keystrokes); return true;
key_presses.into_iter().enumerate().any( }
|(index, keystroke)| { }
if last_match_idx > index || keystroke != key { return false;
return false;
}
last_match_idx = index;
true
},
)
})
} }
}) })
}); });
@ -1232,11 +1248,14 @@ impl KeymapEditor {
match self.search_mode { match self.search_mode {
SearchMode::KeyStroke { .. } => { SearchMode::KeyStroke { .. } => {
window.focus(&self.keystroke_editor.read(cx).recording_focus_handle(cx)); self.keystroke_editor.update(cx, |editor, cx| {
editor.start_recording(&StartRecording, window, cx);
});
} }
SearchMode::Normal => { SearchMode::Normal => {
self.keystroke_editor.update(cx, |editor, cx| { self.keystroke_editor.update(cx, |editor, cx| {
editor.clear_keystrokes(&ClearKeystrokes, window, cx) editor.stop_recording(&StopRecording, window, cx);
editor.clear_keystrokes(&ClearKeystrokes, window, cx);
}); });
window.focus(&self.filter_editor.focus_handle(cx)); window.focus(&self.filter_editor.focus_handle(cx));
} }
@ -2962,16 +2981,6 @@ enum CloseKeystrokeResult {
None, None,
} }
#[derive(PartialEq, Eq, Debug, Clone)]
enum KeyPress<'a> {
Alt,
Control,
Function,
Shift,
Platform,
Key(&'a String),
}
struct KeystrokeInput { struct KeystrokeInput {
keystrokes: Vec<Keystroke>, keystrokes: Vec<Keystroke>,
placeholder_keystrokes: Option<Vec<Keystroke>>, placeholder_keystrokes: Option<Vec<Keystroke>>,
@ -2983,6 +2992,7 @@ struct KeystrokeInput {
/// Handles tripe escape to stop recording /// Handles tripe escape to stop recording
close_keystrokes: Option<Vec<Keystroke>>, close_keystrokes: Option<Vec<Keystroke>>,
close_keystrokes_start: Option<usize>, close_keystrokes_start: Option<usize>,
previous_modifiers: Modifiers,
} }
impl KeystrokeInput { impl KeystrokeInput {
@ -3009,6 +3019,7 @@ impl KeystrokeInput {
search: false, search: false,
close_keystrokes: None, close_keystrokes: None,
close_keystrokes_start: None, close_keystrokes_start: None,
previous_modifiers: Modifiers::default(),
} }
} }
@ -3031,7 +3042,7 @@ impl KeystrokeInput {
} }
fn key_context() -> KeyContext { fn key_context() -> KeyContext {
let mut key_context = KeyContext::new_with_defaults(); let mut key_context = KeyContext::default();
key_context.add("KeystrokeInput"); key_context.add("KeystrokeInput");
key_context key_context
} }
@ -3098,12 +3109,26 @@ impl KeystrokeInput {
) { ) {
let keystrokes_len = self.keystrokes.len(); let keystrokes_len = self.keystrokes.len();
if self.previous_modifiers.modified()
&& event.modifiers.is_subset_of(&self.previous_modifiers)
{
self.previous_modifiers &= event.modifiers;
cx.stop_propagation();
return;
}
if let Some(last) = self.keystrokes.last_mut() if let Some(last) = self.keystrokes.last_mut()
&& last.key.is_empty() && last.key.is_empty()
&& keystrokes_len <= Self::KEYSTROKE_COUNT_MAX && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
{ {
if self.search { if self.search {
last.modifiers = last.modifiers.xor(&event.modifiers); if self.previous_modifiers.modified() {
last.modifiers |= event.modifiers;
self.previous_modifiers |= event.modifiers;
} else {
self.keystrokes.push(Self::dummy(event.modifiers));
self.previous_modifiers |= event.modifiers;
}
} else if !event.modifiers.modified() { } else if !event.modifiers.modified() {
self.keystrokes.pop(); self.keystrokes.pop();
} else { } else {
@ -3113,6 +3138,9 @@ impl KeystrokeInput {
self.keystrokes_changed(cx); self.keystrokes_changed(cx);
} else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX { } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
self.keystrokes.push(Self::dummy(event.modifiers)); self.keystrokes.push(Self::dummy(event.modifiers));
if self.search {
self.previous_modifiers |= event.modifiers;
}
self.keystrokes_changed(cx); self.keystrokes_changed(cx);
} }
cx.stop_propagation(); cx.stop_propagation();
@ -3138,6 +3166,9 @@ impl KeystrokeInput {
{ {
self.close_keystrokes_start = Some(self.keystrokes.len() - 1); self.close_keystrokes_start = Some(self.keystrokes.len() - 1);
} }
if self.search {
self.previous_modifiers = keystroke.modifiers;
}
self.keystrokes_changed(cx); self.keystrokes_changed(cx);
cx.stop_propagation(); cx.stop_propagation();
return; return;
@ -3152,7 +3183,9 @@ impl KeystrokeInput {
self.close_keystrokes_start = Some(self.keystrokes.len()); self.close_keystrokes_start = Some(self.keystrokes.len());
} }
self.keystrokes.push(keystroke.clone()); self.keystrokes.push(keystroke.clone());
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { if self.search {
self.previous_modifiers = keystroke.modifiers;
} else if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
self.keystrokes.push(Self::dummy(keystroke.modifiers)); self.keystrokes.push(Self::dummy(keystroke.modifiers));
} }
} else if close_keystroke_result != CloseKeystrokeResult::Partial { } else if close_keystroke_result != CloseKeystrokeResult::Partial {
@ -3222,17 +3255,11 @@ impl KeystrokeInput {
}) })
} }
fn recording_focus_handle(&self, _cx: &App) -> FocusHandle {
self.inner_focus_handle.clone()
}
fn start_recording(&mut self, _: &StartRecording, window: &mut Window, cx: &mut Context<Self>) { fn start_recording(&mut self, _: &StartRecording, window: &mut Window, cx: &mut Context<Self>) {
if !self.outer_focus_handle.is_focused(window) {
return;
}
self.clear_keystrokes(&ClearKeystrokes, window, cx);
window.focus(&self.inner_focus_handle); window.focus(&self.inner_focus_handle);
cx.notify(); self.clear_keystrokes(&ClearKeystrokes, window, cx);
self.previous_modifiers = window.modifiers();
cx.stop_propagation();
} }
fn stop_recording(&mut self, _: &StopRecording, window: &mut Window, cx: &mut Context<Self>) { fn stop_recording(&mut self, _: &StopRecording, window: &mut Window, cx: &mut Context<Self>) {
@ -3364,7 +3391,7 @@ impl Render for KeystrokeInput {
}) })
.key_context(Self::key_context()) .key_context(Self::key_context())
.on_action(cx.listener(Self::start_recording)) .on_action(cx.listener(Self::start_recording))
.on_action(cx.listener(Self::stop_recording)) .on_action(cx.listener(Self::clear_keystrokes))
.child( .child(
h_flex() h_flex()
.w(horizontal_padding) .w(horizontal_padding)
@ -3633,72 +3660,3 @@ mod persistence {
} }
} }
} }
/// Iterator that yields KeyPress values from a slice of Keystrokes
struct KeyPressIterator<'a> {
keystrokes: &'a [Keystroke],
current_keystroke_index: usize,
current_key_press_index: usize,
}
impl<'a> KeyPressIterator<'a> {
fn new(keystrokes: &'a [Keystroke]) -> Self {
Self {
keystrokes,
current_keystroke_index: 0,
current_key_press_index: 0,
}
}
}
impl<'a> Iterator for KeyPressIterator<'a> {
type Item = KeyPress<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let keystroke = self.keystrokes.get(self.current_keystroke_index)?;
match self.current_key_press_index {
0 => {
self.current_key_press_index = 1;
if keystroke.modifiers.platform {
return Some(KeyPress::Platform);
}
}
1 => {
self.current_key_press_index = 2;
if keystroke.modifiers.alt {
return Some(KeyPress::Alt);
}
}
2 => {
self.current_key_press_index = 3;
if keystroke.modifiers.control {
return Some(KeyPress::Control);
}
}
3 => {
self.current_key_press_index = 4;
if keystroke.modifiers.shift {
return Some(KeyPress::Shift);
}
}
4 => {
self.current_key_press_index = 5;
if keystroke.modifiers.function {
return Some(KeyPress::Function);
}
}
_ => {
self.current_keystroke_index += 1;
self.current_key_press_index = 0;
if keystroke.key.is_empty() {
continue;
}
return Some(KeyPress::Key(&keystroke.key));
}
}
}
}
}

View file

@ -99,7 +99,9 @@ impl Anchor {
} else if self.buffer_id != Some(buffer.remote_id) { } else if self.buffer_id != Some(buffer.remote_id) {
false false
} else { } else {
let fragment_id = buffer.fragment_id_for_anchor(self); let Some(fragment_id) = buffer.try_fragment_id_for_anchor(self) else {
return false;
};
let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>(&None); let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>(&None);
fragment_cursor.seek(&Some(fragment_id), Bias::Left); fragment_cursor.seek(&Some(fragment_id), Bias::Left);
fragment_cursor fragment_cursor

View file

@ -2330,10 +2330,19 @@ impl BufferSnapshot {
} }
fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator { fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator {
self.try_fragment_id_for_anchor(anchor).unwrap_or_else(|| {
panic!(
"invalid anchor {:?}. buffer id: {}, version: {:?}",
anchor, self.remote_id, self.version,
)
})
}
fn try_fragment_id_for_anchor(&self, anchor: &Anchor) -> Option<&Locator> {
if *anchor == Anchor::MIN { if *anchor == Anchor::MIN {
Locator::min_ref() Some(Locator::min_ref())
} else if *anchor == Anchor::MAX { } else if *anchor == Anchor::MAX {
Locator::max_ref() Some(Locator::max_ref())
} else { } else {
let anchor_key = InsertionFragmentKey { let anchor_key = InsertionFragmentKey {
timestamp: anchor.timestamp, timestamp: anchor.timestamp,
@ -2354,20 +2363,12 @@ impl BufferSnapshot {
insertion_cursor.prev(); insertion_cursor.prev();
} }
let Some(insertion) = insertion_cursor.item().filter(|insertion| { insertion_cursor
if cfg!(debug_assertions) { .item()
insertion.timestamp == anchor.timestamp .filter(|insertion| {
} else { !cfg!(debug_assertions) || insertion.timestamp == anchor.timestamp
true })
} .map(|insertion| &insertion.fragment_id)
}) else {
panic!(
"invalid anchor {:?}. buffer id: {}, version: {:?}",
anchor, self.remote_id, self.version
);
};
&insertion.fragment_id
} }
} }

View file

@ -18,15 +18,19 @@ pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<Strin
// In some shells, file descriptors greater than 2 cannot be used in interactive mode, // In some shells, file descriptors greater than 2 cannot be used in interactive mode,
// so file descriptor 0 (stdin) is used instead. This impacts zsh, old bash; perhaps others. // so file descriptor 0 (stdin) is used instead. This impacts zsh, old bash; perhaps others.
// See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482 // See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482
const ENV_OUTPUT_FD: std::os::fd::RawFd = 0; const FD_STDIN: std::os::fd::RawFd = 0;
let redir = match shell_name { const FD_STDOUT: std::os::fd::RawFd = 1;
Some("rc") => format!(">[1={}]", ENV_OUTPUT_FD), // `[1=0]`
_ => format!(">&{}", ENV_OUTPUT_FD), // `>&0` let (fd_num, redir) = match shell_name {
Some("rc") => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
Some("nu") | Some("tcsh") => (FD_STDOUT, "".to_string()),
_ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0`
}; };
command.stdin(Stdio::null()); command.stdin(Stdio::null());
command.stdout(Stdio::piped()); command.stdout(Stdio::piped());
command.stderr(Stdio::piped()); command.stderr(Stdio::piped());
let mut command_prefix = String::new();
match shell_name { match shell_name {
Some("tcsh" | "csh") => { Some("tcsh" | "csh") => {
// For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`) // For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`)
@ -37,18 +41,25 @@ pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<Strin
command_string.push_str("emit fish_prompt;"); command_string.push_str("emit fish_prompt;");
command.arg("-l"); command.arg("-l");
} }
Some("nu") => {
// nu needs special handling for -- options.
command_prefix = String::from("^");
}
_ => { _ => {
command.arg("-l"); command.arg("-l");
} }
} }
// cd into the directory, triggering directory specific side-effects (asdf, direnv, etc) // cd into the directory, triggering directory specific side-effects (asdf, direnv, etc)
command_string.push_str(&format!("cd '{}';", directory.display())); command_string.push_str(&format!("cd '{}';", directory.display()));
command_string.push_str(&format!("{} --printenv {}", zed_path, redir)); command_string.push_str(&format!(
"{}{} --printenv {}",
command_prefix, zed_path, redir
));
command.args(["-i", "-c", &command_string]); command.args(["-i", "-c", &command_string]);
super::set_pre_exec_to_start_new_session(&mut command); super::set_pre_exec_to_start_new_session(&mut command);
let (env_output, process_output) = spawn_and_read_fd(command, ENV_OUTPUT_FD)?; let (env_output, process_output) = spawn_and_read_fd(command, fd_num)?;
let env_output = String::from_utf8_lossy(&env_output); let env_output = String::from_utf8_lossy(&env_output);
anyhow::ensure!( anyhow::ensure!(

View file

@ -73,7 +73,7 @@ impl Workspace {
if let Some(terminal_provider) = self.terminal_provider.as_ref() { if let Some(terminal_provider) = self.terminal_provider.as_ref() {
let task_status = terminal_provider.spawn(spawn_in_terminal, window, cx); let task_status = terminal_provider.spawn(spawn_in_terminal, window, cx);
cx.background_spawn(async move { let task = cx.background_spawn(async move {
match task_status.await { match task_status.await {
Some(Ok(status)) => { Some(Ok(status)) => {
if status.success() { if status.success() {
@ -82,11 +82,11 @@ impl Workspace {
log::debug!("Task spawn failed, code: {:?}", status.code()); log::debug!("Task spawn failed, code: {:?}", status.code());
} }
} }
Some(Err(e)) => log::error!("Task spawn failed: {e}"), Some(Err(e)) => log::error!("Task spawn failed: {e:#}"),
None => log::debug!("Task spawn got cancelled"), None => log::debug!("Task spawn got cancelled"),
} }
}) });
.detach(); self.scheduled_tasks.push(task);
} }
} }

View file

@ -1097,6 +1097,7 @@ pub struct Workspace {
serialized_ssh_project: Option<SerializedSshProject>, serialized_ssh_project: Option<SerializedSshProject>,
_items_serializer: Task<Result<()>>, _items_serializer: Task<Result<()>>,
session_id: Option<String>, session_id: Option<String>,
scheduled_tasks: Vec<Task<()>>,
} }
impl EventEmitter<Event> for Workspace {} impl EventEmitter<Event> for Workspace {}
@ -1428,6 +1429,7 @@ impl Workspace {
_items_serializer, _items_serializer,
session_id: Some(session_id), session_id: Some(session_id),
serialized_ssh_project: None, serialized_ssh_project: None,
scheduled_tasks: Vec::new(),
} }
} }

View file

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

View file

@ -1 +1 @@
dev stable

View file

@ -1,10 +1,10 @@
use client::{Client, UserStore}; use client::{Client, DisableAiSettings, UserStore};
use collections::HashMap; use collections::HashMap;
use copilot::{Copilot, CopilotCompletionProvider}; use copilot::{Copilot, CopilotCompletionProvider};
use editor::Editor; use editor::Editor;
use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity}; use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity};
use language::language_settings::{EditPredictionProvider, all_language_settings}; use language::language_settings::{EditPredictionProvider, all_language_settings};
use settings::SettingsStore; use settings::{Settings as _, SettingsStore};
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{cell::RefCell, rc::Rc, sync::Arc}; use std::{cell::RefCell, rc::Rc, sync::Arc};
use supermaven::{Supermaven, SupermavenCompletionProvider}; use supermaven::{Supermaven, SupermavenCompletionProvider};
@ -195,16 +195,18 @@ fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut Context<Ed
}, },
)) ))
.detach(); .detach();
editor if !DisableAiSettings::get_global(cx).disable_ai {
.register_action(cx.listener( editor
|editor, .register_action(cx.listener(
_: &editor::actions::AcceptPartialCopilotSuggestion, |editor,
window: &mut Window, _: &editor::actions::AcceptPartialCopilotSuggestion,
cx: &mut Context<Editor>| { window: &mut Window,
editor.accept_partial_inline_completion(&Default::default(), window, cx); cx: &mut Context<Editor>| {
}, editor.accept_partial_inline_completion(&Default::default(), window, cx);
)) },
.detach(); ))
.detach();
}
} }
fn assign_edit_prediction_provider( fn assign_edit_prediction_provider(

View file

@ -578,7 +578,7 @@ windows-core = { version = "0.61" }
windows-numerics = { version = "0.2" } windows-numerics = { version = "0.2" }
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] } windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_UI_Shell"] } windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
[target.x86_64-pc-windows-msvc.build-dependencies] [target.x86_64-pc-windows-msvc.build-dependencies]
codespan-reporting = { version = "0.12" } codespan-reporting = { version = "0.12" }
@ -603,7 +603,7 @@ windows-core = { version = "0.61" }
windows-numerics = { version = "0.2" } windows-numerics = { version = "0.2" }
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] } windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_UI_Shell"] } windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
[target.x86_64-unknown-linux-musl.dependencies] [target.x86_64-unknown-linux-musl.dependencies]
aes = { version = "0.8", default-features = false, features = ["zeroize"] } aes = { version = "0.8", default-features = false, features = ["zeroize"] }