Compare commits

..

38 Commits

Author SHA1 Message Date
Piotr Osiewicz
5a120af767 Re-land Cargo.lock 2025-01-09 20:31:21 +01:00
Piotr Osiewicz
6dbed8deeb Merge branch 'main' into wasi-p2 2025-01-09 20:25:53 +01:00
Piotr Osiewicz
0f55ff6f5b Use cargo.lock from main (temporarily) 2025-01-09 20:24:14 +01:00
Mike Sun
9ea7ed8e0a Allow configuring spacing of project panel entries (#16255)
Release Notes:

- Added `project_panel.entry_spacing` setting to configure spacing
between entries in the project panel.

### Comfortable (default)
```json
  "project_panel": {
    "entry_spacing": "comfortable",
```
<img width="1582" alt="Screenshot 2024-08-14 at 5 50 41 PM"
src="https://github.com/user-attachments/assets/3411a82e-7517-4095-bf4a-bbf40000a7cb">

### Standard
```json
  "project_panel": {
    "entry_spacing": "standard",
```
<img width="1582" alt="Screenshot 2024-08-14 at 5 50 54 PM"
src="https://github.com/user-attachments/assets/2c13d799-c405-4301-8214-1cb3cc641c92">

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-09 17:57:52 +00:00
Angelk90
35d3d29bcf Add process ID to terminal tab tooltips (#21955)
Closes #12807

| Before | After |
|--------|--------|
| <img width="1336" alt="Screenshot 2025-01-09 at 2 14 15 PM"
src="https://github.com/user-attachments/assets/8396cf41-74eb-4b5c-89e3-287e4f2ddd1d"
/> | <img width="1336" alt="Screenshot 2025-01-09 at 2 13 34 PM"
src="https://github.com/user-attachments/assets/b39c51e8-fd2c-41fe-9493-396057bd71db"
/> |

Release Notes:

- Added the process ID (PID) to terminal tab tooltips.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-01-09 17:52:06 +00:00
renovate[bot]
9f9f3d215d Update Rust crate itertools to v0.14.0 (#22877)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [itertools](https://redirect.github.com/rust-itertools/itertools) |
dependencies | minor | `0.13` -> `0.14` |
| [itertools](https://redirect.github.com/rust-itertools/itertools) |
workspace.dependencies | minor | `0.13.0` -> `0.14.0` |

---

### Release Notes

<details>
<summary>rust-itertools/itertools (itertools)</summary>

###
[`v0.14.0`](https://redirect.github.com/rust-itertools/itertools/blob/HEAD/CHANGELOG.md#0140)

[Compare
Source](https://redirect.github.com/rust-itertools/itertools/compare/v0.13.0...v0.14.0)

##### Breaking

- Increased MSRV to 1.63.0
([#&#8203;960](https://redirect.github.com/rust-itertools/itertools/issues/960))
- Removed generic parameter from `cons_tuples`
([#&#8203;988](https://redirect.github.com/rust-itertools/itertools/issues/988))

##### Added

- Added `array_combinations`
([#&#8203;991](https://redirect.github.com/rust-itertools/itertools/issues/991))
- Added `k_smallest_relaxed` and variants
([#&#8203;925](https://redirect.github.com/rust-itertools/itertools/issues/925))
- Added `next_array` and `collect_array`
([#&#8203;560](https://redirect.github.com/rust-itertools/itertools/issues/560))
- Implemented `DoubleEndedIterator` for `FilterOk`
([#&#8203;948](https://redirect.github.com/rust-itertools/itertools/issues/948))
- Implemented `DoubleEndedIterator` for `FilterMapOk`
([#&#8203;950](https://redirect.github.com/rust-itertools/itertools/issues/950))

##### Changed

- Allow `Q: ?Sized` in `Itertools::contains`
([#&#8203;971](https://redirect.github.com/rust-itertools/itertools/issues/971))
- Improved hygiene of `chain!`
([#&#8203;943](https://redirect.github.com/rust-itertools/itertools/issues/943))
- Improved `into_group_map_by` documentation
([#&#8203;1000](https://redirect.github.com/rust-itertools/itertools/issues/1000))
- Improved `tree_reduce` documentation
([#&#8203;955](https://redirect.github.com/rust-itertools/itertools/issues/955))
- Improved discoverability of `merge_join_by`
([#&#8203;966](https://redirect.github.com/rust-itertools/itertools/issues/966))
- Improved discoverability of `take_while_inclusive`
([#&#8203;972](https://redirect.github.com/rust-itertools/itertools/issues/972))
- Improved documentation of `find_or_last` and `find_or_first`
([#&#8203;984](https://redirect.github.com/rust-itertools/itertools/issues/984))
- Prevented exponentially large type sizes in `tuple_combinations`
([#&#8203;945](https://redirect.github.com/rust-itertools/itertools/issues/945))
- Added `track_caller` attr for `asser_equal`
([#&#8203;976](https://redirect.github.com/rust-itertools/itertools/issues/976))

##### Notable Internal Changes

- Fixed clippy lints
([#&#8203;956](https://redirect.github.com/rust-itertools/itertools/issues/956),
[#&#8203;987](https://redirect.github.com/rust-itertools/itertools/issues/987),
[#&#8203;1008](https://redirect.github.com/rust-itertools/itertools/issues/1008))
- Addressed warnings within doctests
([#&#8203;964](https://redirect.github.com/rust-itertools/itertools/issues/964))
- CI: Run most tests with miri
([#&#8203;961](https://redirect.github.com/rust-itertools/itertools/issues/961))
- CI: Speed up "cargo-semver-checks" action
([#&#8203;938](https://redirect.github.com/rust-itertools/itertools/issues/938))
- Changed an instance of `default_features` in `Cargo.toml` to
`default-features`
([#&#8203;985](https://redirect.github.com/rust-itertools/itertools/issues/985))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS45Mi4wIiwidXBkYXRlZEluVmVyIjoiMzkuOTIuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 17:48:25 +00:00
Marshall Bowers
4aa4a40e2f extension: Fix manifest filename in error message (#22906)
This PR fixes the incorrect filename for the extension manifest being
used in an error message.

It should be `extension.toml` and not `extension.json`.

Release Notes:

- N/A
2025-01-09 17:38:46 +00:00
Danilo Leal
5c239be757 pane: Add ability to use custom tooltip content (#22879)
This PR is an alternate version of
https://github.com/zed-industries/zed/pull/22850, but now using a
similar approach to the existing `tab_content` and `tab_content_text`,
where `tab_tooltip_content` refers to the existing `tab_tooltip_text` if
there's no custom tooltip content/trait defined, meaning it will
simplify render the text/string content in this case.

This is all motivated by
https://github.com/zed-industries/zed/pull/21955, as we want to pull off
the ability to add custom content to a terminal tab tooltip.

Release Notes:

- N/A
2025-01-09 15:34:30 +00:00
Antonio Scandurra
e64a56ffad Animate Zeta button while generating completions (#22899)
Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-09 15:24:35 +00:00
Richard Feldman
7d905d0791 assistant2: Add "Copy code" button to code blocks (#22866)
Here's what it looks like, including the "Copy" hover text in one case:


![screenshot](https://github.com/user-attachments/assets/c8d27205-9650-493d-bd3c-a8c7beb142f9)


Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-01-09 14:32:42 +00:00
Antonio Scandurra
a8ef0f2426 Include outline when predicting edits with Zeta (#22895)
Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-09 14:26:33 +00:00
Antonio Scandurra
341972c79c Introduce UI affordances to make enabling/disabling inline completions easier (#22894)
Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-09 13:33:30 +00:00
Thorsten Ball
38fbc73ac4 Improve handling tab when inline completion is visible (#22892)
This changes the behaviour of `<tab>` when inline completion is visible.
When the cursor is before the suggested indentation level, accepting a
completion should just indent.

cc @nathansobo @maxdeviant 

Release Notes:

- Changed the behavior of `<tab>` at start of line when an inline
completion (Copilot, Supermaven, ...) is visible. If the cursor is
before the suggested indentation, `<tab>` now indents the line instead
of accepting the visible completion.

Co-authored-by: Antonio <antonio@zed.dev>
2025-01-09 12:44:52 +00:00
Kirill Bulatov
6c50659c30 Do not serialize workspace for item activations with no focus changes (#22891)
Follow-up of https://github.com/zed-industries/zed/pull/22730

Fixes excessive workspace serialization, when scrolling over outline
items in the outline panel: the panel will move the caret (selection)
over the file, following its outlines, causing the same item to be
re-activated over and over.


7a7cef2dd1/crates/workspace/src/persistence/model.rs (L257-L268)

does not seem to use position within an item, just the fact whether the
item is active or not:


7a7cef2dd1/crates/workspace/src/persistence/model.rs (L511-L517)

so, stop serializing the workspace state if no focus changes were made,
or the pane activated is the same.

Release Notes:

- N/A
2025-01-09 11:58:10 +00:00
Kirill Bulatov
a0284a272b Fix outline items navigation (#22890)
* Follows-up https://github.com/zed-industries/zed/pull/22224 , by
adjusting `impl PartialEq for OutlineEntryOutline` to compare outline
items' values too.
Before that, all outline items from the same excerpt were considered
equal.

Adds a test for this

* Stops re-revealing items in the outline panel, when it's focused: now,
when someone scrolls over outline panel items, there is no extra work
happening: the "revealed" item is the one scrolled to

Release Notes:

- Fixed outline items not scrolling properly
2025-01-09 10:25:02 +00:00
Michael Sloan
af1a3cbaac Make completion menu entries mutable (#22880)
Release Notes:

- N/A
2025-01-09 01:21:56 +00:00
Michael Sloan
05bc6b2abd assistant2: Split out implementation of Context::snapshot (#22878)
Release Notes:

- N/A
2025-01-09 00:25:16 +00:00
Kirill Bulatov
6f2b88239b Use distinct carets for line number hovers (#22836)
Release Notes:

- N/A
2025-01-08 23:51:07 +00:00
Matt Prodani
a9d2628c05 Update suggest_edits prompt to clarify usage of <old_text> when using update/create operations (#22341)
Update `suggest_edits` prompt to clarify usage of `<old_text>` when
using update/create operations using update/create operations.

- Add a mention that `old_text` is required for all but create.
- Change definition of `create` operation to also mean overwrite, as
some models heavily prefer rewrites.
- Remove mention of `If this tag is not specified, then the entire file
will be used as the range.` which is not current behavior.


Closes #22340

Release Notes:

- N/A (not sure if this requires a release note)
2025-01-08 23:45:15 +00:00
renovate[bot]
a038d61940 Update serde monorepo to v1.0.217 (#22872)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde](https://serde.rs)
([source](https://redirect.github.com/serde-rs/serde)) | dependencies |
patch | `1.0.216` -> `1.0.217` |
| [serde](https://serde.rs)
([source](https://redirect.github.com/serde-rs/serde)) |
workspace.dependencies | patch | `1.0.216` -> `1.0.217` |
| [serde_derive](https://serde.rs)
([source](https://redirect.github.com/serde-rs/serde)) |
workspace.dependencies | patch | `1.0.216` -> `1.0.217` |

---

### Release Notes

<details>
<summary>serde-rs/serde (serde)</summary>

###
[`v1.0.217`](https://redirect.github.com/serde-rs/serde/releases/tag/v1.0.217)

[Compare
Source](https://redirect.github.com/serde-rs/serde/compare/v1.0.216...v1.0.217)

- Support serializing externally tagged unit variant inside flattened
field
([#&#8203;2786](https://redirect.github.com/serde-rs/serde/issues/2786),
thanks [@&#8203;Mingun](https://redirect.github.com/Mingun))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS45Mi4wIiwidXBkYXRlZEluVmVyIjoiMzkuOTIuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 23:23:50 +00:00
Cole Miller
1d8bd151b7 Fix double read panic in nav history (#22754)
This one seems to be triggered when the assistant's
`View<ContextEditor>` is leased during the call into
`NavHistory::for_each_entry`, which then tries to read it again through
the `ItemHandle` interface. Fix it by skipping entries that can't be
read in the history iteration.

Release Notes:

- N/A
2025-01-08 23:05:34 +00:00
renovate[bot]
ef583e6b5a Update Rust crate open to v5.3.2 (#22862)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [open](https://redirect.github.com/Byron/open-rs) | dependencies |
patch | `5.3.1` -> `5.3.2` |

---

### Release Notes

<details>
<summary>Byron/open-rs (open)</summary>

###
[`v5.3.2`](https://redirect.github.com/Byron/open-rs/blob/HEAD/changelog.md#532-2025-01-05)

[Compare
Source](https://redirect.github.com/Byron/open-rs/compare/v5.3.1...v5.3.2)

##### Bug Fixes

- <csr-id-c452a8c4e56c3726431d8a4a77ad910bc8ae3ecb/> fix `that_detached`
for UNC path of a directory

##### Commit Statistics

<csr-read-only-do-not-edit/>

- 3 commits contributed to the release over the course of 1 calendar
day.
-   51 days passed between releases.
- 1 commit was understood as
[conventional](https://www.conventionalcommits.org).
-   0 issues like '(#ID)' were seen in commit messages

##### Commit Details

<csr-read-only-do-not-edit/>

<details><summary>view details</summary>

-   **Uncategorized**
- Merge pull request
[#&#8203;107](https://redirect.github.com/Byron/open-rs/issues/107) from
amrbashir/fix/windows/remove-unc-and-fallback-on-error
([`472ce26`](472ce262c8))
- Fix `that_detached` for UNC path of a directory
([`c452a8c`](c452a8c4e5))
- Merge pull request
[#&#8203;79](https://redirect.github.com/Byron/open-rs/issues/79) from
Byron/better-docs
([`2646ff8`](2646ff820c))

</details>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS45Mi4wIiwidXBkYXRlZEluVmVyIjoiMzkuOTIuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 23:00:00 +00:00
Marshall Bowers
a4dd92fe06 collab: Prevent users from creating a new subscription when they have overdue subscriptions (#22870)
This PR adjusts the create billing subscription endpoint to prevent
initiating a checkout flow when a user has existing subscriptions that
are overdue.

A subscription is considered "overdue" when either:

- The status is `past_due`
- The status is `canceled` and the cancellation reason is
`payment_failed`

In Stripe, when a subscription has failed payment a certain number of
times, it is canceled with a reason of `payment_failed`. However, today
there is nothing stopping someone from simply creating a new
subscription without paying the outstanding invoices. With this change a
user will need to reconcile their outstanding invoices before they can
sign up for a new subscription.

Release Notes:

- N/A
2025-01-08 22:50:48 +00:00
Michael Sloan
a0fca24e3f assistant2: Add live context type and use in message editor (#22865)
Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
Co-authored-by: Marshall <marshall@zed.dev>
2025-01-08 21:47:58 +00:00
renovate[bot]
5d8ef94c86 Update Rust crate serde_json to v1.0.135 (#22863)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_json](https://redirect.github.com/serde-rs/json) | dependencies
| patch | `1.0.134` -> `1.0.135` |
| [serde_json](https://redirect.github.com/serde-rs/json) |
workspace.dependencies | patch | `1.0.134` -> `1.0.135` |

---

### Release Notes

<details>
<summary>serde-rs/json (serde_json)</summary>

###
[`v1.0.135`](https://redirect.github.com/serde-rs/json/releases/tag/v1.0.135)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/v1.0.134...v1.0.135)

- Add serde_json::Map::into_values method
([#&#8203;1226](https://redirect.github.com/serde-rs/json/issues/1226),
thanks [@&#8203;tisonkun](https://redirect.github.com/tisonkun))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS45Mi4wIiwidXBkYXRlZEluVmVyIjoiMzkuOTIuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 21:20:47 +00:00
Michael Sloan
fe35695b13 Release syntax aware heuristic expansion of diagnostic excerpts (#22858)
Implementation PR was #21942

Release Notes:

- Improved diagnostic excerpts by using syntactic info to determine the
context lines to show.
2025-01-08 20:53:52 +00:00
Conrad Irwin
9ef454d7eb Add section on how to disable "Verifying..." popup when developing on macOS (#22857)
Release Notes:

- N/A
2025-01-08 20:00:41 +00:00
Marshall Bowers
7e39023ea5 assistant2: Push logic for adding thread context down into the ContextStore (#22855)
This PR takes the logic for adding thread context out of the
`ThreadContextPicker` and pushes it down into the `ContextStore`.

Release Notes:

- N/A
2025-01-08 19:54:54 +00:00
Marshall Bowers
b78396505f collab: Record cancellation reason on billing subscriptions (#22853)
This PR updates the `billing_subscriptions` in the database to record
the cancellation reason from Stripe.

We're primarily interested in this so we can check for subscriptions
that were canceled for being `past_due`.

Release Notes:

- N/A
2025-01-08 19:38:10 +00:00
Marshall Bowers
69dde8e31d assistant2: Push logic for adding directory context down into the ContextStore (#22852)
This PR takes the logic for adding file context out of the
`DirectoryContextPicker` and pushes it down into the `ContextStore`.

Release Notes:

- N/A
2025-01-08 18:43:44 +00:00
Marshall Bowers
86f5bb1cc0 assistant2: Push logic for adding file context down into the ContextStore (#22846)
This PR takes the logic for adding file context out of the
`FileContextPicker` and pushes it down into the `ContextStore`.

Release Notes:

- N/A
2025-01-08 17:46:49 +00:00
Cole Miller
d855eb3acb Update reference to editor::OpenFile in keymap (#22827)
Follow-up to #22494

Release Notes:

- N/A
2025-01-08 17:42:22 +00:00
tims
632372a4f1 linux: Fix issue with project-specific env not being found via .envrc (direnv) (#22803)
Closes #18908

This PR started as a cleanup of redundant logic for setting up envs when
Zed is launched as a desktop entry on Linux. More on this can be read
[here](https://github.com/zed-industries/zed/pull/22335#issuecomment-2574726377).
The TLDR is that desktop entries on Linux sometimes might not have the
correct envs (as they don't `cwd` into your project directory). To
address this, we initially tried to fix it by loading the default shell
and its env vars.

However, a better solution, as recommended by @mrnugget, is to pass
`env` as `None`. Internally, if `env` is `None`, it falls back to the
project's working dir envs. This removes the need to manually load the
envs and is cleaner.

Additionally, it also fixes an issue with Zed not loading
project-specific envs because now we are actually doing so (albeit
unintentionally?).

I don't have macOS to test, but I believe this is not an issue on macOS
since it uses the Zed binary instead of the CLI, which essentially sets
the CLI `env` to `None` automatically.

Before:

Here, I have `/home/tims/go/bin` set up in `.envrc`, which only loads in
that project directory.

When launching Zed via the CLI in the project directory, notice
`/home/tims/go/bin` is in the `PATH`. As a result, we use the
user-installed `gopls` server.

```sh
[INFO] attempting to start language server "gopls", path: "/home/tims/temp/go-proj", id: 1
[INFO] using project environment variables from CLI. PATH="/home/tims/go/bin:/usr/local/go/bin"
[INFO] found user-installed language server for gopls. path: "/home/tims/go/bin/gopls", arguments: ["-mode=stdio"]
[INFO] starting language server process. binary path: "/home/tims/go/bin/gopls", working directory: "/home/tims/temp/go-proj", args: ["-mode=stdio"]
```

However, when using the desktop entry and attempting to load envs from
the default shell, notice `/home/tims/go/bin` is no longer there since
it's not in the project directory. Zed cannot find the user-installed
language server and starts downloading its own `gopls`.

```sh
[INFO] attempting to start language server "gopls", path: "/home/tims/temp/go-proj", id: 1
[INFO] using project environment variables from CLI. PATH="/usr/local/go/bin"
[INFO] fetching latest version of language server "gopls"
[INFO] downloading language server "gopls"
[INFO] starting language server process. binary path: "/home/tims/.local/share/zed/languages/gopls/gopls_0.17.1_go_1.23.4", working directory: "/home/tims/temp/go-proj", args: ["-mode=stdio"]
```

After: 

When using the desktop entry, we pass the CLI env as `None`. For the
language server, it falls back to the project directory envs. Result,
Zed finds the user-installed language server.

```sh
[INFO] attempting to start language server "gopls", path: "/home/tims/temp/go-proj", id: 1
[INFO] using project environment variables shell launched in "/home/tims/temp/go-proj". PATH="/home/tims/go/bin:/usr/local/go/bin"
[INFO] found user-installed language server for gopls. path: "/home/tims/go/bin/gopls", arguments: ["-mode=stdio"]
[INFO] starting language server process. binary path: "/home/tims/go/bin/gopls", working directory: "/home/tims/temp/go-proj", args: ["-mode=stdio"]
```

Release Notes:

- Fixed issue with project-specific env not being found via .envrc
(direnv) on Linux
2025-01-08 16:38:19 +00:00
Thorsten Ball
a248981fca zeta: Validate completion responses for markers (#22840)
Check for markers and how many there are to avoid markers showing up in
completions.

Release Notes:

- N/A
2025-01-08 16:34:05 +00:00
Vladimir Varankin
9850bf8022 Fix extend selection shortcuts in JetBrains keymap on macOS (#22814)
Fixups https://github.com/zed-industries/zed/pull/20199

As mentioned in [the post-merge comment][1], the original change was
wrong. The JetBrains IDEs use <kbd>⌥</kbd> (option) key on macOS for the
shortcuts, which corresponds to the <kbd>alt</kbd> key in the keymap
config.

Release Notes:

- Fixed extend/shrink selection in JetBrains keymap on macOS

[1]:
https://github.com/zed-industries/zed/pull/20199#issuecomment-2468136572
2025-01-08 16:01:21 +00:00
Peter Tripp
83889bb235 Bump Zed to v0.170 (#22838) 2025-01-08 11:02:44 -05:00
Piotr Osiewicz
76d8623b86 Update crates/extension/src/extension_builder.rs 2024-11-18 12:59:26 +01:00
Piotr Osiewicz
c0b751be1f extension: Bump WASI to p2 2024-11-18 12:57:56 +01:00
87 changed files with 2449 additions and 1717 deletions

View File

@@ -10,6 +10,7 @@ on:
pull_request:
branches:
- "**"
merge_group:
concurrency:
# Allow only one workflow per any non-`main` branch.
@@ -23,6 +24,31 @@ env:
RUSTFLAGS: "-D warnings"
jobs:
check_docs_only:
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.check_changes.outputs.docs_only }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Check for non-docs changes
id: check_changes
run: |
if [ "${{ github.event_name }}" == "merge_group" ]; then
# When we're running in a merge queue, never assume that the changes
# are docs-only, as there could be other PRs in the group that
# contain non-docs changes.
echo "Running in the merge queue"
echo "docs_only=false" >> $GITHUB_OUTPUT
elif git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -qvE '^docs/'; then
echo "Detected non-docs changes"
echo "docs_only=false" >> $GITHUB_OUTPUT
else
echo "Docs-only change"
echo "docs_only=true" >> $GITHUB_OUTPUT
fi
migration_checks:
name: Check Postgres and Protobuf migrations, mergability
if: github.repository_owner == 'zed-industries'
@@ -95,6 +121,7 @@ jobs:
runs-on:
- self-hosted
- test
needs: check_docs_only
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -102,29 +129,35 @@ jobs:
clean: false
- name: cargo clippy
if: needs.check_docs_only.outputs.docs_only == 'false'
run: ./script/clippy
- name: Check unused dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: bnjbvr/cargo-machete@main
- name: Check licenses
if: needs.check_docs_only.outputs.docs_only == 'false'
run: |
script/check-licenses
script/generate-licenses /tmp/zed_licenses_output
- name: Check for new vulnerable dependencies
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' && needs.check_docs_only.outputs.docs_only == 'false'
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
with:
license-check: false
- name: Run tests
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: ./.github/actions/run_tests
- name: Build collab
if: needs.check_docs_only.outputs.docs_only == 'false'
run: cargo build -p collab
- name: Build other binaries and features
if: needs.check_docs_only.outputs.docs_only == 'false'
run: |
cargo build --workspace --bins --all-features
cargo check -p gpui --features "macos-blade"
@@ -138,6 +171,7 @@ jobs:
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204
needs: check_docs_only
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
@@ -148,21 +182,26 @@ jobs:
clean: false
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
- name: Install Linux dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
run: ./script/linux
- name: cargo clippy
if: needs.check_docs_only.outputs.docs_only == 'false'
run: ./script/clippy
- name: Run tests
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: ./.github/actions/run_tests
- name: Build other binaries and features
if: needs.check_docs_only.outputs.docs_only == 'false'
run: |
cargo build -p zed
cargo check -p workspace
@@ -173,6 +212,7 @@ jobs:
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204
needs: check_docs_only
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
@@ -183,15 +223,18 @@ jobs:
clean: false
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
- name: Install Clang & Mold
if: needs.check_docs_only.outputs.docs_only == 'false'
run: ./script/remote-server && ./script/install-mold 2.34.0
- name: Build Remote Server
if: needs.check_docs_only.outputs.docs_only == 'false'
run: cargo build -p remote_server
# todo(windows): Actually run the tests
@@ -200,6 +243,7 @@ jobs:
name: (Windows) Run Clippy and tests
if: github.repository_owner == 'zed-industries'
runs-on: hosted-windows-1
needs: check_docs_only
steps:
# more info here:- https://github.com/rust-lang/cargo/issues/13020
- name: Enable longer pathnames for git
@@ -210,20 +254,23 @@ jobs:
clean: false
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "github"
- name: cargo clippy
if: needs.check_docs_only.outputs.docs_only == 'false'
# Windows can't run shell scripts, so we need to use `cargo xtask`.
run: cargo xtask clippy
- name: Build Zed
if: needs.check_docs_only.outputs.docs_only == 'false'
run: cargo build
bundle-mac:
timeout-minutes: 120
timeout-minutes: 60
name: Create a macOS bundle
runs-on:
- self-hosted
@@ -312,9 +359,9 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-linux-x86_x64:
bundle-linux:
timeout-minutes: 60
name: Linux x86_x64 release bundle
name: Create a Linux bundle
runs-on:
- buildjet-16vcpu-ubuntu-2004
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
@@ -362,7 +409,7 @@ jobs:
bundle-linux-aarch64: # this runs on ubuntu22.04
timeout-minutes: 60
name: Linux arm64 release bundle
name: Create arm64 Linux bundle
runs-on:
- buildjet-16vcpu-ubuntu-2204-arm
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
@@ -411,7 +458,7 @@ jobs:
auto-release-preview:
name: Auto release preview
if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }}
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64]
needs: [bundle-mac, bundle-linux, bundle-linux-aarch64]
runs-on:
- self-hosted
- bundle

View File

@@ -7,6 +7,7 @@ on:
push:
branches:
- main
merge_group:
jobs:
check_formatting:

733
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,6 @@ members = [
"crates/feedback",
"crates/file_finder",
"crates/file_icons",
"crates/fireworks",
"crates/fs",
"crates/fsevent",
"crates/fuzzy",
@@ -223,7 +222,6 @@ feature_flags = { path = "crates/feature_flags" }
feedback = { path = "crates/feedback" }
file_finder = { path = "crates/file_finder" }
file_icons = { path = "crates/file_icons" }
fireworks = { path = "crates/fireworks" }
fs = { path = "crates/fs" }
fsevent = { path = "crates/fsevent" }
fuzzy = { path = "crates/fuzzy" }
@@ -393,7 +391,7 @@ ignore = "0.4.22"
image = "0.25.1"
indexmap = { version = "2.7.0", features = ["serde"] }
indoc = "2"
itertools = "0.13.0"
itertools = "0.14.0"
jsonwebtoken = "9.3"
jupyter-protocol = { version = "0.5.0" }
jupyter-websocket-client = { version = "0.8.0" }
@@ -514,14 +512,14 @@ url = "2.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
wasmparser = "0.215"
wasm-encoder = "0.215"
wasmtime = { version = "24", default-features = false, features = [
wasmtime = { version = "26", default-features = false, features = [
"async",
"demangle",
"runtime",
"cranelift",
"component-model",
] }
wasmtime-wasi = "24"
wasmtime-wasi = "26"
which = "6.0.0"
wit-component = "0.201"
zstd = "0.11"

View File

@@ -805,8 +805,7 @@
"context": "RateCompletionModal",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion",
"cmd-enter": "zeta::ThumbsUp",
"shift-down": "zeta::NextEdit",
"shift-up": "zeta::PreviousEdit",
"right": "zeta::PreviewCompletion"

View File

@@ -55,7 +55,7 @@
}
},
{
"context": "Workspace",
"context": "Workspace && !Terminal",
"bindings": {
"ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal
"ctrl-x 5 0": "workspace::CloseWindow", // delete-frame
@@ -72,18 +72,6 @@
"ctrl-x s": "workspace::SaveAll" // save-some-buffers
}
},
{
// Workaround to enable using emacs in the Zed terminal.
// Unbind so Zed ignores these keys and lets emacs handle them.
"context": "Terminal",
"bindings": {
"ctrl-x ctrl-c": null, // save-buffers-kill-terminal
"ctrl-x ctrl-f": null, // find-file
"ctrl-x ctrl-s": null, // save-buffer
"ctrl-x ctrl-w": null, // write-file
"ctrl-x s": null // save-some-buffers
}
},
{
"context": "BufferSearchBar > Editor",
"bindings": {

View File

@@ -24,8 +24,8 @@
"ctrl-g": ["editor::SelectNext", { "replace_newest": false }],
"ctrl-cmd-g": ["editor::SelectPrevious", { "replace_newest": false }],
"cmd-/": ["editor::ToggleComments", { "advance_downwards": true }],
"cmd-up": "editor::SelectLargerSyntaxNode",
"cmd-down": "editor::SelectSmallerSyntaxNode",
"alt-up": "editor::SelectLargerSyntaxNode",
"alt-down": "editor::SelectSmallerSyntaxNode",
"shift-alt-up": "editor::MoveLineUp",
"shift-alt-down": "editor::MoveLineDown",
"cmd-alt-l": "editor::Format",

View File

@@ -13,15 +13,15 @@ You must describe the change using the following XML structure:
- <description> (optional) - An arbitrarily-long comment that describes the purpose
of this edit.
- <old_text> (optional) - An excerpt from the file's current contents that uniquely
identifies a range within the file where the edit should occur. If this tag is not
specified, then the entire file will be used as the range.
identifies a range within the file where the edit should occur. Required for all operations
except `create`.
- <new_text> (required) - The new text to insert into the file.
- <operation> (required) - The type of change that should occur at the given range
of the file. Must be one of the following values:
- `update`: Replaces the entire range with the new text.
- `insert_before`: Inserts the new text before the range.
- `insert_after`: Inserts new text after the range.
- `create`: Creates a new file with the given path and the new text.
- `create`: Creates or overwrites a file with the given path and the new text.
- `delete`: Deletes the specified range from the file.
<guidelines>

View File

@@ -372,6 +372,8 @@
"default_width": 240,
// Where to dock the project panel. Can be 'left' or 'right'.
"dock": "left",
// Spacing between worktree entries in the project panel. Can be 'comfortable' or 'standard'.
"entry_spacing": "comfortable",
// Whether to show file icons in the project panel.
"file_icons": true,
// Whether to show folder icons or chevrons for directories in the project panel.

View File

@@ -595,7 +595,7 @@ impl AssistantPanel {
true
}
pane::Event::ActivateItem { local } => {
pane::Event::ActivateItem { local, .. } => {
if *local {
self.workspace
.update(cx, |workspace, cx| {
@@ -4272,6 +4272,10 @@ impl Item for ContextEditor {
None
}
}
fn include_in_nav_history() -> bool {
false
}
}
impl SearchableItem for ContextEditor {

View File

@@ -16,9 +16,7 @@ use editor::{
EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot,
ToOffset as _, ToPoint,
};
use feature_flags::{
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedPro,
};
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
use fs::Fs;
use futures::{
channel::mpsc,
@@ -75,16 +73,7 @@ pub fn init(
let workspace = cx.view().clone();
InlineAssistant::update_global(cx, |inline_assistant, cx| {
inline_assistant.register_workspace(&workspace, cx)
});
cx.observe_flag::<Assistant2FeatureFlag, _>({
|is_assistant2_enabled, _view, cx| {
InlineAssistant::update_global(cx, |inline_assistant, _cx| {
inline_assistant.is_assistant2_enabled = is_assistant2_enabled;
});
}
})
.detach();
})
.detach();
}
@@ -102,7 +91,6 @@ pub struct InlineAssistant {
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
fs: Arc<dyn Fs>,
is_assistant2_enabled: bool,
}
impl Global for InlineAssistant {}
@@ -124,7 +112,6 @@ impl InlineAssistant {
prompt_builder,
telemetry,
fs,
is_assistant2_enabled: false,
}
}
@@ -185,22 +172,15 @@ impl InlineAssistant {
item: &dyn ItemHandle,
cx: &mut WindowContext,
) {
let is_assistant2_enabled = self.is_assistant2_enabled;
if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
if is_assistant2_enabled {
editor
.remove_code_action_provider(ASSISTANT_CODE_ACTION_PROVIDER_ID.into(), cx);
} else {
editor.add_code_action_provider(
Rc::new(AssistantCodeActionProvider {
editor: cx.view().downgrade(),
workspace: workspace.downgrade(),
}),
cx,
);
}
editor.push_code_action_provider(
Rc::new(AssistantCodeActionProvider {
editor: cx.view().downgrade(),
workspace: workspace.downgrade(),
}),
cx,
);
});
}
}
@@ -3446,13 +3426,7 @@ struct AssistantCodeActionProvider {
workspace: WeakView<Workspace>,
}
const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant";
impl CodeActionProvider for AssistantCodeActionProvider {
fn id(&self) -> Arc<str> {
ASSISTANT_CODE_ACTION_PROVIDER_ID.into()
}
fn code_actions(
&self,
buffer: &Model<Buffer>,

View File

@@ -19,6 +19,7 @@ assets.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
client.workspace = true
clock.workspace = true
chrono.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
@@ -33,6 +34,7 @@ gpui.workspace = true
handlebars.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true

View File

@@ -282,11 +282,13 @@ impl ActiveThread {
.child(div().p_2p5().text_ui(cx).child(markdown.clone()))
.when_some(context, |parent, context| {
if !context.is_empty() {
parent.child(h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
context.iter().map(|context| {
ContextPill::new_added(context.clone(), false, None)
}),
))
parent.child(
h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
context.into_iter().map(|context| {
ContextPill::new_added(context, false, None)
}),
),
)
} else {
parent
}

View File

@@ -421,8 +421,7 @@ impl CodegenAlternative {
};
if let Some(context_store) = &self.context_store {
let context = context_store.update(cx, |this, _cx| this.context().clone());
attach_context_to_message(&mut request_message, context);
attach_context_to_message(&mut request_message, context_store.read(cx).snapshot(cx));
}
request_message.content.push(prompt.into());
@@ -1053,7 +1052,7 @@ mod tests {
stream::{self},
Stream,
};
use gpui::{Context, TestAppContext};
use gpui::TestAppContext;
use indoc::indoc;
use language::{
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,

View File

@@ -1,8 +1,17 @@
use gpui::SharedString;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use collections::BTreeMap;
use gpui::{AppContext, Model, SharedString};
use language::Buffer;
use language_model::{LanguageModelRequestMessage, MessageContent};
use serde::{Deserialize, Serialize};
use text::BufferId;
use util::post_inc;
use crate::thread::Thread;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct ContextId(pub(crate) usize);
@@ -14,16 +23,17 @@ impl ContextId {
/// Some context attached to a message in a thread.
#[derive(Debug, Clone)]
pub struct Context {
pub struct ContextSnapshot {
pub id: ContextId,
pub name: SharedString,
pub parent: Option<SharedString>,
pub tooltip: Option<SharedString>,
pub kind: ContextKind,
/// Text to send to the model. This is not refreshed by `snapshot`.
pub text: SharedString,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContextKind {
File,
Directory,
@@ -31,18 +41,156 @@ pub enum ContextKind {
Thread,
}
#[derive(Debug)]
pub enum Context {
File(FileContext),
Directory(DirectoryContext),
FetchedUrl(FetchedUrlContext),
Thread(ThreadContext),
}
impl Context {
pub fn id(&self) -> ContextId {
match self {
Self::File(file) => file.id,
Self::Directory(directory) => directory.snapshot.id,
Self::FetchedUrl(url) => url.id,
Self::Thread(thread) => thread.id,
}
}
}
// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
// the context from the message editor in this case.
#[derive(Debug)]
pub struct FileContext {
pub id: ContextId,
pub buffer: Model<Buffer>,
#[allow(unused)]
pub version: clock::Global,
pub text: SharedString,
}
#[derive(Debug)]
pub struct DirectoryContext {
#[allow(unused)]
pub path: Rc<Path>,
// TODO: The choice to make this a BTreeMap was a result of use in a version of
// ContextStore::will_include_buffer before I realized that the path logic should be used there
// too.
#[allow(unused)]
pub buffers: BTreeMap<BufferId, (Model<Buffer>, clock::Global)>,
pub snapshot: ContextSnapshot,
}
#[derive(Debug)]
pub struct FetchedUrlContext {
pub id: ContextId,
pub url: SharedString,
pub text: SharedString,
}
// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
// explicitly or have a WeakModel<Thread> and remove during snapshot.
#[derive(Debug)]
pub struct ThreadContext {
pub id: ContextId,
pub thread: Model<Thread>,
pub text: SharedString,
}
impl Context {
pub fn snapshot(&self, cx: &AppContext) -> Option<ContextSnapshot> {
match &self {
Self::File(file_context) => file_context.snapshot(cx),
Self::Directory(directory_context) => Some(directory_context.snapshot()),
Self::FetchedUrl(fetched_url_context) => Some(fetched_url_context.snapshot()),
Self::Thread(thread_context) => Some(thread_context.snapshot(cx)),
}
}
}
impl FileContext {
pub fn path(&self, cx: &AppContext) -> Option<Arc<Path>> {
let buffer = self.buffer.read(cx);
if let Some(file) = buffer.file() {
Some(file.path().clone())
} else {
log::error!("Buffer that had a path unexpectedly no longer has a path.");
None
}
}
pub fn snapshot(&self, cx: &AppContext) -> Option<ContextSnapshot> {
let path = self.path(cx)?;
let full_path: SharedString = path.to_string_lossy().into_owned().into();
let name = match path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => full_path.clone(),
};
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().into_owned().into());
Some(ContextSnapshot {
id: self.id,
name,
parent,
tooltip: Some(full_path),
kind: ContextKind::File,
text: self.text.clone(),
})
}
}
impl DirectoryContext {
pub fn snapshot(&self) -> ContextSnapshot {
self.snapshot.clone()
}
}
impl FetchedUrlContext {
pub fn snapshot(&self) -> ContextSnapshot {
ContextSnapshot {
id: self.id,
name: self.url.clone(),
parent: None,
tooltip: None,
kind: ContextKind::FetchedUrl,
text: self.text.clone(),
}
}
}
impl ThreadContext {
pub fn snapshot(&self, cx: &AppContext) -> ContextSnapshot {
let thread = self.thread.read(cx);
ContextSnapshot {
id: self.id,
name: thread.summary().unwrap_or("New thread".into()),
parent: None,
tooltip: None,
kind: ContextKind::Thread,
text: self.text.clone(),
}
}
}
pub fn attach_context_to_message(
message: &mut LanguageModelRequestMessage,
context: impl IntoIterator<Item = Context>,
contexts: impl Iterator<Item = ContextSnapshot>,
) {
let mut file_context = String::new();
let mut directory_context = String::new();
let mut fetch_context = String::new();
let mut thread_context = String::new();
for context in context.into_iter() {
for context in contexts {
match context.kind {
ContextKind::File { .. } => {
ContextKind::File => {
file_context.push_str(&context.text);
file_context.push('\n');
}
@@ -56,7 +204,7 @@ pub fn attach_context_to_message(
fetch_context.push_str(&context.text);
fetch_context.push('\n');
}
ContextKind::Thread => {
ContextKind::Thread { .. } => {
thread_context.push_str(&context.name);
thread_context.push('\n');
thread_context.push_str(&context.text);

View File

@@ -2,17 +2,16 @@ use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::anyhow;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, Worktree, WorktreeId};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{prelude::*, ListItem};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::{push_fenced_codeblock, ContextStore};
use crate::context_store::ContextStore;
pub struct DirectoryContextPicker {
picker: View<Picker<DirectoryContextPickerDelegate>>,
@@ -179,107 +178,45 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
return;
};
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
};
let Some(task) = self
.context_store
.update(cx, |context_store, cx| {
context_store.add_directory(project_path, cx)
})
.ok()
else {
return;
};
let path = mat.path.clone();
let already_included = self
.context_store
.update(cx, |context_store, _cx| {
if let Some(context_id) = context_store.included_directory(&path) {
context_store.remove_context(&context_id);
true
} else {
false
}
})
.unwrap_or(true);
if already_included {
return;
}
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
let workspace = self.workspace.clone();
let confirm_behavior = self.confirm_behavior;
cx.spawn(|this, mut cx| async move {
let worktree = project.update(&mut cx, |project, cx| {
project
.worktree_for_id(worktree_id, cx)
.ok_or_else(|| anyhow!("no worktree found for {worktree_id:?}"))
})??;
let files = worktree.update(&mut cx, |worktree, _cx| {
collect_files_in_path(worktree, &path)
})?;
let open_buffer_tasks = project.update(&mut cx, |project, cx| {
files
.into_iter()
.map(|file_path| {
project.open_buffer(
ProjectPath {
worktree_id,
path: file_path.clone(),
},
cx,
)
})
.collect::<Vec<_>>()
})?;
let buffers = futures::future::join_all(open_buffer_tasks).await;
this.update(&mut cx, |this, cx| {
let mut text = String::new();
let mut ok_count = 0;
for buffer in buffers.into_iter().flatten() {
let buffer = buffer.read(cx);
let path = buffer.file().map_or(&path, |file| file.path());
push_fenced_codeblock(&path, buffer.text(), &mut text);
ok_count += 1;
match task.await {
Ok(()) => {
this.update(&mut cx, |this, cx| match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(cx),
})?;
}
if ok_count == 0 {
Err(err) => {
let Some(workspace) = workspace.upgrade() else {
return anyhow::Ok(());
};
workspace.update(cx, |workspace, cx| {
workspace.show_error(
&anyhow::anyhow!(
"Could not read any text files from {}",
path.display()
),
cx,
);
});
return anyhow::Ok(());
}
this.delegate
.context_store
.update(cx, |context_store, _cx| {
context_store.insert_directory(&path, text);
workspace.update(&mut cx, |workspace, cx| {
workspace.show_error(&err, cx);
})?;
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(cx),
}
anyhow::Ok(())
})??;
}
anyhow::Ok(())
})
.detach_and_log_err(cx)
.detach_and_log_err(cx);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
@@ -303,7 +240,7 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
let added = self.context_store.upgrade().map_or(false, |context_store| {
context_store
.read(cx)
.included_directory(&path_match.path)
.includes_directory(&path_match.path)
.is_some()
});
@@ -327,17 +264,3 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
)
}
}
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
let mut files = Vec::new();
for entry in worktree.child_entries(path) {
if entry.is_dir() {
files.extend(collect_files_in_path(worktree, &entry.path));
} else if entry.is_file() {
files.push(entry.path.clone());
}
}
files
}

View File

@@ -82,10 +82,12 @@ impl FetchContextPickerDelegate {
}
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
let mut url = url.to_owned();
if !url.starts_with("https://") && !url.starts_with("http://") {
url = format!("https://{url}");
}
let prefixed_url = if !url.starts_with("https://") && !url.starts_with("http://") {
Some(format!("https://{url}"))
} else {
None
};
let url = prefixed_url.as_deref().unwrap_or(url);
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
@@ -200,7 +202,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
this.delegate
.context_store
.update(cx, |context_store, _cx| {
if context_store.included_url(&url).is_none() {
if context_store.includes_url(&url).is_none() {
context_store.insert_fetched_url(url, text);
}
})?;
@@ -234,7 +236,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let added = self.context_store.upgrade().map_or(false, |context_store| {
context_store.read(cx).included_url(&self.url).is_some()
context_store.read(cx).includes_url(&self.url).is_some()
});
Some(

View File

@@ -11,7 +11,7 @@ use util::ResultExt as _;
use workspace::Workspace;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::{ContextStore, IncludedFile};
use crate::context_store::{ContextStore, FileInclusion};
pub struct FileContextPicker {
picker: View<Picker<FileContextPickerDelegate>>,
@@ -193,81 +193,41 @@ impl PickerDelegate for FileContextPickerDelegate {
return;
};
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
};
let Some(task) = self
.context_store
.update(cx, |context_store, cx| {
context_store.add_file(project_path, cx)
})
.ok()
else {
return;
};
let path = mat.path.clone();
let already_included = self
.context_store
.update(cx, |context_store, _cx| {
match context_store.included_file(&path) {
Some(IncludedFile::Direct(context_id)) => {
context_store.remove_context(&context_id);
true
}
Some(IncludedFile::InDirectory(_)) => true,
None => false,
}
})
.unwrap_or(true);
if already_included {
return;
}
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
let workspace = self.workspace.clone();
let confirm_behavior = self.confirm_behavior;
cx.spawn(|this, mut cx| async move {
let Some(open_buffer_task) = project
.update(&mut cx, |project, cx| {
let project_path = ProjectPath {
worktree_id,
path: path.clone(),
};
let task = project.open_buffer(project_path, cx);
Some(task)
})
.ok()
.flatten()
else {
return anyhow::Ok(());
};
let result = open_buffer_task.await;
this.update(&mut cx, |this, cx| match result {
Ok(buffer) => {
this.delegate
.context_store
.update(cx, |context_store, cx| {
context_store.insert_file(buffer.read(cx));
})?;
match confirm_behavior {
match task.await {
Ok(()) => {
this.update(&mut cx, |this, cx| match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(cx),
}
anyhow::Ok(())
})?;
}
Err(err) => {
let Some(workspace) = workspace.upgrade() else {
return anyhow::Ok(());
};
workspace.update(cx, |workspace, cx| {
workspace.update(&mut cx, |workspace, cx| {
workspace.show_error(&err, cx);
});
anyhow::Ok(())
})?;
}
})??;
}
anyhow::Ok(())
})
@@ -315,10 +275,11 @@ impl PickerDelegate for FileContextPickerDelegate {
(file_name, Some(directory))
};
let added = self
.context_store
.upgrade()
.and_then(|context_store| context_store.read(cx).included_file(&path_match.path));
let added = self.context_store.upgrade().and_then(|context_store| {
context_store
.read(cx)
.will_include_file_path(&path_match.path, cx)
});
Some(
ListItem::new(ix)
@@ -335,7 +296,7 @@ impl PickerDelegate for FileContextPickerDelegate {
})),
)
.when_some(added, |el, added| match added {
IncludedFile::Direct(_) => el.end_slot(
FileInclusion::Direct(_) => el.end_slot(
h_flex()
.gap_1()
.child(
@@ -345,7 +306,7 @@ impl PickerDelegate for FileContextPickerDelegate {
)
.child(Label::new("Added").size(LabelSize::Small)),
),
IncludedFile::InDirectory(dir_name) => {
FileInclusion::InDirectory(dir_name) => {
let dir_name = dir_name.to_string_lossy().into_owned();
el.end_slot(

View File

@@ -167,13 +167,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
};
self.context_store
.update(cx, |context_store, cx| {
if let Some(context_id) = context_store.included_thread(&entry.id) {
context_store.remove_context(&context_id);
} else {
context_store.insert_thread(thread.read(cx));
}
})
.update(cx, |context_store, cx| context_store.add_thread(thread, cx))
.ok();
match self.confirm_behavior {
@@ -199,8 +193,8 @@ impl PickerDelegate for ThreadContextPickerDelegate {
) -> Option<Self::ListItem> {
let thread = &self.matches[ix];
let added = self.context_store.upgrade().map_or(false, |ctx_store| {
ctx_store.read(cx).included_thread(&thread.id).is_some()
let added = self.context_store.upgrade().map_or(false, |context_store| {
context_store.read(cx).includes_thread(&thread.id).is_some()
});
Some(

View File

@@ -1,37 +1,54 @@
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use collections::{HashMap, HashSet};
use gpui::SharedString;
use anyhow::{anyhow, bail, Result};
use collections::{BTreeMap, HashMap};
use gpui::{AppContext, Model, ModelContext, SharedString, Task, WeakView};
use language::Buffer;
use project::{ProjectPath, Worktree};
use text::BufferId;
use workspace::Workspace;
use crate::thread::Thread;
use crate::{
context::{Context, ContextId, ContextKind},
thread::ThreadId,
use crate::context::{
Context, ContextId, ContextKind, ContextSnapshot, DirectoryContext, FetchedUrlContext,
FileContext, ThreadContext,
};
use crate::thread::{Thread, ThreadId};
pub struct ContextStore {
workspace: WeakView<Workspace>,
context: Vec<Context>,
// TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
next_context_id: ContextId,
files: HashMap<PathBuf, ContextId>,
files: BTreeMap<BufferId, ContextId>,
directories: HashMap<PathBuf, ContextId>,
threads: HashMap<ThreadId, ContextId>,
fetched_urls: HashMap<String, ContextId>,
}
impl ContextStore {
pub fn new() -> Self {
pub fn new(workspace: WeakView<Workspace>) -> Self {
Self {
workspace,
context: Vec::new(),
next_context_id: ContextId(0),
files: HashMap::default(),
files: BTreeMap::default(),
directories: HashMap::default(),
threads: HashMap::default(),
fetched_urls: HashMap::default(),
}
}
pub fn snapshot<'a>(
&'a self,
cx: &'a AppContext,
) -> impl Iterator<Item = ContextSnapshot> + 'a {
self.context()
.iter()
.flat_map(|context| context.snapshot(cx))
}
pub fn context(&self) -> &Vec<Context> {
&self.context
}
@@ -44,42 +61,156 @@ impl ContextStore {
self.fetched_urls.clear();
}
pub fn insert_file(&mut self, buffer: &Buffer) {
pub fn add_file(
&mut self,
project_path: ProjectPath,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("failed to read project")));
};
cx.spawn(|this, mut cx| async move {
let open_buffer_task = project.update(&mut cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?;
let buffer = open_buffer_task.await?;
let buffer_id = buffer.update(&mut cx, |buffer, _cx| buffer.remote_id())?;
let already_included = this.update(&mut cx, |this, _cx| {
match this.will_include_buffer(buffer_id, &project_path.path) {
Some(FileInclusion::Direct(context_id)) => {
this.remove_context(context_id);
true
}
Some(FileInclusion::InDirectory(_)) => true,
None => false,
}
})?;
if already_included {
return anyhow::Ok(());
}
this.update(&mut cx, |this, cx| {
this.insert_file(buffer, cx);
})?;
anyhow::Ok(())
})
}
pub fn insert_file(&mut self, buffer_model: Model<Buffer>, cx: &AppContext) {
let buffer = buffer_model.read(cx);
let Some(file) = buffer.file() else {
return;
};
let path = file.path();
let mut text = String::new();
push_fenced_codeblock(file.path(), buffer.text(), &mut text);
let id = self.next_context_id.post_inc();
self.files.insert(path.to_path_buf(), id);
let full_path: SharedString = path.to_string_lossy().into_owned().into();
let name = match path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => full_path.clone(),
};
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().into_owned().into());
let mut text = String::new();
push_fenced_codeblock(path, buffer.text(), &mut text);
self.context.push(Context {
self.files.insert(buffer.remote_id(), id);
self.context.push(Context::File(FileContext {
id,
name,
parent,
tooltip: Some(full_path),
kind: ContextKind::File,
buffer: buffer_model,
version: buffer.version.clone(),
text: text.into(),
});
}));
}
pub fn insert_directory(&mut self, path: &Path, text: impl Into<SharedString>) {
pub fn add_directory(
&mut self,
project_path: ProjectPath,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("failed to read project")));
};
let already_included = if let Some(context_id) = self.includes_directory(&project_path.path)
{
self.remove_context(context_id);
true
} else {
false
};
if already_included {
return Task::ready(Ok(()));
}
let worktree_id = project_path.worktree_id;
cx.spawn(|this, mut cx| async move {
let worktree = project.update(&mut cx, |project, cx| {
project
.worktree_for_id(worktree_id, cx)
.ok_or_else(|| anyhow!("no worktree found for {worktree_id:?}"))
})??;
let files = worktree.update(&mut cx, |worktree, _cx| {
collect_files_in_path(worktree, &project_path.path)
})?;
let open_buffer_tasks = project.update(&mut cx, |project, cx| {
files
.into_iter()
.map(|file_path| {
project.open_buffer(
ProjectPath {
worktree_id,
path: file_path.clone(),
},
cx,
)
})
.collect::<Vec<_>>()
})?;
let buffers = futures::future::join_all(open_buffer_tasks).await;
this.update(&mut cx, |this, cx| {
let mut text = String::new();
let mut directory_buffers = BTreeMap::new();
for buffer_model in buffers {
let buffer_model = buffer_model?;
let buffer = buffer_model.read(cx);
let path = buffer.file().map_or(&project_path.path, |file| file.path());
push_fenced_codeblock(&path, buffer.text(), &mut text);
directory_buffers
.insert(buffer.remote_id(), (buffer_model, buffer.version.clone()));
}
if directory_buffers.is_empty() {
bail!(
"could not read any text files from {}",
&project_path.path.display()
);
}
this.insert_directory(&project_path.path, directory_buffers, text);
anyhow::Ok(())
})??;
anyhow::Ok(())
})
}
pub fn insert_directory(
&mut self,
path: &Path,
buffers: BTreeMap<BufferId, (Model<Buffer>, clock::Global)>,
text: impl Into<SharedString>,
) {
let id = self.next_context_id.post_inc();
self.directories.insert(path.to_path_buf(), id);
@@ -95,70 +226,104 @@ impl ContextStore {
.and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().into_owned().into());
self.context.push(Context {
id,
name,
parent,
tooltip: Some(full_path),
kind: ContextKind::Directory,
text: text.into(),
});
self.context.push(Context::Directory(DirectoryContext {
path: path.into(),
buffers,
snapshot: ContextSnapshot {
id,
name,
parent,
tooltip: Some(full_path),
kind: ContextKind::Directory,
text: text.into(),
},
}));
}
pub fn insert_thread(&mut self, thread: &Thread) {
let context_id = self.next_context_id.post_inc();
self.threads.insert(thread.id().clone(), context_id);
pub fn add_thread(&mut self, thread: Model<Thread>, cx: &mut ModelContext<Self>) {
if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
self.remove_context(context_id);
} else {
self.insert_thread(thread, cx);
}
}
self.context.push(Context {
id: context_id,
name: thread.summary().unwrap_or("New thread".into()),
parent: None,
tooltip: None,
kind: ContextKind::Thread,
text: thread.text().into(),
});
pub fn insert_thread(&mut self, thread: Model<Thread>, cx: &AppContext) {
let id = self.next_context_id.post_inc();
let thread_ref = thread.read(cx);
let text = thread_ref.text().into();
self.threads.insert(thread_ref.id().clone(), id);
self.context
.push(Context::Thread(ThreadContext { id, thread, text }));
}
pub fn insert_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
let context_id = self.next_context_id.post_inc();
self.fetched_urls.insert(url.clone(), context_id);
let id = self.next_context_id.post_inc();
self.context.push(Context {
id: context_id,
name: url.into(),
parent: None,
tooltip: None,
kind: ContextKind::FetchedUrl,
self.fetched_urls.insert(url.clone(), id);
self.context.push(Context::FetchedUrl(FetchedUrlContext {
id,
url: url.into(),
text: text.into(),
});
}));
}
pub fn remove_context(&mut self, id: &ContextId) {
let Some(ix) = self.context.iter().position(|context| context.id == *id) else {
pub fn remove_context(&mut self, id: ContextId) {
let Some(ix) = self.context.iter().position(|context| context.id() == id) else {
return;
};
match self.context.remove(ix).kind {
ContextKind::File => {
self.files.retain(|_, context_id| context_id != id);
match self.context.remove(ix) {
Context::File(_) => {
self.files.retain(|_, context_id| *context_id != id);
}
ContextKind::Directory => {
self.directories.retain(|_, context_id| context_id != id);
Context::Directory(_) => {
self.directories.retain(|_, context_id| *context_id != id);
}
ContextKind::FetchedUrl => {
self.fetched_urls.retain(|_, context_id| context_id != id);
Context::FetchedUrl(_) => {
self.fetched_urls.retain(|_, context_id| *context_id != id);
}
ContextKind::Thread => {
self.threads.retain(|_, context_id| context_id != id);
Context::Thread(_) => {
self.threads.retain(|_, context_id| *context_id != id);
}
}
}
pub fn included_file(&self, path: &Path) -> Option<IncludedFile> {
if let Some(id) = self.files.get(path) {
return Some(IncludedFile::Direct(*id));
/// Returns whether the buffer is already included directly in the context, or if it will be
/// included in the context via a directory. Directory inclusion is based on paths rather than
/// buffer IDs as the directory will be re-scanned.
pub fn will_include_buffer(&self, buffer_id: BufferId, path: &Path) -> Option<FileInclusion> {
if let Some(context_id) = self.files.get(&buffer_id) {
return Some(FileInclusion::Direct(*context_id));
}
self.will_include_file_path_via_directory(path)
}
/// Returns whether this file path is already included directly in the context, or if it will be
/// included in the context via a directory.
pub fn will_include_file_path(&self, path: &Path, cx: &AppContext) -> Option<FileInclusion> {
if !self.files.is_empty() {
let found_file_context = self.context.iter().find(|context| match &context {
Context::File(file_context) => {
if let Some(file_path) = file_context.path(cx) {
*file_path == *path
} else {
false
}
}
_ => false,
});
if let Some(context) = found_file_context {
return Some(FileInclusion::Direct(context.id()));
}
}
self.will_include_file_path_via_directory(path)
}
fn will_include_file_path_via_directory(&self, path: &Path) -> Option<FileInclusion> {
if self.directories.is_empty() {
return None;
}
@@ -167,40 +332,27 @@ impl ContextStore {
while buf.pop() {
if let Some(_) = self.directories.get(&buf) {
return Some(IncludedFile::InDirectory(buf));
return Some(FileInclusion::InDirectory(buf));
}
}
None
}
pub fn included_directory(&self, path: &Path) -> Option<ContextId> {
pub fn includes_directory(&self, path: &Path) -> Option<ContextId> {
self.directories.get(path).copied()
}
pub fn included_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
pub fn includes_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
self.threads.get(thread_id).copied()
}
pub fn included_url(&self, url: &str) -> Option<ContextId> {
pub fn includes_url(&self, url: &str) -> Option<ContextId> {
self.fetched_urls.get(url).copied()
}
pub fn duplicated_names(&self) -> HashSet<SharedString> {
let mut seen = HashSet::default();
let mut dupes = HashSet::default();
for context in self.context().iter() {
if !seen.insert(&context.name) {
dupes.insert(context.name.clone());
}
}
dupes
}
}
pub enum IncludedFile {
pub enum FileInclusion {
Direct(ContextId),
InDirectory(PathBuf),
}
@@ -225,3 +377,17 @@ pub(crate) fn push_fenced_codeblock(path: &Path, content: String, buffer: &mut S
buffer.push_str("```\n");
}
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
let mut files = Vec::new();
for entry in worktree.child_entries(path) {
if entry.is_dir() {
files.extend(collect_files_in_path(worktree, &entry.path));
} else if entry.is_file() {
files.push(entry.path.clone());
}
}
files
}

View File

@@ -1,10 +1,12 @@
use std::rc::Rc;
use collections::HashSet;
use editor::Editor;
use gpui::{
AppContext, DismissEvent, EventEmitter, FocusHandle, Model, Subscription, View, WeakModel,
WeakView,
};
use itertools::Itertools;
use language::Buffer;
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
use workspace::Workspace;
@@ -73,11 +75,17 @@ impl ContextStrip {
let active_item = workspace.read(cx).active_item(cx)?;
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
let active_buffer = editor.buffer().read(cx).as_singleton()?;
let active_buffer_model = editor.buffer().read(cx).as_singleton()?;
let active_buffer = active_buffer_model.read(cx);
let path = active_buffer.read(cx).file()?.path();
let path = active_buffer.file()?.path();
if self.context_store.read(cx).included_file(path).is_some() {
if self
.context_store
.read(cx)
.will_include_buffer(active_buffer.remote_id(), path)
.is_some()
{
return None;
}
@@ -88,7 +96,7 @@ impl ContextStrip {
Some(SuggestedContext::File {
name,
buffer: active_buffer.downgrade(),
buffer: active_buffer_model.downgrade(),
})
}
@@ -106,7 +114,7 @@ impl ContextStrip {
if self
.context_store
.read(cx)
.included_thread(active_thread.id())
.includes_thread(active_thread.id())
.is_some()
{
return None;
@@ -131,13 +139,24 @@ impl ContextStrip {
impl Render for ContextStrip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let context_store = self.context_store.read(cx);
let context = context_store.context().clone();
let context = context_store
.context()
.iter()
.flat_map(|context| context.snapshot(cx))
.collect::<Vec<_>>();
let context_picker = self.context_picker.clone();
let focus_handle = self.focus_handle.clone();
let suggested_context = self.suggested_context(cx);
let dupe_names = context_store.duplicated_names();
let dupe_names = context
.iter()
.map(|context| context.name.clone())
.sorted()
.tuple_windows()
.filter(|(a, b)| a == b)
.map(|(a, _)| a)
.collect::<HashSet<SharedString>>();
h_flex()
.flex_wrap()
@@ -194,11 +213,11 @@ impl Render for ContextStrip {
context.clone(),
dupe_names.contains(&context.name),
Some({
let context = context.clone();
let id = context.id;
let context_store = self.context_store.clone();
Rc::new(cx.listener(move |_this, _event, cx| {
context_store.update(cx, |this, _cx| {
this.remove_context(&context.id);
this.remove_context(id);
});
cx.notify();
}))
@@ -284,12 +303,12 @@ impl SuggestedContext {
match self {
Self::File { buffer, name: _ } => {
if let Some(buffer) = buffer.upgrade() {
context_store.insert_file(buffer.read(cx));
context_store.insert_file(buffer, cx);
};
}
Self::Thread { thread, name: _ } => {
if let Some(thread) = thread.upgrade() {
context_store.insert_thread(thread.read(cx));
context_store.insert_thread(thread, cx);
};
}
}

View File

@@ -19,7 +19,6 @@ use editor::{
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
};
use feature_flags::{Assistant2FeatureFlag, FeatureFlagViewExt as _};
use fs::Fs;
use util::ResultExt;
@@ -54,16 +53,7 @@ pub fn init(
let workspace = cx.view().clone();
InlineAssistant::update_global(cx, |inline_assistant, cx| {
inline_assistant.register_workspace(&workspace, cx)
});
cx.observe_flag::<Assistant2FeatureFlag, _>({
|is_assistant2_enabled, _view, cx| {
InlineAssistant::update_global(cx, |inline_assistant, _cx| {
inline_assistant.is_assistant2_enabled = is_assistant2_enabled;
});
}
})
.detach();
})
.detach();
}
@@ -86,7 +76,6 @@ pub struct InlineAssistant {
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
fs: Arc<dyn Fs>,
is_assistant2_enabled: bool,
}
impl Global for InlineAssistant {}
@@ -108,7 +97,6 @@ impl InlineAssistant {
prompt_builder,
telemetry,
fs,
is_assistant2_enabled: false,
}
}
@@ -169,31 +157,21 @@ impl InlineAssistant {
item: &dyn ItemHandle,
cx: &mut WindowContext,
) {
let is_assistant2_enabled = self.is_assistant2_enabled;
if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
if is_assistant2_enabled {
let thread_store = workspace
.read(cx)
.panel::<AssistantPanel>(cx)
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
let thread_store = workspace
.read(cx)
.panel::<AssistantPanel>(cx)
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
editor.add_code_action_provider(
Rc::new(AssistantCodeActionProvider {
editor: cx.view().downgrade(),
workspace: workspace.downgrade(),
thread_store,
}),
cx,
);
// Remove the Assistant1 code action provider, as it still might be registered.
editor.remove_code_action_provider("assistant".into(), cx);
} else {
editor
.remove_code_action_provider(ASSISTANT_CODE_ACTION_PROVIDER_ID.into(), cx);
}
editor.push_code_action_provider(
Rc::new(AssistantCodeActionProvider {
editor: cx.view().downgrade(),
workspace: workspace.downgrade(),
thread_store,
}),
cx,
);
});
}
}
@@ -357,7 +335,7 @@ impl InlineAssistant {
let mut assist_to_focus = None;
for range in codegen_ranges {
let assist_id = self.next_assist_id.post_inc();
let context_store = cx.new_model(|_cx| ContextStore::new());
let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone()));
let codegen = cx.new_model(|cx| {
BufferCodegen::new(
editor.read(cx).buffer().clone(),
@@ -467,7 +445,7 @@ impl InlineAssistant {
range.end = range.end.bias_right(&snapshot);
}
let context_store = cx.new_model(|_cx| ContextStore::new());
let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone()));
let codegen = cx.new_model(|cx| {
BufferCodegen::new(
@@ -1595,13 +1573,7 @@ struct AssistantCodeActionProvider {
thread_store: Option<WeakModel<ThreadStore>>,
}
const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant2";
impl CodeActionProvider for AssistantCodeActionProvider {
fn id(&self) -> Arc<str> {
ASSISTANT_CODE_ACTION_PROVIDER_ID.into()
}
fn code_actions(
&self,
buffer: &Model<Buffer>,

View File

@@ -47,7 +47,7 @@ impl MessageEditor {
thread: Model<Thread>,
cx: &mut ViewContext<Self>,
) -> Self {
let context_store = cx.new_model(|_cx| ContextStore::new());
let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone()));
let context_picker_menu_handle = PopoverMenuHandle::default();
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
@@ -147,11 +147,10 @@ impl MessageEditor {
editor.clear(cx);
text
});
let context = self
.context_store
.update(cx, |this, _cx| this.context().clone());
self.thread.update(cx, |thread, cx| {
let thread = self.thread.clone();
thread.update(cx, |thread, cx| {
let context = self.context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
thread.insert_user_message(user_message, context, cx);
let mut request = thread.to_completion_request(request_kind, cx);

View File

@@ -78,7 +78,7 @@ impl TerminalInlineAssistant {
let prompt_buffer = cx.new_model(|cx| {
MultiBuffer::singleton(cx.new_model(|cx| Buffer::local(String::new(), cx)), cx)
});
let context_store = cx.new_model(|_cx| ContextStore::new());
let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone()));
let codegen = cx.new_model(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
let prompt_editor = cx.new_view(|cx| {
@@ -245,10 +245,10 @@ impl TerminalInlineAssistant {
cache: false,
};
let context = assist
.context_store
.update(cx, |this, _cx| this.context().clone());
attach_context_to_message(&mut request_message, context);
attach_context_to_message(
&mut request_message,
assist.context_store.read(cx).snapshot(cx),
);
request_message.content.push(prompt.into());

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use chrono::{DateTime, Utc};
use collections::{HashMap, HashSet};
use collections::{BTreeMap, HashMap, HashSet};
use futures::future::Shared;
use futures::{FutureExt as _, StreamExt as _};
use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task};
@@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
use util::{post_inc, TryFutureExt as _};
use uuid::Uuid;
use crate::context::{attach_context_to_message, Context, ContextId};
use crate::context::{attach_context_to_message, ContextId, ContextSnapshot};
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
@@ -64,7 +64,7 @@ pub struct Thread {
pending_summary: Task<Option<()>>,
messages: Vec<Message>,
next_message_id: MessageId,
context: HashMap<ContextId, Context>,
context: BTreeMap<ContextId, ContextSnapshot>,
context_by_message: HashMap<MessageId, Vec<ContextId>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
@@ -83,7 +83,7 @@ impl Thread {
pending_summary: Task::ready(None),
messages: Vec::new(),
next_message_id: MessageId(0),
context: HashMap::default(),
context: BTreeMap::default(),
context_by_message: HashMap::default(),
completion_count: 0,
pending_completions: Vec::new(),
@@ -131,7 +131,7 @@ impl Thread {
&self.tools
}
pub fn context_for_message(&self, id: MessageId) -> Option<Vec<Context>> {
pub fn context_for_message(&self, id: MessageId) -> Option<Vec<ContextSnapshot>> {
let context = self.context_by_message.get(&id)?;
Some(
context
@@ -149,7 +149,7 @@ impl Thread {
pub fn insert_user_message(
&mut self,
text: impl Into<String>,
context: Vec<Context>,
context: Vec<ContextSnapshot>,
cx: &mut ModelContext<Self>,
) {
let message_id = self.insert_message(Role::User, text, cx);

View File

@@ -3,12 +3,12 @@ use std::rc::Rc;
use gpui::ClickEvent;
use ui::{prelude::*, IconButtonShape, Tooltip};
use crate::context::{Context, ContextKind};
use crate::context::{ContextKind, ContextSnapshot};
#[derive(IntoElement)]
pub enum ContextPill {
Added {
context: Context,
context: ContextSnapshot,
dupe_name: bool,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
},
@@ -21,7 +21,7 @@ pub enum ContextPill {
impl ContextPill {
pub fn new_added(
context: Context,
context: ContextSnapshot,
dupe_name: bool,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
) -> Self {
@@ -49,10 +49,10 @@ impl ContextPill {
}
}
pub fn kind(&self) -> &ContextKind {
pub fn kind(&self) -> ContextKind {
match self {
Self::Added { context, .. } => &context.kind,
Self::Suggested { kind, .. } => kind,
Self::Added { context, .. } => context.kind,
Self::Suggested { kind, .. } => *kind,
}
}
}

View File

@@ -19,10 +19,7 @@ use tempfile::NamedTempFile;
use util::paths::PathWithPosition;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use {
std::io::IsTerminal,
util::{load_login_shell_environment, load_shell_from_passwd, ResultExt},
};
use std::io::IsTerminal;
struct Detect;
@@ -167,15 +164,24 @@ fn main() -> Result<()> {
None
};
// On Linux, desktop entry uses `cli` to spawn `zed`, so we need to load env vars from the shell
// since it doesn't inherit env vars from the terminal.
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if !std::io::stdout().is_terminal() {
load_shell_from_passwd().log_err();
load_login_shell_environment().log_err();
}
let env = {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
// On Linux, the desktop entry uses `cli` to spawn `zed`.
// We need to handle env vars correctly since std::env::vars() may not contain
// project-specific vars (e.g. those set by direnv).
// By setting env to None here, the LSP will use worktree env vars instead,
// which is what we want.
if !std::io::stdout().is_terminal() {
None
} else {
Some(std::env::vars().collect::<HashMap<_, _>>())
}
}
let env = Some(std::env::vars().collect::<HashMap<_, _>>());
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
Some(std::env::vars().collect::<HashMap<_, _>>())
};
let exit_status = Arc::new(Mutex::new(None));
let mut paths = vec![];

View File

@@ -34,7 +34,6 @@ collections.workspace = true
dashmap.workspace = true
derive_more.workspace = true
envy = "0.4.2"
fireworks.workspace = true
futures.workspace = true
google_ai.workspace = true
hex.workspace = true

View File

@@ -438,7 +438,8 @@ CREATE TABLE IF NOT EXISTS billing_subscriptions (
billing_customer_id INTEGER NOT NULL REFERENCES billing_customers(id),
stripe_subscription_id TEXT NOT NULL,
stripe_subscription_status TEXT NOT NULL,
stripe_cancel_at TIMESTAMP
stripe_cancel_at TIMESTAMP,
stripe_cancellation_reason TEXT
);
CREATE INDEX "ix_billing_subscriptions_on_billing_customer_id" ON billing_subscriptions (billing_customer_id);

View File

@@ -0,0 +1,2 @@
alter table billing_subscriptions
add column stripe_cancellation_reason text;

View File

@@ -12,8 +12,8 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use std::{str::FromStr, sync::Arc, time::Duration};
use stripe::{
BillingPortalSession, CreateBillingPortalSession, CreateBillingPortalSessionFlowData,
CreateBillingPortalSessionFlowDataAfterCompletion,
BillingPortalSession, CancellationDetailsReason, CreateBillingPortalSession,
CreateBillingPortalSessionFlowData, CreateBillingPortalSessionFlowDataAfterCompletion,
CreateBillingPortalSessionFlowDataAfterCompletionRedirect,
CreateBillingPortalSessionFlowDataType, CreateCustomer, Customer, CustomerId, EventObject,
EventType, Expandable, ListEvents, Subscription, SubscriptionId, SubscriptionStatus,
@@ -21,8 +21,10 @@ use stripe::{
use util::ResultExt;
use crate::api::events::SnowflakeRow;
use crate::db::billing_subscription::{StripeCancellationReason, StripeSubscriptionStatus};
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
use crate::rpc::{ResultExt as _, Server};
use crate::{db::UserId, llm::db::LlmDatabase};
use crate::{
db::{
billing_customer, BillingSubscriptionId, CreateBillingCustomerParams,
@@ -32,10 +34,6 @@ use crate::{
},
stripe_billing::StripeBilling,
};
use crate::{
db::{billing_subscription::StripeSubscriptionStatus, UserId},
llm::db::LlmDatabase,
};
use crate::{AppState, Cents, Error, Result};
pub fn router() -> Router {
@@ -251,6 +249,13 @@ async fn create_billing_subscription(
));
}
if app.db.has_overdue_billing_subscriptions(user.id).await? {
return Err(Error::http(
StatusCode::PAYMENT_REQUIRED,
"user has overdue billing subscriptions".into(),
));
}
let customer_id =
if let Some(existing_customer) = app.db.get_billing_customer_by_user_id(user.id).await? {
CustomerId::from_str(&existing_customer.stripe_customer_id)
@@ -679,6 +684,12 @@ async fn handle_customer_subscription_event(
.and_then(|cancel_at| DateTime::from_timestamp(cancel_at, 0))
.map(|time| time.naive_utc()),
),
stripe_cancellation_reason: ActiveValue::set(
subscription
.cancellation_details
.and_then(|details| details.reason)
.map(|reason| reason.into()),
),
},
)
.await?;
@@ -715,6 +726,10 @@ async fn handle_customer_subscription_event(
billing_customer_id: billing_customer.id,
stripe_subscription_id: subscription.id.to_string(),
stripe_subscription_status: subscription.status.into(),
stripe_cancellation_reason: subscription
.cancellation_details
.and_then(|details| details.reason)
.map(|reason| reason.into()),
})
.await?;
}
@@ -791,6 +806,16 @@ impl From<SubscriptionStatus> for StripeSubscriptionStatus {
}
}
impl From<CancellationDetailsReason> for StripeCancellationReason {
fn from(value: CancellationDetailsReason) -> Self {
match value {
CancellationDetailsReason::CancellationRequested => Self::CancellationRequested,
CancellationDetailsReason::PaymentDisputed => Self::PaymentDisputed,
CancellationDetailsReason::PaymentFailed => Self::PaymentFailed,
}
}
}
/// Finds or creates a billing customer using the provided customer.
async fn find_or_create_billing_customer(
app: &Arc<AppState>,

View File

@@ -1,4 +1,4 @@
use crate::db::billing_subscription::StripeSubscriptionStatus;
use crate::db::billing_subscription::{StripeCancellationReason, StripeSubscriptionStatus};
use super::*;
@@ -7,6 +7,7 @@ pub struct CreateBillingSubscriptionParams {
pub billing_customer_id: BillingCustomerId,
pub stripe_subscription_id: String,
pub stripe_subscription_status: StripeSubscriptionStatus,
pub stripe_cancellation_reason: Option<StripeCancellationReason>,
}
#[derive(Debug, Default)]
@@ -15,6 +16,7 @@ pub struct UpdateBillingSubscriptionParams {
pub stripe_subscription_id: ActiveValue<String>,
pub stripe_subscription_status: ActiveValue<StripeSubscriptionStatus>,
pub stripe_cancel_at: ActiveValue<Option<DateTime>>,
pub stripe_cancellation_reason: ActiveValue<Option<StripeCancellationReason>>,
}
impl Database {
@@ -28,6 +30,7 @@ impl Database {
billing_customer_id: ActiveValue::set(params.billing_customer_id),
stripe_subscription_id: ActiveValue::set(params.stripe_subscription_id.clone()),
stripe_subscription_status: ActiveValue::set(params.stripe_subscription_status),
stripe_cancellation_reason: ActiveValue::set(params.stripe_cancellation_reason),
..Default::default()
})
.exec_without_returning(&*tx)
@@ -51,6 +54,7 @@ impl Database {
stripe_subscription_id: params.stripe_subscription_id.clone(),
stripe_subscription_status: params.stripe_subscription_status.clone(),
stripe_cancel_at: params.stripe_cancel_at.clone(),
stripe_cancellation_reason: params.stripe_cancellation_reason.clone(),
..Default::default()
})
.exec(&*tx)
@@ -166,4 +170,40 @@ impl Database {
})
.await
}
/// Returns whether the user has any overdue billing subscriptions.
pub async fn has_overdue_billing_subscriptions(&self, user_id: UserId) -> Result<bool> {
Ok(self.count_overdue_billing_subscriptions(user_id).await? > 0)
}
/// Returns the count of the overdue billing subscriptions for the user with the specified ID.
///
/// This includes subscriptions:
/// - Whose status is `past_due`
/// - Whose status is `canceled` and the cancellation reason is `payment_failed`
pub async fn count_overdue_billing_subscriptions(&self, user_id: UserId) -> Result<usize> {
self.transaction(|tx| async move {
let past_due = billing_subscription::Column::StripeSubscriptionStatus
.eq(StripeSubscriptionStatus::PastDue);
let payment_failed = billing_subscription::Column::StripeSubscriptionStatus
.eq(StripeSubscriptionStatus::Canceled)
.and(
billing_subscription::Column::StripeCancellationReason
.eq(StripeCancellationReason::PaymentFailed),
);
let count = billing_subscription::Entity::find()
.inner_join(billing_customer::Entity)
.filter(
billing_customer::Column::UserId
.eq(user_id)
.and(past_due.or(payment_failed)),
)
.count(&*tx)
.await?;
Ok(count as usize)
})
.await
}
}

View File

@@ -12,6 +12,7 @@ pub struct Model {
pub stripe_subscription_id: String,
pub stripe_subscription_status: StripeSubscriptionStatus,
pub stripe_cancel_at: Option<DateTime>,
pub stripe_cancellation_reason: Option<StripeCancellationReason>,
pub created_at: DateTime,
}
@@ -73,3 +74,18 @@ impl StripeSubscriptionStatus {
}
}
}
/// The cancellation reason for a Stripe subscription.
///
/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-cancellation_details-reason)
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)]
#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
#[serde(rename_all = "snake_case")]
pub enum StripeCancellationReason {
#[sea_orm(string_value = "cancellation_requested")]
CancellationRequested,
#[sea_orm(string_value = "payment_disputed")]
PaymentDisputed,
#[sea_orm(string_value = "payment_failed")]
PaymentFailed,
}

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::db::billing_subscription::StripeSubscriptionStatus;
use crate::db::billing_subscription::{StripeCancellationReason, StripeSubscriptionStatus};
use crate::db::tests::new_test_user;
use crate::db::{CreateBillingCustomerParams, CreateBillingSubscriptionParams};
use crate::test_both_dbs;
@@ -41,6 +41,7 @@ async fn test_get_active_billing_subscriptions(db: &Arc<Database>) {
billing_customer_id: customer.id,
stripe_subscription_id: "sub_active_user".into(),
stripe_subscription_status: StripeSubscriptionStatus::Active,
stripe_cancellation_reason: None,
})
.await
.unwrap();
@@ -75,6 +76,7 @@ async fn test_get_active_billing_subscriptions(db: &Arc<Database>) {
billing_customer_id: customer.id,
stripe_subscription_id: "sub_past_due_user".into(),
stripe_subscription_status: StripeSubscriptionStatus::PastDue,
stripe_cancellation_reason: None,
})
.await
.unwrap();
@@ -86,3 +88,113 @@ async fn test_get_active_billing_subscriptions(db: &Arc<Database>) {
assert_eq!(subscription_count, 0);
}
}
test_both_dbs!(
test_count_overdue_billing_subscriptions,
test_count_overdue_billing_subscriptions_postgres,
test_count_overdue_billing_subscriptions_sqlite
);
async fn test_count_overdue_billing_subscriptions(db: &Arc<Database>) {
// A user with no subscription has no overdue billing subscriptions.
{
let user_id = new_test_user(db, "no-subscription-user@example.com").await;
let subscription_count = db
.count_overdue_billing_subscriptions(user_id)
.await
.unwrap();
assert_eq!(subscription_count, 0);
}
// A user with a past-due subscription has an overdue billing subscription.
{
let user_id = new_test_user(db, "past-due-user@example.com").await;
let customer = db
.create_billing_customer(&CreateBillingCustomerParams {
user_id,
stripe_customer_id: "cus_past_due_user".into(),
})
.await
.unwrap();
assert_eq!(customer.stripe_customer_id, "cus_past_due_user".to_string());
db.create_billing_subscription(&CreateBillingSubscriptionParams {
billing_customer_id: customer.id,
stripe_subscription_id: "sub_past_due_user".into(),
stripe_subscription_status: StripeSubscriptionStatus::PastDue,
stripe_cancellation_reason: None,
})
.await
.unwrap();
let subscription_count = db
.count_overdue_billing_subscriptions(user_id)
.await
.unwrap();
assert_eq!(subscription_count, 1);
}
// A user with a canceled subscription with a reason of `payment_failed` has an overdue billing subscription.
{
let user_id =
new_test_user(db, "canceled-subscription-payment-failed-user@example.com").await;
let customer = db
.create_billing_customer(&CreateBillingCustomerParams {
user_id,
stripe_customer_id: "cus_canceled_subscription_payment_failed_user".into(),
})
.await
.unwrap();
assert_eq!(
customer.stripe_customer_id,
"cus_canceled_subscription_payment_failed_user".to_string()
);
db.create_billing_subscription(&CreateBillingSubscriptionParams {
billing_customer_id: customer.id,
stripe_subscription_id: "sub_canceled_subscription_payment_failed_user".into(),
stripe_subscription_status: StripeSubscriptionStatus::Canceled,
stripe_cancellation_reason: Some(StripeCancellationReason::PaymentFailed),
})
.await
.unwrap();
let subscription_count = db
.count_overdue_billing_subscriptions(user_id)
.await
.unwrap();
assert_eq!(subscription_count, 1);
}
// A user with a canceled subscription with a reason of `cancellation_requested` has no overdue billing subscriptions.
{
let user_id = new_test_user(db, "canceled-subscription-user@example.com").await;
let customer = db
.create_billing_customer(&CreateBillingCustomerParams {
user_id,
stripe_customer_id: "cus_canceled_subscription_user".into(),
})
.await
.unwrap();
assert_eq!(
customer.stripe_customer_id,
"cus_canceled_subscription_user".to_string()
);
db.create_billing_subscription(&CreateBillingSubscriptionParams {
billing_customer_id: customer.id,
stripe_subscription_id: "sub_canceled_subscription_user".into(),
stripe_subscription_status: StripeSubscriptionStatus::Canceled,
stripe_cancellation_reason: Some(StripeCancellationReason::CancellationRequested),
})
.await
.unwrap();
let subscription_count = db
.count_overdue_billing_subscriptions(user_id)
.await
.unwrap();
assert_eq!(subscription_count, 0);
}
}

View File

@@ -440,11 +440,8 @@ async fn predict_edits(
_country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
Json(params): Json<PredictEditsParams>,
) -> Result<impl IntoResponse> {
if !claims.is_staff && !claims.has_predict_edits_feature_flag {
return Err(Error::http(
StatusCode::FORBIDDEN,
"no access to Zed's edit prediction feature".to_string(),
));
if !claims.is_staff {
return Err(anyhow!("not found"))?;
}
let api_url = state
@@ -473,55 +470,26 @@ async fn predict_edits(
.replace("<outline>", &outline_prefix)
.replace("<events>", &params.input_events)
.replace("<excerpt>", &params.input_excerpt);
let request_start = std::time::Instant::now();
let mut response = fireworks::complete(
let mut response = open_ai::complete_text(
&state.http_client,
api_url,
api_key,
fireworks::CompletionRequest {
open_ai::CompletionRequest {
model: model.to_string(),
prompt: prompt.clone(),
max_tokens: 2048,
max_tokens: 1024,
temperature: 0.,
prediction: Some(fireworks::Prediction::Content {
prediction: Some(open_ai::Prediction::Content {
content: params.input_excerpt,
}),
rewrite_speculation: Some(true),
},
)
.await?;
let duration = request_start.elapsed();
let choice = response
.completion
.choices
.pop()
.context("no output from completion response")?;
state.executor.spawn_detached({
let kinesis_client = state.kinesis_client.clone();
let kinesis_stream = state.config.kinesis_stream.clone();
let model = model.clone();
async move {
SnowflakeRow::new(
"Fireworks Completion Requested",
claims.metrics_id,
claims.is_staff,
claims.system_id.clone(),
json!({
"model": model.to_string(),
"headers": response.headers,
"usage": response.completion.usage,
"duration": duration.as_secs_f64(),
}),
)
.write(&kinesis_client, &kinesis_stream)
.await
.log_err();
}
});
Ok(Json(PredictEditsResponse {
output_excerpt: choice.text,
}))

View File

@@ -22,8 +22,6 @@ pub struct LlmTokenClaims {
pub github_user_login: String,
pub is_staff: bool,
pub has_llm_closed_beta_feature_flag: bool,
#[serde(default)]
pub has_predict_edits_feature_flag: bool,
pub has_llm_subscription: bool,
pub max_monthly_spend_in_cents: u32,
pub custom_llm_monthly_allowance_in_cents: Option<u32>,
@@ -39,7 +37,6 @@ impl LlmTokenClaims {
is_staff: bool,
billing_preferences: Option<billing_preference::Model>,
has_llm_closed_beta_feature_flag: bool,
has_predict_edits_feature_flag: bool,
has_llm_subscription: bool,
plan: rpc::proto::Plan,
system_id: Option<String>,
@@ -61,7 +58,6 @@ impl LlmTokenClaims {
github_user_login: user.github_login.clone(),
is_staff,
has_llm_closed_beta_feature_flag,
has_predict_edits_feature_flag,
has_llm_subscription,
max_monthly_spend_in_cents: billing_preferences
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0, |preferences| {

View File

@@ -4025,7 +4025,6 @@ async fn get_llm_api_token(
let flags = db.get_user_flags(session.user_id()).await?;
let has_language_models_feature_flag = flags.iter().any(|flag| flag == "language-models");
let has_llm_closed_beta_feature_flag = flags.iter().any(|flag| flag == "llm-closed-beta");
let has_predict_edits_feature_flag = flags.iter().any(|flag| flag == "predict-edits");
if !session.is_staff() && !has_language_models_feature_flag {
Err(anyhow!("permission denied"))?
@@ -4062,7 +4061,6 @@ async fn get_llm_api_token(
session.is_staff(),
billing_preferences,
has_llm_closed_beta_feature_flag,
has_predict_edits_feature_flag,
has_llm_subscription,
session.current_plan(&db).await?,
session.system_id.clone(),

View File

@@ -18,7 +18,6 @@ collections.workspace = true
ctor.workspace = true
editor.workspace = true
env_logger.workspace = true
feature_flags.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true

View File

@@ -14,7 +14,6 @@ use editor::{
scroll::Autoscroll,
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
};
use feature_flags::FeatureFlagAppExt;
use gpui::{
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
FocusableView, Global, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement,
@@ -933,18 +932,16 @@ fn context_range_for_entry(
snapshot: &BufferSnapshot,
cx: &AppContext,
) -> Range<Point> {
if cx.is_staff() {
if let Some(rows) = heuristic_syntactic_expand(
entry.range.clone(),
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
snapshot,
cx,
) {
return Range {
start: Point::new(*rows.start(), 0),
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
};
}
if let Some(rows) = heuristic_syntactic_expand(
entry.range.clone(),
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
snapshot,
cx,
) {
return Range {
start: Point::new(*rows.start(), 0),
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
};
}
Range {
start: Point::new(entry.range.start.row.saturating_sub(context), 0),

View File

@@ -1,8 +1,8 @@
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
div, pulsating_between, px, uniform_list, Animation, AnimationExt, AnyElement,
BackgroundExecutor, Div, FontWeight, ListSizingBehavior, Model, ScrollStrategy, SharedString,
Size, StrikethroughStyle, StyledText, UniformListScrollHandle, ViewContext, WeakView,
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,
Model, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
UniformListScrollHandle, ViewContext, WeakView,
};
use language::Buffer;
use language::{CodeLabel, Documentation};
@@ -10,8 +10,6 @@ use lsp::LanguageServerId;
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
use project::{CodeAction, Completion, TaskSourceKind};
use settings::Settings;
use std::time::Duration;
use std::{
cell::RefCell,
cmp::{min, Reverse},
@@ -335,6 +333,9 @@ impl CompletionsMenu {
entries[0] = hint;
}
_ => {
if self.selected_item != 0 {
self.selected_item += 1;
}
entries.insert(0, hint);
}
}
@@ -462,9 +463,10 @@ impl CompletionsMenu {
len
}
CompletionEntry::InlineCompletionHint(hint) => {
"Zed AI / ".chars().count() + hint.label().chars().count()
}
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
provider_name,
..
}) => provider_name.len(),
})
.map(|(ix, _)| ix);
drop(completions);
@@ -488,12 +490,6 @@ impl CompletionsMenu {
.enumerate()
.map(|(ix, mat)| {
let item_ix = start_ix + ix;
let buffer_font = theme::ThemeSettings::get_global(cx).buffer_font.clone();
let base_label = h_flex()
.gap_1()
.child(div().font(buffer_font.clone()).child("Zed AI"))
.child(div().px_0p5().child("/").opacity(0.2));
match mat {
CompletionEntry::Match(mat) => {
let candidate_id = mat.candidate_id;
@@ -577,57 +573,20 @@ impl CompletionsMenu {
.end_slot::<Label>(documentation_label),
)
}
CompletionEntry::InlineCompletionHint(
hint @ InlineCompletionMenuHint::None,
) => div().min_w(px(250.)).max_w(px(500.)).child(
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
provider_name,
..
}) => div().min_w(px(250.)).max_w(px(500.)).child(
ListItem::new("inline-completion")
.inset(true)
.toggle_state(item_ix == selected_item)
.start_slot(Icon::new(IconName::ZedPredict))
.child(
base_label.child(
StyledText::new(hint.label())
.with_highlights(&style.text, None),
),
),
),
CompletionEntry::InlineCompletionHint(
hint @ InlineCompletionMenuHint::Loading,
) => div().min_w(px(250.)).max_w(px(500.)).child(
ListItem::new("inline-completion")
.inset(true)
.toggle_state(item_ix == selected_item)
.start_slot(Icon::new(IconName::ZedPredict))
.child(base_label.child({
let text_style = style.text.clone();
StyledText::new(hint.label())
.with_highlights(&text_style, None)
.with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(1))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
move |text, delta| {
let mut text_style = text_style.clone();
text_style.color =
text_style.color.opacity(delta);
text.with_highlights(&text_style, None)
},
)
})),
),
CompletionEntry::InlineCompletionHint(
hint @ InlineCompletionMenuHint::Loaded { .. },
) => div().min_w(px(250.)).max_w(px(500.)).child(
ListItem::new("inline-completion")
.inset(true)
.toggle_state(item_ix == selected_item)
.start_slot(Icon::new(IconName::ZedPredict))
.child(
base_label.child(
StyledText::new(hint.label())
.with_highlights(&style.text, None),
),
StyledText::new(format!(
"{} Completion",
SharedString::new_static(provider_name)
))
.with_highlights(&style.text, None),
)
.on_click(cx.listener(move |editor, _event, cx| {
cx.stop_propagation();
@@ -684,20 +643,19 @@ impl CompletionsMenu {
Documentation::Undocumented => return None,
}
}
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::Loaded { text }) => {
match text {
InlineCompletionText::Edit { text, highlights } => div()
.mx_1()
.rounded_md()
.bg(cx.theme().colors().editor_background)
.child(
gpui::StyledText::new(text.clone())
.with_highlights(&style.text, highlights.clone()),
),
InlineCompletionText::Move(text) => div().child(text.clone()),
}
}
CompletionEntry::InlineCompletionHint(_) => return None,
CompletionEntry::InlineCompletionHint(hint) => match &hint.text {
InlineCompletionText::Edit { text, highlights } => div()
.mx_1()
.rounded(px(6.))
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
gpui::StyledText::new(text.clone())
.with_highlights(&style.text, highlights.clone()),
),
InlineCompletionText::Move(text) => div().child(text.clone()),
},
};
Some(

View File

@@ -459,21 +459,9 @@ pub fn make_suggestion_styles(cx: &WindowContext) -> InlineCompletionStyles {
type CompletionId = usize;
#[derive(Debug, Clone)]
enum InlineCompletionMenuHint {
Loading,
Loaded { text: InlineCompletionText },
None,
}
impl InlineCompletionMenuHint {
pub fn label(&self) -> &'static str {
match self {
InlineCompletionMenuHint::Loading | InlineCompletionMenuHint::Loaded { .. } => {
"Edit Prediction"
}
InlineCompletionMenuHint::None => "No Prediction",
}
}
struct InlineCompletionMenuHint {
provider_name: &'static str,
text: InlineCompletionText,
}
#[derive(Clone, Debug)]
@@ -1739,12 +1727,8 @@ impl Editor {
self.input_enabled = input_enabled;
}
pub fn set_inline_completions_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
pub fn set_inline_completions_enabled(&mut self, enabled: bool) {
self.enable_inline_completions = enabled;
if !self.enable_inline_completions {
self.take_active_inline_completion(cx);
cx.notify();
}
}
pub fn set_autoindent(&mut self, autoindent: bool) {
@@ -3835,26 +3819,6 @@ impl Editor {
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
use language::ToOffset as _;
{
let context_menu = self.context_menu.borrow();
if let CodeContextMenu::Completions(menu) = context_menu.as_ref()? {
let entries = menu.entries.borrow();
let entry = entries.get(item_ix.unwrap_or(menu.selected_item));
match entry {
Some(CompletionEntry::InlineCompletionHint(
InlineCompletionMenuHint::Loading,
)) => return Some(Task::ready(Ok(()))),
Some(CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::None)) => {
drop(entries);
drop(context_menu);
self.context_menu_next(&Default::default(), cx);
return Some(Task::ready(Ok(())));
}
_ => {}
}
}
}
let completions_menu =
if let CodeContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
menu
@@ -3865,7 +3829,7 @@ impl Editor {
let entries = completions_menu.entries.borrow();
let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?;
let mat = match mat {
CompletionEntry::InlineCompletionHint(_) => {
CompletionEntry::InlineCompletionHint { .. } => {
self.accept_inline_completion(&AcceptInlineCompletion, cx);
cx.stop_propagation();
return Some(Task::ready(Ok(())));
@@ -4330,29 +4294,15 @@ impl Editor {
self.available_code_actions.take();
}
pub fn add_code_action_provider(
pub fn push_code_action_provider(
&mut self,
provider: Rc<dyn CodeActionProvider>,
cx: &mut ViewContext<Self>,
) {
if self
.code_action_providers
.iter()
.any(|existing_provider| existing_provider.id() == provider.id())
{
return;
}
self.code_action_providers.push(provider);
self.refresh_code_actions(cx);
}
pub fn remove_code_action_provider(&mut self, id: Arc<str>, cx: &mut ViewContext<Self>) {
self.code_action_providers
.retain(|provider| provider.id() != id);
self.refresh_code_actions(cx);
}
fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
let buffer = self.buffer.read(cx);
let newest_selection = self.selections.newest_anchor().clone();
@@ -4542,8 +4492,7 @@ impl Editor {
if !user_requested
&& (!self.enable_inline_completions
|| !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx)
|| !self.is_focused(cx)
|| buffer.read(cx).is_empty())
|| !self.is_focused(cx))
{
self.discard_inline_completion(false, cx);
return None;
@@ -4813,7 +4762,6 @@ impl Editor {
|| (!self.completion_tasks.is_empty() && !self.has_active_inline_completion()));
if completions_menu_has_precedence
|| !offset_selection.is_empty()
|| !self.enable_inline_completions
|| self
.active_inline_completion
.as_ref()
@@ -4936,8 +4884,8 @@ impl Editor {
&mut self,
cx: &mut ViewContext<Self>,
) -> Option<InlineCompletionMenuHint> {
let provider = self.inline_completion_provider()?;
if self.has_active_inline_completion() {
let provider_name = self.inline_completion_provider()?.display_name();
let editor_snapshot = self.snapshot(cx);
let text = match &self.active_inline_completion.as_ref()?.completion {
@@ -4954,11 +4902,12 @@ impl Editor {
}
};
Some(InlineCompletionMenuHint::Loaded { text })
} else if provider.is_refreshing(cx) {
Some(InlineCompletionMenuHint::Loading)
Some(InlineCompletionMenuHint {
provider_name,
text,
})
} else {
Some(InlineCompletionMenuHint::None)
None
}
}
@@ -13610,8 +13559,6 @@ pub trait CompletionProvider {
}
pub trait CodeActionProvider {
fn id(&self) -> Arc<str>;
fn code_actions(
&self,
buffer: &Model<Buffer>,
@@ -13630,10 +13577,6 @@ pub trait CodeActionProvider {
}
impl CodeActionProvider for Model<Project> {
fn id(&self) -> Arc<str> {
"project".into()
}
fn code_actions(
&self,
buffer: &Model<Buffer>,

View File

@@ -543,29 +543,8 @@ impl EditorElement {
// and run the selection logic.
modifiers.alt = false;
} else {
let scroll_position_row =
position_map.scroll_pixel_position.y / position_map.line_height;
let display_row = (((event.position - gutter_hitbox.bounds.origin).y
+ position_map.scroll_pixel_position.y)
/ position_map.line_height)
as u32;
let multi_buffer_row = position_map
.snapshot
.display_point_to_point(
DisplayPoint::new(DisplayRow(display_row), 0),
Bias::Right,
)
.row;
let line_offset_from_top = display_row - scroll_position_row as u32;
// if double click is made without alt, open the corresponding excerp
editor.open_excerpts_common(
Some(JumpData::MultiBufferRow {
row: MultiBufferRow(multi_buffer_row),
line_offset_from_top,
}),
false,
cx,
);
editor.open_excerpts(&OpenExcerpts, cx);
return;
}
}
@@ -3979,7 +3958,13 @@ impl EditorElement {
let Some(()) = line.paint(hitbox.origin, line_height, cx).log_err() else {
continue;
};
cx.set_cursor_style(CursorStyle::PointingHand, hitbox);
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
if is_singleton {
cx.set_cursor_style(CursorStyle::IBeam, hitbox);
} else {
cx.set_cursor_style(CursorStyle::PointingHand, hitbox);
}
}
}

View File

@@ -18,28 +18,19 @@ use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
use wasmparser::Parser;
use wit_component::ComponentEncoder;
/// Currently, we compile with Rust's `wasm32-wasip1` target, which works with WASI `preview1`.
/// But the WASM component model is based on WASI `preview2`. So we need an 'adapter' WASM
/// module, which implements the `preview1` interface in terms of `preview2`.
///
/// Once Rust 1.78 is released, there will be a `wasm32-wasip2` target available, so we will
/// not need the adapter anymore.
const RUST_TARGET: &str = "wasm32-wasip1";
const WASI_ADAPTER_URL: &str =
"https://github.com/bytecodealliance/wasmtime/releases/download/v18.0.2/wasi_snapshot_preview1.reactor.wasm";
const RUST_TARGET: &str = "wasm32-wasip2";
/// Compiling Tree-sitter parsers from C to WASM requires Clang 17, and a WASM build of libc
/// and clang's runtime library. The `wasi-sdk` provides these binaries.
///
/// Once Clang 17 and its wasm target are available via system package managers, we won't need
/// to download this.
const WASI_SDK_URL: &str = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-21/";
const WASI_SDK_URL: &str = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/";
const WASI_SDK_ASSET_NAME: Option<&str> = if cfg!(target_os = "macos") {
Some("wasi-sdk-21.0-macos.tar.gz")
Some("wasi-sdk-24.0-macos.tar.gz")
} else if cfg!(any(target_os = "linux", target_os = "freebsd")) {
Some("wasi-sdk-21.0-linux.tar.gz")
Some("wasi-sdk-24.0-linux.tar.gz")
} else if cfg!(target_os = "windows") {
Some("wasi-sdk-21.0.m-mingw.tar.gz")
Some("wasi-sdk-24.0.m-mingw.tar.gz")
} else {
None
};
@@ -121,8 +112,6 @@ impl ExtensionBuilder {
options: CompileExtensionOptions,
) -> Result<(), anyhow::Error> {
self.install_rust_wasm_target_if_needed()?;
let adapter_bytes = self.install_wasi_preview1_adapter_if_needed().await?;
let cargo_toml_content = fs::read_to_string(extension_dir.join("Cargo.toml"))?;
let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content)?;
@@ -130,6 +119,7 @@ impl ExtensionBuilder {
"compiling Rust crate for extension {}",
extension_dir.display()
);
let output = util::command::new_std_command("cargo")
.args(["build", "--target", RUST_TARGET])
.args(options.release.then_some("--release"))
@@ -168,20 +158,12 @@ impl ExtensionBuilder {
let wasm_bytes = fs::read(&wasm_path)
.with_context(|| format!("failed to read output module `{}`", wasm_path.display()))?;
let encoder = ComponentEncoder::default()
.module(&wasm_bytes)?
.adapter("wasi_snapshot_preview1", &adapter_bytes)
.context("failed to load adapter module")?
.validate(true);
log::info!(
"encoding wasm component for extension {}",
extension_dir.display()
);
let component_bytes = encoder
.encode()
.context("failed to encode wasm component")?;
let component_bytes = wasm_bytes;
let component_bytes = self
.strip_custom_sections(&component_bytes)
@@ -379,38 +361,6 @@ impl ExtensionBuilder {
Ok(())
}
async fn install_wasi_preview1_adapter_if_needed(&self) -> Result<Vec<u8>> {
let cache_path = self.cache_dir.join("wasi_snapshot_preview1.reactor.wasm");
if let Ok(content) = fs::read(&cache_path) {
if Parser::is_core_wasm(&content) {
return Ok(content);
}
}
fs::remove_file(&cache_path).ok();
log::info!(
"downloading wasi adapter module to {}",
cache_path.display()
);
let mut response = self
.http
.get(WASI_ADAPTER_URL, AsyncBody::default(), true)
.await?;
let mut content = Vec::new();
let mut body = BufReader::new(response.body_mut());
body.read_to_end(&mut content).await?;
fs::write(&cache_path, &content)
.with_context(|| format!("failed to save file {}", cache_path.display()))?;
if !Parser::is_core_wasm(&content) {
bail!("downloaded wasi adapter is invalid");
}
Ok(content)
}
async fn install_wasi_sdk_if_needed(&self) -> Result<PathBuf> {
let url = if let Some(asset_name) = WASI_SDK_ASSET_NAME {
format!("{WASI_SDK_URL}/{asset_name}")

View File

@@ -175,7 +175,7 @@ impl ExtensionManifest {
.await
.with_context(|| format!("failed to load {extension_name} extension.toml"))?;
toml::from_str(&manifest_content)
.with_context(|| format!("invalid extension.json for extension {extension_name}"))
.with_context(|| format!("invalid extension.toml for extension {extension_name}"))
}
}
}

View File

@@ -84,7 +84,7 @@ impl HostWorktree for WasmState {
latest::HostWorktree::which(self, delegate, binary_name).await
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
Ok(())
}
}

View File

@@ -92,7 +92,7 @@ impl HostWorktree for WasmState {
latest::HostWorktree::which(self, delegate, binary_name).await
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
// We only ever hand out borrows of worktrees.
Ok(())
}

View File

@@ -147,7 +147,7 @@ impl HostWorktree for WasmState {
latest::HostWorktree::which(self, delegate, binary_name).await
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
// We only ever hand out borrows of worktrees.
Ok(())
}

View File

@@ -240,7 +240,7 @@ impl HostKeyValueStore for WasmState {
kv_store.insert(key, value).await.to_wasmtime_result()
}
fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
async fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
// We only ever hand out borrows of key-value stores.
Ok(())
}
@@ -282,7 +282,7 @@ impl HostWorktree for WasmState {
latest::HostWorktree::which(self, delegate, binary_name).await
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
// We only ever hand out borrows of worktrees.
Ok(())
}
@@ -350,7 +350,7 @@ impl http_client::HostHttpResponseStream for WasmState {
.to_wasmtime_result()
}
fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
async fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
Ok(())
}
}

View File

@@ -259,7 +259,7 @@ impl HostKeyValueStore for WasmState {
kv_store.insert(key, value).await.to_wasmtime_result()
}
fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
async fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
// We only ever hand out borrows of key-value stores.
Ok(())
}
@@ -275,7 +275,7 @@ impl HostProject for WasmState {
Ok(project.worktree_ids())
}
fn drop(&mut self, _project: Resource<Project>) -> Result<()> {
async fn drop(&mut self, _project: Resource<Project>) -> Result<()> {
// We only ever hand out borrows of projects.
Ok(())
}
@@ -325,7 +325,7 @@ impl HostWorktree for WasmState {
Ok(delegate.which(binary_name).await)
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
// We only ever hand out borrows of worktrees.
Ok(())
}
@@ -393,7 +393,7 @@ impl http_client::HostHttpResponseStream for WasmState {
.to_wasmtime_result()
}
fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
async fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
Ok(())
}
}

View File

@@ -59,9 +59,9 @@ impl FeatureFlag for ToolUseFeatureFlag {
}
}
pub struct PredictEditsFeatureFlag;
impl FeatureFlag for PredictEditsFeatureFlag {
const NAME: &'static str = "predict-edits";
pub struct ZetaFeatureFlag;
impl FeatureFlag for ZetaFeatureFlag {
const NAME: &'static str = "zeta";
}
pub struct GitUiFeatureFlag;

View File

@@ -1,19 +0,0 @@
[package]
name = "fireworks"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/fireworks.rs"
[dependencies]
anyhow.workspace = true
futures.workspace = true
http_client.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

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

View File

@@ -1,173 +0,0 @@
use anyhow::{anyhow, Result};
use futures::AsyncReadExt;
use http_client::{http::HeaderMap, AsyncBody, HttpClient, Method, Request as HttpRequest};
use serde::{Deserialize, Serialize};
pub const FIREWORKS_API_URL: &str = "https://api.openai.com/v1";
#[derive(Debug, Serialize, Deserialize)]
pub struct CompletionRequest {
pub model: String,
pub prompt: String,
pub max_tokens: u32,
pub temperature: f32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prediction: Option<Prediction>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rewrite_speculation: Option<bool>,
}
#[derive(Clone, Deserialize, Serialize, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Prediction {
Content { content: String },
}
#[derive(Debug)]
pub struct Response {
pub completion: CompletionResponse,
pub headers: Headers,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CompletionResponse {
pub id: String,
pub object: String,
pub created: u64,
pub model: String,
pub choices: Vec<CompletionChoice>,
pub usage: Usage,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CompletionChoice {
pub text: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct Headers {
pub server_processing_time: Option<f64>,
pub request_id: Option<String>,
pub prompt_tokens: Option<u32>,
pub speculation_generated_tokens: Option<u32>,
pub cached_prompt_tokens: Option<u32>,
pub backend_host: Option<String>,
pub num_concurrent_requests: Option<u32>,
pub deployment: Option<String>,
pub tokenizer_queue_duration: Option<f64>,
pub tokenizer_duration: Option<f64>,
pub prefill_queue_duration: Option<f64>,
pub prefill_duration: Option<f64>,
pub generation_queue_duration: Option<f64>,
}
impl Headers {
pub fn parse(headers: &HeaderMap) -> Self {
Headers {
request_id: headers
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.map(String::from),
server_processing_time: headers
.get("fireworks-server-processing-time")
.and_then(|v| v.to_str().ok()?.parse().ok()),
prompt_tokens: headers
.get("fireworks-prompt-tokens")
.and_then(|v| v.to_str().ok()?.parse().ok()),
speculation_generated_tokens: headers
.get("fireworks-speculation-generated-tokens")
.and_then(|v| v.to_str().ok()?.parse().ok()),
cached_prompt_tokens: headers
.get("fireworks-cached-prompt-tokens")
.and_then(|v| v.to_str().ok()?.parse().ok()),
backend_host: headers
.get("fireworks-backend-host")
.and_then(|v| v.to_str().ok())
.map(String::from),
num_concurrent_requests: headers
.get("fireworks-num-concurrent-requests")
.and_then(|v| v.to_str().ok()?.parse().ok()),
deployment: headers
.get("fireworks-deployment")
.and_then(|v| v.to_str().ok())
.map(String::from),
tokenizer_queue_duration: headers
.get("fireworks-tokenizer-queue-duration")
.and_then(|v| v.to_str().ok()?.parse().ok()),
tokenizer_duration: headers
.get("fireworks-tokenizer-duration")
.and_then(|v| v.to_str().ok()?.parse().ok()),
prefill_queue_duration: headers
.get("fireworks-prefill-queue-duration")
.and_then(|v| v.to_str().ok()?.parse().ok()),
prefill_duration: headers
.get("fireworks-prefill-duration")
.and_then(|v| v.to_str().ok()?.parse().ok()),
generation_queue_duration: headers
.get("fireworks-generation-queue-duration")
.and_then(|v| v.to_str().ok()?.parse().ok()),
}
}
}
pub async fn complete(
client: &dyn HttpClient,
api_url: &str,
api_key: &str,
request: CompletionRequest,
) -> Result<Response> {
let uri = format!("{api_url}/completions");
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key));
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
let mut response = client.send(request).await?;
if response.status().is_success() {
let headers = Headers::parse(response.headers());
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
Ok(Response {
completion: serde_json::from_str(&body)?,
headers,
})
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
#[derive(Deserialize)]
struct FireworksResponse {
error: FireworksError,
}
#[derive(Deserialize)]
struct FireworksError {
message: String,
}
match serde_json::from_str::<FireworksResponse>(&body) {
Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
"Failed to connect to Fireworks API: {}",
response.error.message,
)),
_ => Err(anyhow!(
"Failed to connect to Fireworks API: {} {}",
response.status(),
body,
)),
}
}
}

View File

@@ -47,9 +47,12 @@ windows.workspace = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
ashpd.workspace = true
which.workspace = true
shlex.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
[features]
test-support = ["gpui/test-support", "git/test-support"]

View File

@@ -9,6 +9,9 @@ use git::GitHostingProviderRegistry;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use ashpd::desktop::trash;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use smol::process::Command;
#[cfg(unix)]
use std::os::fd::AsFd;
#[cfg(unix)]
@@ -518,24 +521,7 @@ impl Fs for RealFs {
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
smol::unblock(move || {
let mut tmp_file = if cfg!(any(target_os = "linux", target_os = "freebsd")) {
// Use the directory of the destination as temp dir to avoid
// invalid cross-device link error, and XDG_CACHE_DIR for fallback.
// See https://github.com/zed-industries/zed/pull/8437 for more details.
NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))
} else if cfg!(target_os = "windows") {
// If temp dir is set to a different drive than the destination,
// we receive error:
//
// failed to persist temporary file:
// The system cannot move the file to a different disk drive. (os error 17)
//
// So we use the directory of the destination as a temp dir to avoid it.
// https://github.com/zed-industries/zed/issues/16571
NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))
} else {
NamedTempFile::new()
}?;
let mut tmp_file = create_temp_file(&path)?;
tmp_file.write_all(data.as_bytes())?;
tmp_file.persist(path)?;
Ok::<(), anyhow::Error>(())
@@ -550,13 +536,43 @@ impl Fs for RealFs {
if let Some(path) = path.parent() {
self.create_dir(path).await?;
}
let file = smol::fs::File::create(path).await?;
let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
for chunk in chunks(text, line_ending) {
writer.write_all(chunk.as_bytes()).await?;
match smol::fs::File::create(path).await {
Ok(file) => {
let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
for chunk in chunks(text, line_ending) {
writer.write_all(chunk.as_bytes()).await?;
}
writer.flush().await?;
Ok(())
}
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
let target_path = path.to_path_buf();
let temp_file = smol::unblock(move || create_temp_file(&target_path)).await?;
let temp_path = temp_file.into_temp_path();
let temp_path_for_write = temp_path.to_path_buf();
let async_file = smol::fs::OpenOptions::new()
.write(true)
.open(&temp_path)
.await?;
let mut writer = smol::io::BufWriter::with_capacity(buffer_size, async_file);
for chunk in chunks(text, line_ending) {
writer.write_all(chunk.as_bytes()).await?;
}
writer.flush().await?;
write_to_file_as_root(temp_path_for_write, path.to_path_buf()).await
} else {
// Todo: Implement for Mac and Windows
Err(e.into())
}
}
Err(e) => Err(e.into()),
}
writer.flush().await?;
Ok(())
}
async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
@@ -1963,6 +1979,84 @@ fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
})
}
fn create_temp_file(path: &Path) -> Result<NamedTempFile> {
let temp_file = if cfg!(any(target_os = "linux", target_os = "freebsd")) {
// Use the directory of the destination as temp dir to avoid
// invalid cross-device link error, and XDG_CACHE_DIR for fallback.
// See https://github.com/zed-industries/zed/pull/8437 for more details.
NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))?
} else if cfg!(target_os = "windows") {
// If temp dir is set to a different drive than the destination,
// we receive error:
//
// failed to persist temporary file:
// The system cannot move the file to a different disk drive. (os error 17)
//
// So we use the directory of the destination as a temp dir to avoid it.
// https://github.com/zed-industries/zed/issues/16571
NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))?
} else {
NamedTempFile::new()?
};
Ok(temp_file)
}
#[cfg(target_os = "macos")]
async fn write_to_file_as_root(_temp_file_path: PathBuf, _target_file_path: PathBuf) -> Result<()> {
unimplemented!("write_to_file_as_root is not implemented")
}
#[cfg(target_os = "windows")]
async fn write_to_file_as_root(_temp_file_path: PathBuf, _target_file_path: PathBuf) -> Result<()> {
unimplemented!("write_to_file_as_root is not implemented")
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
async fn write_to_file_as_root(temp_file_path: PathBuf, target_file_path: PathBuf) -> Result<()> {
use shlex::try_quote;
use std::os::unix::fs::PermissionsExt;
use which::which;
let pkexec_path = smol::unblock(|| which("pkexec"))
.await
.map_err(|_| anyhow::anyhow!("pkexec not found in PATH"))?;
let script_file = smol::unblock(move || {
let script_file = tempfile::Builder::new()
.prefix("write-to-file-as-root-")
.tempfile_in(paths::temp_dir())?;
writeln!(
script_file.as_file(),
"#!/usr/bin/env sh\nset -eu\ncat \"{}\" > \"{}\"",
try_quote(&temp_file_path.to_string_lossy())?,
try_quote(&target_file_path.to_string_lossy())?
)?;
let mut perms = script_file.as_file().metadata()?.permissions();
perms.set_mode(0o700); // rwx------
script_file.as_file().set_permissions(perms)?;
Result::<_>::Ok(script_file)
})
.await?;
let script_path = script_file.into_temp_path();
let output = Command::new(&pkexec_path)
.arg("--disable-internal-agent")
.arg(&script_path)
.output()
.await?;
if !output.status.success() {
return Err(anyhow::anyhow!("Failed to write to file as root"));
}
Ok(())
}
pub fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {

View File

@@ -2,7 +2,7 @@ use crate::{
ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
Pixels, Point, SharedString, Size, TextRun, TextStyle, Truncate, WhiteSpace, WindowContext,
WrappedLine, WrappedLineLayout, TOOLTIP_DELAY,
WrappedLine, TOOLTIP_DELAY,
};
use anyhow::anyhow;
use parking_lot::{Mutex, MutexGuard};
@@ -443,36 +443,6 @@ impl TextLayout {
None
}
/// Retrieve the layout for the line containing the given byte index.
pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<WrappedLineLayout>> {
let element_state = self.lock();
let element_state = element_state
.as_ref()
.expect("measurement has not been performed");
let bounds = element_state
.bounds
.expect("prepaint has not been performed");
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
let mut line_start_ix = 0;
for line in &element_state.lines {
let line_end_ix = line_start_ix + line.len();
if index < line_start_ix {
break;
} else if index > line_end_ix {
line_origin.y += line.size(line_height).height;
line_start_ix = line_end_ix + 1;
continue;
} else {
return Some(line.layout.clone());
}
}
None
}
/// The bounds of this layout.
pub fn bounds(&self) -> Bounds<Pixels> {
self.0.lock().as_ref().unwrap().bounds.unwrap()

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use copilot::{Copilot, Status};
use editor::{scroll::Autoscroll, Editor};
use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag};
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
use fs::Fs;
use gpui::{
actions, div, pulsating_between, Action, Animation, AnimationExt, AppContext,
@@ -201,14 +201,14 @@ impl Render for InlineCompletionButton {
);
}
InlineCompletionProvider::Zed => {
if !cx.has_flag::<PredictEditsFeatureFlag>() {
InlineCompletionProvider::Zeta => {
if !cx.has_flag::<ZetaFeatureFlag>() {
return div();
}
let this = cx.view().clone();
let button = IconButton::new("zeta", IconName::ZedPredict)
.tooltip(|cx| Tooltip::text("Edit Prediction", cx));
.tooltip(|cx| Tooltip::text("Zed Predict", cx));
let is_refreshing = self
.inline_completion_provider

View File

@@ -203,7 +203,7 @@ pub enum InlineCompletionProvider {
#[default]
Copilot,
Supermaven,
Zed,
Zeta,
}
/// The settings for inline completions, such as [GitHub Copilot](https://github.com/features/copilot)

View File

@@ -212,18 +212,9 @@ impl LspAdapter for TypeScriptLspAdapter {
_ => None,
}?;
let one_line = |s: &str| s.replace(" ", "").replace('\n', " ");
let text = if let Some(description) = item
.label_details
.as_ref()
.and_then(|label_details| label_details.description.as_ref())
{
format!("{} {}", item.label, one_line(description))
} else if let Some(detail) = &item.detail {
format!("{} {}", item.label, one_line(detail))
} else {
item.label.clone()
let text = match &item.detail {
Some(detail) => format!("{} {}", item.label, detail),
None => item.label.clone(),
};
Some(language::CodeLabel {

View File

@@ -14,7 +14,7 @@ use parser::{parse_links_only, parse_markdown, MarkdownEvent, MarkdownTag, Markd
use std::{iter, mem, ops::Range, rc::Rc, sync::Arc};
use theme::SyntaxTheme;
use ui::prelude::*;
use ui::{prelude::*, Tooltip};
use util::{ResultExt, TryFutureExt};
#[derive(Clone)]
@@ -667,6 +667,31 @@ impl Element for MarkdownElement {
}
MarkdownTagEnd::CodeBlock => {
builder.trim_trailing_newline();
builder.flush_text();
builder.modify_current_div(|el| {
let id =
ElementId::NamedInteger("copy-markdown-code".into(), range.end);
let copy_button = div().absolute().top_1().right_1().w_5().child(
IconButton::new(id, IconName::Copy)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(|cx| Tooltip::text("Copy Code Block", cx))
.on_click({
let code = without_fences(
parsed_markdown.source()[range.clone()].trim(),
)
.to_string();
move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
code.clone(),
))
}
}),
);
el.child(copy_button)
});
builder.pop_div();
builder.pop_code_block();
if self.style.code_block.text.is_some() {
@@ -917,6 +942,13 @@ impl MarkdownElementBuilder {
self.div_stack.push(div);
}
fn modify_current_div(&mut self, f: impl FnOnce(AnyDiv) -> AnyDiv) {
self.flush_text();
if let Some(div) = self.div_stack.pop() {
self.div_stack.push(f(div));
}
}
fn pop_div(&mut self) {
self.flush_text();
let div = self.div_stack.pop().unwrap().into_any_element();
@@ -1001,7 +1033,7 @@ impl MarkdownElementBuilder {
}
}
fn flush_text(&mut self) {
pub fn flush_text(&mut self) {
let line = mem::take(&mut self.pending_line);
if line.text.is_empty() {
return;
@@ -1220,3 +1252,43 @@ impl RenderedText {
.find(|link| link.source_range.contains(&source_index))
}
}
/// Some markdown blocks are indented, and others have e.g. ```rust … ``` around them.
/// If this block is fenced with backticks, strip them off (and the language name).
/// We use this when copying code blocks to the clipboard.
fn without_fences(mut markdown: &str) -> &str {
if let Some(opening_backticks) = markdown.find("```") {
markdown = &markdown[opening_backticks..];
// Trim off the next newline. This also trims off a language name if it's there.
if let Some(newline) = markdown.find('\n') {
markdown = &markdown[newline + 1..];
}
};
if let Some(closing_backticks) = markdown.rfind("```") {
markdown = &markdown[..closing_backticks];
};
markdown
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_without_fences() {
let input = "```rust\nlet x = 5;\n```";
assert_eq!(without_fences(input), "let x = 5;\n");
let input = " ```\nno language\n``` ";
assert_eq!(without_fences(input), "no language\n");
let input = "plain text";
assert_eq!(without_fences(input), "plain text");
let input = "```python\nprint('hello')\nprint('world')\n```";
assert_eq!(without_fences(input), "print('hello')\nprint('world')\n");
}
}

View File

@@ -269,7 +269,7 @@ struct ExcerptIdMapping {
/// A range of text from a single [`Buffer`], to be shown as an [`Excerpt`].
/// These ranges are relative to the buffer itself
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct ExcerptRange<T> {
/// The full range of text to be shown in the excerpt.
pub context: Range<T>,

View File

@@ -83,8 +83,8 @@ fn get_max_tokens(name: &str) -> usize {
"codellama" | "starcoder2" => 16384,
"mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "qwen2.5-coder"
| "dolphin-mixtral" => 32768,
"llama3.1" | "phi3" | "phi3.5" | "phi4" | "command-r" | "deepseek-coder-v2"
| "yi-coder" | "llama3.2" => 128000,
"llama3.1" | "phi3" | "phi3.5" | "command-r" | "deepseek-coder-v2" | "yi-coder"
| "llama3.2" => 128000,
_ => DEFAULT_TOKENS,
}
.clamp(1, MAXIMUM_TOKENS)

View File

@@ -394,12 +394,10 @@ impl PartialEq for PanelEntry {
Self::FoldedDirs(FoldedDirsEntry {
worktree_id: worktree_id_a,
entries: entries_a,
..
}),
Self::FoldedDirs(FoldedDirsEntry {
worktree_id: worktree_id_b,
entries: entries_b,
..
}),
) => worktree_id_a == worktree_id_b && entries_a == entries_b,
(Self::Outline(a), Self::Outline(b)) => a == b,
@@ -523,25 +521,13 @@ impl SearchData {
}
}
#[derive(Clone, Debug, Eq)]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct OutlineEntryExcerpt {
id: ExcerptId,
buffer_id: BufferId,
range: ExcerptRange<language::Anchor>,
}
impl PartialEq for OutlineEntryExcerpt {
fn eq(&self, other: &Self) -> bool {
self.buffer_id == other.buffer_id && self.id == other.id
}
}
impl Hash for OutlineEntryExcerpt {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
(self.buffer_id, self.id).hash(state)
}
}
#[derive(Clone, Debug, Eq)]
struct OutlineEntryOutline {
buffer_id: BufferId,
@@ -551,13 +537,24 @@ struct OutlineEntryOutline {
impl PartialEq for OutlineEntryOutline {
fn eq(&self, other: &Self) -> bool {
self.buffer_id == other.buffer_id && self.excerpt_id == other.excerpt_id
self.buffer_id == other.buffer_id
&& self.excerpt_id == other.excerpt_id
&& self.outline.depth == other.outline.depth
&& self.outline.range == other.outline.range
&& self.outline.text == other.outline.text
}
}
impl Hash for OutlineEntryOutline {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
(self.buffer_id, self.excerpt_id).hash(state);
(
self.buffer_id,
self.excerpt_id,
self.outline.depth,
&self.outline.range,
&self.outline.text,
)
.hash(state);
}
}
@@ -1060,8 +1057,7 @@ impl OutlinePanel {
FsEntry::Directory(..) => None,
})
.skip_while(|id| *id != buffer_id)
.skip(1)
.next();
.nth(1);
if let Some(previous_buffer_id) = previous_buffer_id {
if !active_editor.read(cx).buffer_folded(previous_buffer_id, cx)
{
@@ -1813,7 +1809,10 @@ impl OutlinePanel {
}
fn reveal_entry_for_selection(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
if !self.active || !OutlinePanelSettings::get_global(cx).auto_reveal_entries {
if !self.active
|| !OutlinePanelSettings::get_global(cx).auto_reveal_entries
|| self.focus_handle.contains_focused(cx)
{
return;
}
let project = self.project.clone();
@@ -4965,6 +4964,7 @@ impl GenerationState {
#[cfg(test)]
mod tests {
use db::indoc;
use gpui::{TestAppContext, VisualTestContext, WindowHandle};
use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher};
use pretty_assertions::assert_eq;
@@ -5498,6 +5498,312 @@ mod tests {
});
}
#[gpui::test]
async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
init_test(cx);
let root = "/root";
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
root,
json!({
"src": {
"lib.rs": indoc!("
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct OutlineEntryExcerpt {
id: ExcerptId,
buffer_id: BufferId,
range: ExcerptRange<language::Anchor>,
}"),
}
}),
)
.await;
let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
project.read_with(cx, |project, _| {
project.languages().add(Arc::new(
rust_lang()
.with_outline_query(
r#"
(struct_item
(visibility_modifier)? @context
"struct" @context
name: (_) @name) @item
(field_declaration
(visibility_modifier)? @context
name: (_) @name) @item
"#,
)
.unwrap(),
))
});
let workspace = add_outline_panel(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let outline_panel = outline_panel(&workspace, cx);
outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
let _editor = workspace
.update(cx, |workspace, cx| {
workspace.open_abs_path(PathBuf::from("/root/src/lib.rs"), true, cx)
})
.unwrap()
.await
.expect("Failed to open Rust source file")
.downcast::<Editor>()
.expect("Should open an editor for Rust source file");
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
indoc!(
"
outline: struct OutlineEntryExcerpt
outline: id
outline: buffer_id
outline: range"
)
);
});
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.select_next(&SelectNext, cx);
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
indoc!(
"
outline: struct OutlineEntryExcerpt <==== selected
outline: id
outline: buffer_id
outline: range"
)
);
});
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.select_next(&SelectNext, cx);
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
indoc!(
"
outline: struct OutlineEntryExcerpt
outline: id <==== selected
outline: buffer_id
outline: range"
)
);
});
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.select_next(&SelectNext, cx);
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
indoc!(
"
outline: struct OutlineEntryExcerpt
outline: id
outline: buffer_id <==== selected
outline: range"
)
);
});
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.select_next(&SelectNext, cx);
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
indoc!(
"
outline: struct OutlineEntryExcerpt
outline: id
outline: buffer_id
outline: range <==== selected"
)
);
});
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.select_next(&SelectNext, cx);
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
indoc!(
"
outline: struct OutlineEntryExcerpt <==== selected
outline: id
outline: buffer_id
outline: range"
)
);
});
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.select_prev(&SelectPrev, cx);
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
indoc!(
"
outline: struct OutlineEntryExcerpt
outline: id
outline: buffer_id
outline: range <==== selected"
)
);
});
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.select_prev(&SelectPrev, cx);
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
indoc!(
"
outline: struct OutlineEntryExcerpt
outline: id
outline: buffer_id <==== selected
outline: range"
)
);
});
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.select_prev(&SelectPrev, cx);
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
indoc!(
"
outline: struct OutlineEntryExcerpt
outline: id <==== selected
outline: buffer_id
outline: range"
)
);
});
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.select_prev(&SelectPrev, cx);
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
indoc!(
"
outline: struct OutlineEntryExcerpt <==== selected
outline: id
outline: buffer_id
outline: range"
)
);
});
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.select_prev(&SelectPrev, cx);
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
indoc!(
"
outline: struct OutlineEntryExcerpt
outline: id
outline: buffer_id
outline: range <==== selected"
)
);
});
}
#[gpui::test(iterations = 10)]
async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
init_test(cx);

View File

@@ -54,8 +54,8 @@ use std::{
use theme::ThemeSettings;
use ui::{
prelude::*, v_flex, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind,
IndentGuideColors, IndentGuideLayout, KeyBinding, Label, ListItem, Scrollbar, ScrollbarState,
Tooltip,
IndentGuideColors, IndentGuideLayout, KeyBinding, Label, ListItem, ListItemSpacing, Scrollbar,
ScrollbarState, Tooltip,
};
use util::{maybe, paths::compare_paths, ResultExt, TakeUntilExt, TryFutureExt};
use workspace::{
@@ -3447,6 +3447,12 @@ impl ProjectPanel {
ListItem::new(entry_id.to_proto() as usize)
.indent_level(depth)
.indent_step_size(px(settings.indent_size))
.spacing(match settings.entry_spacing {
project_panel_settings::EntrySpacing::Comfortable => ListItemSpacing::Dense,
project_panel_settings::EntrySpacing::Standard => {
ListItemSpacing::ExtraDense
}
})
.selectable(false)
.when_some(canonical_path, |this, path| {
this.end_slot::<AnyElement>(

View File

@@ -18,11 +18,22 @@ pub enum ShowIndentGuides {
Never,
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EntrySpacing {
/// Comfortable spacing of entries.
#[default]
Comfortable,
/// The standard spacing of entries.
Standard,
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct ProjectPanelSettings {
pub button: bool,
pub default_width: Pixels,
pub dock: ProjectPanelDockPosition,
pub entry_spacing: EntrySpacing,
pub file_icons: bool,
pub folder_icons: bool,
pub git_status: bool,
@@ -90,6 +101,10 @@ pub struct ProjectPanelSettingsContent {
///
/// Default: left
pub dock: Option<ProjectPanelDockPosition>,
/// Spacing between worktree entries in the project panel.
///
/// Default: comfortable
pub entry_spacing: Option<EntrySpacing>,
/// Whether to show file icons in the project panel.
///
/// Default: true

View File

@@ -13,5 +13,5 @@ workspace = true
[dependencies]
gpui.workspace = true
itertools = { package = "itertools", version = "0.13" }
itertools = { package = "itertools", version = "0.14" }
smallvec.workspace = true

View File

@@ -10,7 +10,7 @@ use windows::Win32::{Foundation::HANDLE, System::Threading::GetProcessId};
use sysinfo::{Pid, Process, ProcessRefreshKind, RefreshKind, System, UpdateKind};
struct ProcessIdGetter {
pub struct ProcessIdGetter {
handle: i32,
fallback_pid: u32,
}
@@ -31,6 +31,10 @@ impl ProcessIdGetter {
}
Some(Pid::from_u32(pid as u32))
}
pub fn fallback_pid(&self) -> u32 {
self.fallback_pid
}
}
#[cfg(windows)]
@@ -62,6 +66,10 @@ impl ProcessIdGetter {
}
Some(Pid::from_u32(pid))
}
pub fn fallback_pid(&self) -> u32 {
self.fallback_pid
}
}
#[derive(Clone, Debug)]
@@ -96,6 +104,10 @@ impl PtyProcessInfo {
}
}
pub fn pid_getter(&self) -> &ProcessIdGetter {
&self.pid_getter
}
fn refresh(&mut self) -> Option<&Process> {
let pid = self.pid_getter.pid()?;
if self.system.refresh_processes_specifics(

View File

@@ -146,16 +146,13 @@ fn populate_pane_items(
cx: &mut ViewContext<Pane>,
) {
let mut item_index = pane.items_len();
let mut active_item_index = None;
for item in items {
if Some(item.item_id().as_u64()) == active_item {
active_item_index = Some(item_index);
}
let activate_item = Some(item.item_id().as_u64()) == active_item;
pane.add_item(Box::new(item), false, false, None, cx);
item_index += 1;
}
if let Some(index) = active_item_index {
pane.activate_item(index, false, false, cx);
if activate_item {
pane.activate_item(item_index, false, false, cx);
}
}
}

View File

@@ -31,7 +31,7 @@ use ui::{
};
use util::{ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent, PanelHandle},
dock::{DockPosition, Panel, PanelEvent},
item::SerializableItem,
move_active_item, move_item, pane,
ui::IconName,
@@ -75,7 +75,6 @@ pub struct TerminalPanel {
deferred_tasks: HashMap<TaskId, Task<()>>,
assistant_enabled: bool,
assistant_tab_bar_button: Option<AnyView>,
active: bool,
}
impl TerminalPanel {
@@ -83,6 +82,7 @@ impl TerminalPanel {
let project = workspace.project();
let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, cx);
let center = PaneGroup::new(pane.clone());
cx.focus_view(&pane);
let terminal_panel = Self {
center,
active_pane: pane,
@@ -95,7 +95,6 @@ impl TerminalPanel {
deferred_tasks: HashMap::default(),
assistant_enabled: false,
assistant_tab_bar_button: None,
active: false,
};
terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx);
terminal_panel
@@ -282,25 +281,6 @@ impl TerminalPanel {
}
}
if let Some(workspace) = workspace.upgrade() {
let should_focus = workspace
.update(&mut cx, |workspace, cx| {
workspace.active_item(cx).is_none()
&& workspace.is_dock_at_position_open(terminal_panel.position(cx), cx)
})
.unwrap_or(false);
if should_focus {
terminal_panel
.update(&mut cx, |panel, cx| {
panel.active_pane.update(cx, |pane, cx| {
pane.focus_active_item(cx);
});
})
.ok();
}
}
Ok(terminal_panel)
}
@@ -1359,9 +1339,7 @@ impl Panel for TerminalPanel {
}
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
let old_active = self.active;
self.active = active;
if !active || old_active == active || !self.has_no_terminals(cx) {
if !active || !self.has_no_terminals(cx) {
return;
}
cx.defer(|this, cx| {

View File

@@ -0,0 +1,36 @@
use gpui::{IntoElement, Render, ViewContext};
use ui::{prelude::*, tooltip_container, Divider};
pub struct TerminalTooltip {
title: SharedString,
pid: u32,
}
impl TerminalTooltip {
pub fn new(title: impl Into<SharedString>, pid: u32) -> Self {
Self {
title: title.into(),
pid,
}
}
}
impl Render for TerminalTooltip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
tooltip_container(cx, move |this, _cx| {
this.occlude()
.on_mouse_move(|_, cx| cx.stop_propagation())
.child(
v_flex()
.gap_1()
.child(Label::new(self.title.clone()))
.child(Divider::horizontal())
.child(
Label::new(format!("Process ID (PID): {}", self.pid))
.color(Color::Muted)
.size(LabelSize::Small),
),
)
})
}
}

View File

@@ -1,6 +1,7 @@
mod persistence;
pub mod terminal_element;
pub mod terminal_panel;
pub mod terminal_tab_tooltip;
use collections::HashSet;
use editor::{actions::SelectAll, scroll::Autoscroll, Editor};
@@ -26,13 +27,16 @@ use terminal::{
};
use terminal_element::{is_blank, TerminalElement};
use terminal_panel::TerminalPanel;
use terminal_tab_tooltip::TerminalTooltip;
use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Tooltip};
use util::{
paths::{PathWithPosition, SanitizedPath},
ResultExt,
};
use workspace::{
item::{BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams},
item::{
BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent,
},
register_serializable_item,
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace,
@@ -996,8 +1000,17 @@ impl Render for TerminalView {
impl Item for TerminalView {
type Event = ItemEvent;
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
Some(self.terminal().read(cx).title(false).into())
fn tab_tooltip_content(&self, cx: &AppContext) -> Option<TabTooltipContent> {
let terminal = self.terminal().read(cx);
let title = terminal.title(false);
let pid = terminal.pty_info.pid_getter().fallback_pid();
Some(TabTooltipContent::Custom(Box::new(
move |cx: &mut WindowContext| {
cx.new_view(|_| TerminalTooltip::new(title.clone(), pid))
.into()
},
)))
}
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {

View File

@@ -11,6 +11,7 @@ use crate::{prelude::*, Disclosure};
pub enum ListItemSpacing {
#[default]
Dense,
ExtraDense,
Sparse,
}
@@ -219,6 +220,7 @@ impl RenderOnce for ListItem {
.px(DynamicSpacing::Base06.rems(cx))
.map(|this| match self.spacing {
ListItemSpacing::Dense => this,
ListItemSpacing::ExtraDense => this.py_neg_px(),
ListItemSpacing::Sparse => this.py_1(),
})
.when(self.inset && !self.disabled, |this| {

View File

@@ -1204,7 +1204,7 @@ impl Vim {
.map_or(false, |provider| provider.show_completions_in_normal_mode()),
_ => false,
};
editor.set_inline_completions_enabled(enable_inline_completions, cx);
editor.set_inline_completions_enabled(enable_inline_completions);
});
cx.notify()
}

View File

@@ -178,6 +178,11 @@ impl TabContentParams {
}
}
pub enum TabTooltipContent {
Text(SharedString),
Custom(Box<dyn Fn(&mut WindowContext) -> AnyView>),
}
pub trait Item: FocusableView + EventEmitter<Self::Event> {
type Event;
@@ -206,6 +211,25 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
None
}
/// Returns the tab tooltip text.
///
/// Use this if you don't need to customize the tab tooltip content.
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
None
}
/// Returns the tab tooltip content.
///
/// By default this returns a Tooltip text from
/// `tab_tooltip_text`.
fn tab_tooltip_content(&self, cx: &AppContext) -> Option<TabTooltipContent> {
self.tab_tooltip_text(cx).map(TabTooltipContent::Text)
}
fn tab_description(&self, _: usize, _: &AppContext) -> Option<SharedString> {
None
}
fn to_item_events(_event: &Self::Event, _f: impl FnMut(ItemEvent)) {}
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
@@ -214,12 +238,6 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
false
}
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
None
}
fn tab_description(&self, _: usize, _: &AppContext) -> Option<SharedString> {
None
}
fn telemetry_event_text(&self) -> Option<&'static str> {
None
@@ -320,6 +338,10 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
fn preserve_preview(&self, _cx: &AppContext) -> bool {
false
}
fn include_in_nav_history() -> bool {
true
}
}
pub trait SerializableItem: Item {
@@ -394,10 +416,11 @@ pub trait ItemHandle: 'static + Send {
handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
) -> gpui::Subscription;
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement;
fn tab_icon(&self, cx: &WindowContext) -> Option<Icon>;
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
fn tab_tooltip_content(&self, cx: &AppContext) -> Option<TabTooltipContent>;
fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str>;
fn dragged_tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
@@ -464,6 +487,7 @@ pub trait ItemHandle: 'static + Send {
fn downgrade_item(&self) -> Box<dyn WeakItemHandle>;
fn workspace_settings<'a>(&self, cx: &'a AppContext) -> &'a WorkspaceSettings;
fn preserve_preview(&self, cx: &AppContext) -> bool;
fn include_in_nav_history(&self) -> bool;
}
pub trait WeakItemHandle: Send + Sync {
@@ -498,10 +522,6 @@ impl<T: Item> ItemHandle for View<T> {
self.focus_handle(cx)
}
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
self.read(cx).tab_tooltip_text(cx)
}
fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str> {
self.read(cx).telemetry_event_text()
}
@@ -518,6 +538,14 @@ impl<T: Item> ItemHandle for View<T> {
self.read(cx).tab_icon(cx)
}
fn tab_tooltip_content(&self, cx: &AppContext) -> Option<TabTooltipContent> {
self.read(cx).tab_tooltip_content(cx)
}
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
self.read(cx).tab_tooltip_text(cx)
}
fn dragged_tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
self.read(cx).tab_content(
TabContentParams {
@@ -877,6 +905,10 @@ impl<T: Item> ItemHandle for View<T> {
fn preserve_preview(&self, cx: &AppContext) -> bool {
self.read(cx).preserve_preview(cx)
}
fn include_in_nav_history(&self) -> bool {
T::include_in_nav_history()
}
}
impl From<Box<dyn ItemHandle>> for AnyView {

View File

@@ -1,7 +1,7 @@
use crate::{
item::{
ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
ShowDiagnostics, TabContentParams, WeakItemHandle,
ShowDiagnostics, TabContentParams, TabTooltipContent, WeakItemHandle,
},
move_item,
notifications::NotifyResultExt,
@@ -206,6 +206,7 @@ pub enum Event {
},
ActivateItem {
local: bool,
focus_changed: bool,
},
Remove {
focus_on_pane: Option<View<Pane>>,
@@ -236,7 +237,7 @@ impl fmt::Debug for Event {
.debug_struct("AddItem")
.field("item", &item.item_id())
.finish(),
Event::ActivateItem { local } => f
Event::ActivateItem { local, .. } => f
.debug_struct("ActivateItem")
.field("local", local)
.finish(),
@@ -1092,9 +1093,6 @@ impl Pane {
prev_item.deactivated(cx);
}
}
cx.emit(Event::ActivateItem {
local: activate_pane,
});
if let Some(newly_active_item) = self.items.get(index) {
self.activation_history
@@ -1114,6 +1112,11 @@ impl Pane {
self.focus_active_item(cx);
}
cx.emit(Event::ActivateItem {
local: activate_pane,
focus_changed: focus_item,
});
if !self.is_tab_pinned(index) {
self.tab_bar_scroll_handle
.scroll_to_item(index - self.pinned_tab_count);
@@ -2146,8 +2149,11 @@ impl Pane {
this.drag_split_direction = None;
this.handle_external_paths_drop(paths, cx)
}))
.when_some(item.tab_tooltip_text(cx), |tab, text| {
tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
.when_some(item.tab_tooltip_content(cx), |tab, content| match content {
TabTooltipContent::Text(text) => {
tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
}
TabTooltipContent::Custom(element_fn) => tab.tooltip(move |cx| element_fn(cx)),
})
.start_slot::<Indicator>(indicator)
.map(|this| {
@@ -3095,8 +3101,14 @@ impl Render for Pane {
impl ItemNavHistory {
pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
self.history
.push(data, self.item.clone(), self.is_preview, cx);
if self
.item
.upgrade()
.is_some_and(|item| item.include_in_nav_history())
{
self.history
.push(data, self.item.clone(), self.is_preview, cx);
}
}
pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {

View File

@@ -2295,19 +2295,6 @@ impl Workspace {
}
}
pub fn is_dock_at_position_open(
&self,
position: DockPosition,
cx: &mut ViewContext<Self>,
) -> bool {
let dock = match position {
DockPosition::Left => &self.left_dock,
DockPosition::Bottom => &self.bottom_dock,
DockPosition::Right => &self.right_dock,
};
dock.read(cx).is_open()
}
pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
let dock = match dock_side {
DockPosition::Left => &self.left_dock,
@@ -2988,13 +2975,15 @@ impl Workspace {
match target {
Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
Some(ActivateInDirectionTarget::Dock(dock)) => {
dock.update(cx, |dock, cx| {
// Defer this to avoid a panic when the dock's active panel is already on the stack.
cx.defer(move |cx| {
let dock = dock.read(cx);
if let Some(panel) = dock.active_panel() {
panel.focus_handle(cx).focus(cx);
} else {
log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
}
});
})
}
None => {}
}
@@ -3112,7 +3101,10 @@ impl Workspace {
pane::Event::Remove { focus_on_pane } => {
self.remove_pane(pane, focus_on_pane.clone(), cx);
}
pane::Event::ActivateItem { local } => {
pane::Event::ActivateItem {
local,
focus_changed,
} => {
cx.on_next_frame(|_, cx| {
cx.invalidate_character_coordinates();
});
@@ -3127,6 +3119,7 @@ impl Workspace {
self.active_item_path_changed(cx);
self.update_active_view_for_followers(cx);
}
serialize_workspace = *focus_changed || &pane != self.active_pane();
}
pane::Event::UserSavedItem { item, save_intent } => {
cx.emit(Event::UserSavedItem {

View File

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

View File

@@ -1 +1 @@
stable
dev

View File

@@ -4,7 +4,7 @@ use client::Client;
use collections::HashMap;
use copilot::{Copilot, CopilotCompletionProvider};
use editor::{Editor, EditorMode};
use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag};
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
use gpui::{AnyWindowHandle, AppContext, Context, ViewContext, WeakView};
use language::language_settings::{all_language_settings, InlineCompletionProvider};
use settings::SettingsStore;
@@ -49,11 +49,11 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
});
}
if cx.has_flag::<PredictEditsFeatureFlag>() {
if cx.has_flag::<ZetaFeatureFlag>() {
cx.on_action(clear_zeta_edit_history);
}
cx.observe_flag::<PredictEditsFeatureFlag, _>({
cx.observe_flag::<ZetaFeatureFlag, _>({
let editors = editors.clone();
let client = client.clone();
move |active, cx| {
@@ -164,11 +164,8 @@ fn assign_inline_completion_provider(
editor.set_inline_completion_provider(Some(provider), cx);
}
}
language::language_settings::InlineCompletionProvider::Zed => {
if cx.has_flag::<PredictEditsFeatureFlag>()
|| (cfg!(debug_assertions) && client.status().borrow().is_connected())
{
language::language_settings::InlineCompletionProvider::Zeta => {
if cx.has_flag::<ZetaFeatureFlag>() || cfg!(debug_assertions) {
let zeta = zeta::Zeta::register(client.clone(), cx);
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
if buffer.read(cx).file().is_some() {

View File

@@ -39,7 +39,6 @@ telemetry.workspace = true
telemetry_events.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true

View File

@@ -1,161 +0,0 @@
use std::cmp;
use crate::InlineCompletion;
use gpui::{
point, prelude::*, quad, size, AnyElement, AppContext, Bounds, Corners, Edges, HighlightStyle,
Hsla, StyledText, TextLayout, TextStyle,
};
use language::OffsetRangeExt;
use settings::Settings;
use theme::ThemeSettings;
use ui::prelude::*;
pub struct CompletionDiffElement {
element: AnyElement,
text_layout: TextLayout,
cursor_offset: usize,
}
impl CompletionDiffElement {
pub fn new(completion: &InlineCompletion, cx: &AppContext) -> Self {
let mut diff = completion
.snapshot
.text_for_range(completion.excerpt_range.clone())
.collect::<String>();
let mut cursor_offset_in_diff = None;
let mut delta = 0;
let mut diff_highlights = Vec::new();
for (old_range, new_text) in completion.edits.iter() {
let old_range = old_range.to_offset(&completion.snapshot);
if cursor_offset_in_diff.is_none() && completion.cursor_offset <= old_range.end {
cursor_offset_in_diff =
Some(completion.cursor_offset - completion.excerpt_range.start + delta);
}
let old_start_in_diff = old_range.start - completion.excerpt_range.start + delta;
let old_end_in_diff = old_range.end - completion.excerpt_range.start + delta;
if old_start_in_diff < old_end_in_diff {
diff_highlights.push((
old_start_in_diff..old_end_in_diff,
HighlightStyle {
background_color: Some(cx.theme().status().deleted_background),
strikethrough: Some(gpui::StrikethroughStyle {
thickness: px(1.),
color: Some(cx.theme().colors().text_muted),
}),
..Default::default()
},
));
}
if !new_text.is_empty() {
diff.insert_str(old_end_in_diff, new_text);
diff_highlights.push((
old_end_in_diff..old_end_in_diff + new_text.len(),
HighlightStyle {
background_color: Some(cx.theme().status().created_background),
..Default::default()
},
));
delta += new_text.len();
}
}
let cursor_offset_in_diff = cursor_offset_in_diff
.unwrap_or_else(|| completion.cursor_offset - completion.excerpt_range.start + delta);
let settings = ThemeSettings::get_global(cx).clone();
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_size: settings.buffer_font_size(cx).into(),
font_family: settings.buffer_font.family,
font_features: settings.buffer_font.features,
font_fallbacks: settings.buffer_font.fallbacks,
line_height: relative(settings.buffer_line_height.value()),
font_weight: settings.buffer_font.weight,
font_style: settings.buffer_font.style,
..Default::default()
};
let element = StyledText::new(diff).with_highlights(&text_style, diff_highlights);
let text_layout = element.layout().clone();
CompletionDiffElement {
element: element.into_any_element(),
text_layout,
cursor_offset: cursor_offset_in_diff,
}
}
}
impl IntoElement for CompletionDiffElement {
type Element = Self;
fn into_element(self) -> Self {
self
}
}
impl Element for CompletionDiffElement {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&gpui::GlobalElementId>,
cx: &mut WindowContext,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
(self.element.request_layout(cx), ())
}
fn prepaint(
&mut self,
_id: Option<&gpui::GlobalElementId>,
_bounds: gpui::Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
cx: &mut WindowContext,
) -> Self::PrepaintState {
self.element.prepaint(cx);
}
fn paint(
&mut self,
_id: Option<&gpui::GlobalElementId>,
_bounds: gpui::Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
cx: &mut WindowContext,
) {
if let Some(position) = self.text_layout.position_for_index(self.cursor_offset) {
let bounds = self.text_layout.bounds();
let line_height = self.text_layout.line_height();
let line_width = self
.text_layout
.line_layout_for_index(self.cursor_offset)
.map_or(bounds.size.width, |layout| layout.width());
cx.paint_quad(quad(
Bounds::new(
point(bounds.origin.x, position.y),
size(cmp::max(bounds.size.width, line_width), line_height),
),
Corners::default(),
cx.theme().colors().editor_active_line_background,
Edges::default(),
Hsla::transparent_black(),
));
self.element.paint(cx);
cx.paint_quad(quad(
Bounds::new(position, size(px(2.), line_height)),
Corners::default(),
cx.theme().players().local().cursor,
Edges::default(),
Hsla::transparent_black(),
));
}
}
}

View File

@@ -1,11 +1,13 @@
use crate::{CompletionDiffElement, InlineCompletion, InlineCompletionRating, Zeta};
use crate::{InlineCompletion, InlineCompletionRating, Zeta};
use editor::Editor;
use gpui::{
actions, prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
View, ViewContext,
actions, prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
HighlightStyle, Model, StyledText, TextStyle, View, ViewContext,
};
use language::language_settings;
use language::{language_settings, OffsetRangeExt};
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
use ui::{prelude::*, KeyBinding, List, ListItem, ListItemSpacing, Tooltip};
use workspace::{ModalView, Workspace};
@@ -13,6 +15,8 @@ actions!(
zeta,
[
RateCompletions,
ThumbsUp,
ThumbsDown,
ThumbsUpActiveCompletion,
ThumbsDownActiveCompletion,
NextEdit,
@@ -37,7 +41,6 @@ pub struct RateCompletionModal {
selected_index: usize,
focus_handle: FocusHandle,
_subscription: gpui::Subscription,
current_view: RateCompletionView,
}
struct ActiveCompletion {
@@ -45,21 +48,6 @@ struct ActiveCompletion {
feedback_editor: View<Editor>,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
enum RateCompletionView {
SuggestedEdits,
RawInput,
}
impl RateCompletionView {
pub fn name(&self) -> &'static str {
match self {
Self::SuggestedEdits => "Suggested Edits",
Self::RawInput => "Recorded Events & Input",
}
}
}
impl RateCompletionModal {
pub fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
if let Some(zeta) = Zeta::global(cx) {
@@ -69,14 +57,12 @@ impl RateCompletionModal {
pub fn new(zeta: Model<Zeta>, cx: &mut ViewContext<Self>) -> Self {
let subscription = cx.observe(&zeta, |_, _, cx| cx.notify());
Self {
zeta,
selected_index: 0,
focus_handle: cx.focus_handle(),
active_completion: None,
_subscription: subscription,
current_view: RateCompletionView::SuggestedEdits,
}
}
@@ -88,7 +74,7 @@ impl RateCompletionModal {
self.selected_index += 1;
self.selected_index = usize::min(
self.selected_index,
self.zeta.read(cx).shown_completions().count(),
self.zeta.read(cx).recent_completions().count(),
);
cx.notify();
}
@@ -102,7 +88,7 @@ impl RateCompletionModal {
let next_index = self
.zeta
.read(cx)
.shown_completions()
.recent_completions()
.skip(self.selected_index)
.enumerate()
.skip(1) // Skip straight to the next item
@@ -117,12 +103,12 @@ impl RateCompletionModal {
fn select_prev_edit(&mut self, _: &PreviousEdit, cx: &mut ViewContext<Self>) {
let zeta = self.zeta.read(cx);
let completions_len = zeta.shown_completions_len();
let completions_len = zeta.recent_completions_len();
let prev_index = self
.zeta
.read(cx)
.shown_completions()
.recent_completions()
.rev()
.skip((completions_len - 1) - self.selected_index)
.enumerate()
@@ -143,7 +129,28 @@ impl RateCompletionModal {
}
fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
self.selected_index = self.zeta.read(cx).shown_completions_len() - 1;
self.selected_index = self.zeta.read(cx).recent_completions_len() - 1;
cx.notify();
}
fn thumbs_up(&mut self, _: &ThumbsUp, cx: &mut ViewContext<Self>) {
self.zeta.update(cx, |zeta, cx| {
let completion = zeta
.recent_completions()
.skip(self.selected_index)
.next()
.cloned();
if let Some(completion) = completion {
zeta.rate_completion(
&completion,
InlineCompletionRating::Positive,
"".to_string(),
cx,
);
}
});
self.select_next_edit(&Default::default(), cx);
cx.notify();
}
@@ -170,11 +177,7 @@ impl RateCompletionModal {
cx.notify();
}
pub fn thumbs_down_active(
&mut self,
_: &ThumbsDownActiveCompletion,
cx: &mut ViewContext<Self>,
) {
fn thumbs_down_active(&mut self, _: &ThumbsDownActiveCompletion, cx: &mut ViewContext<Self>) {
if let Some(active) = &self.active_completion {
if active.feedback_editor.read(cx).text(cx).is_empty() {
return;
@@ -210,7 +213,7 @@ impl RateCompletionModal {
let completion = self
.zeta
.read(cx)
.shown_completions()
.recent_completions()
.skip(self.selected_index)
.take(1)
.next()
@@ -223,7 +226,7 @@ impl RateCompletionModal {
let completion = self
.zeta
.read(cx)
.shown_completions()
.recent_completions()
.skip(self.selected_index)
.take(1)
.next()
@@ -243,7 +246,7 @@ impl RateCompletionModal {
self.selected_index = self
.zeta
.read(cx)
.shown_completions()
.recent_completions()
.enumerate()
.find(|(_, completion_b)| completion.id == completion_b.id)
.map(|(ix, _)| ix)
@@ -283,127 +286,99 @@ impl RateCompletionModal {
cx.notify();
}
fn render_view_nav(&self, cx: &ViewContext<Self>) -> impl IntoElement {
h_flex()
.h_8()
.px_1()
.border_b_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().elevated_surface_background)
.gap_1()
.child(
Button::new(
ElementId::Name("suggested-edits".into()),
RateCompletionView::SuggestedEdits.name(),
)
.label_size(LabelSize::Small)
.on_click(cx.listener(move |this, _, cx| {
this.current_view = RateCompletionView::SuggestedEdits;
cx.notify();
}))
.toggle_state(self.current_view == RateCompletionView::SuggestedEdits),
)
.child(
Button::new(
ElementId::Name("raw-input".into()),
RateCompletionView::RawInput.name(),
)
.label_size(LabelSize::Small)
.on_click(cx.listener(move |this, _, cx| {
this.current_view = RateCompletionView::RawInput;
cx.notify();
}))
.toggle_state(self.current_view == RateCompletionView::RawInput),
)
}
fn render_suggested_edits(&self, cx: &mut ViewContext<Self>) -> Option<gpui::Stateful<Div>> {
let active_completion = self.active_completion.as_ref()?;
let bg_color = cx.theme().colors().editor_background;
Some(
div()
.id("diff")
.p_4()
.size_full()
.bg(bg_color)
.overflow_scroll()
.whitespace_nowrap()
.child(CompletionDiffElement::new(
&active_completion.completion,
cx,
)),
)
}
fn render_raw_input(&self, cx: &mut ViewContext<Self>) -> Option<gpui::Stateful<Div>> {
Some(
v_flex()
.size_full()
.overflow_hidden()
.relative()
.child(
div()
.id("raw-input")
.py_4()
.px_6()
.size_full()
.bg(cx.theme().colors().editor_background)
.overflow_scroll()
.child(if let Some(active_completion) = &self.active_completion {
format!(
"{}\n{}",
active_completion.completion.input_events,
active_completion.completion.input_excerpt
)
} else {
"No active completion".to_string()
}),
)
.id("raw-input-view"),
)
}
fn render_active_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
let active_completion = self.active_completion.as_ref()?;
let completion_id = active_completion.completion.id;
let focus_handle = &self.focus_handle(cx);
let border_color = cx.theme().colors().border;
let bg_color = cx.theme().colors().editor_background;
let mut diff = active_completion
.completion
.snapshot
.text_for_range(active_completion.completion.excerpt_range.clone())
.collect::<String>();
let mut delta = 0;
let mut diff_highlights = Vec::new();
for (old_range, new_text) in active_completion.completion.edits.iter() {
let old_range = old_range.to_offset(&active_completion.completion.snapshot);
let old_start_in_text =
old_range.start - active_completion.completion.excerpt_range.start + delta;
let old_end_in_text =
old_range.end - active_completion.completion.excerpt_range.start + delta;
if old_start_in_text < old_end_in_text {
diff_highlights.push((
old_start_in_text..old_end_in_text,
HighlightStyle {
background_color: Some(cx.theme().status().deleted_background),
strikethrough: Some(gpui::StrikethroughStyle {
thickness: px(1.),
color: Some(cx.theme().colors().text_muted),
}),
..Default::default()
},
));
}
if !new_text.is_empty() {
diff.insert_str(old_end_in_text, new_text);
diff_highlights.push((
old_end_in_text..old_end_in_text + new_text.len(),
HighlightStyle {
background_color: Some(cx.theme().status().created_background),
..Default::default()
},
));
delta += new_text.len();
}
}
let settings = ThemeSettings::get_global(cx).clone();
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_size: settings.buffer_font_size(cx).into(),
font_family: settings.buffer_font.family,
font_features: settings.buffer_font.features,
font_fallbacks: settings.buffer_font.fallbacks,
line_height: relative(settings.buffer_line_height.value()),
font_weight: settings.buffer_font.weight,
font_style: settings.buffer_font.style,
..Default::default()
};
let rated = self.zeta.read(cx).is_completion_rated(completion_id);
let was_shown = self.zeta.read(cx).was_completion_shown(completion_id);
let feedback_empty = active_completion
.feedback_editor
.read(cx)
.text(cx)
.is_empty();
let label_container = h_flex().pl_1().gap_1p5();
let border_color = cx.theme().colors().border;
let bg_color = cx.theme().colors().editor_background;
let label_container = || h_flex().pl_1().gap_1p5();
Some(
v_flex()
.size_full()
.overflow_hidden()
.relative()
.child(
v_flex()
div()
.id("diff")
.py_4()
.px_6()
.size_full()
.overflow_hidden()
.relative()
.child(self.render_view_nav(cx))
.when_some(match self.current_view {
RateCompletionView::SuggestedEdits => self.render_suggested_edits(cx),
RateCompletionView::RawInput => self.render_raw_input(cx),
}, |this, element| this.child(element))
.bg(bg_color)
.overflow_scroll()
.child(StyledText::new(diff).with_highlights(&text_style, diff_highlights)),
)
.when(!rated, |this| {
.when_some((!rated).then(|| ()), |this, _| {
this.child(
h_flex()
.p_2()
.gap_2()
.border_y_1()
.border_color(border_color)
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
@@ -415,14 +390,14 @@ impl RateCompletionModal {
.pr_2()
.flex_wrap()
.child(
Label::new("Explain why this completion is good or bad. If it's negative, describe what you expected instead.")
Label::new("Ensure you explain why this completion is negative or positive. In case it's negative, report what you expected instead.")
.size(LabelSize::Small)
.color(Color::Muted)
)
)
)
})
.when(!rated, |this| {
.when_some((!rated).then(|| ()), |this, _| {
this.child(
div()
.h_40()
@@ -442,7 +417,7 @@ impl RateCompletionModal {
.justify_between()
.children(if rated {
Some(
label_container
label_container()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
@@ -452,7 +427,7 @@ impl RateCompletionModal {
)
} else if active_completion.completion.edits.is_empty() {
Some(
label_container
label_container()
.child(
Icon::new(IconName::Warning)
.size(IconSize::Small)
@@ -460,14 +435,30 @@ impl RateCompletionModal {
)
.child(Label::new("No edits produced.").color(Color::Muted)),
)
} else if !was_shown {
Some(
label_container()
.child(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new("Completion wasn't shown because another valid one was already on screen.")),
)
} else {
Some(label_container)
Some(label_container())
})
.child(
h_flex()
.gap_1()
.child(
Button::new("bad", "Bad Completion")
.key_binding(KeyBinding::for_action_in(
&ThumbsDown,
&self.focus_handle(cx),
cx,
))
.style(ButtonStyle::Filled)
.icon(IconName::ThumbsDown)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
@@ -477,11 +468,6 @@ impl RateCompletionModal {
Tooltip::text("Explain what's bad about it before reporting it", cx)
})
})
.key_binding(KeyBinding::for_action_in(
&ThumbsDownActiveCompletion,
focus_handle,
cx,
))
.on_click(cx.listener(move |this, _, cx| {
this.thumbs_down_active(
&ThumbsDownActiveCompletion,
@@ -491,15 +477,16 @@ impl RateCompletionModal {
)
.child(
Button::new("good", "Good Completion")
.key_binding(KeyBinding::for_action_in(
&ThumbsUp,
&self.focus_handle(cx),
cx,
))
.style(ButtonStyle::Filled)
.icon(IconName::ThumbsUp)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.disabled(rated)
.key_binding(KeyBinding::for_action_in(
&ThumbsUpActiveCompletion,
focus_handle,
cx,
))
.on_click(cx.listener(move |this, _, cx| {
this.thumbs_up_active(&ThumbsUpActiveCompletion, cx);
})),
@@ -525,6 +512,7 @@ impl Render for RateCompletionModal {
.on_action(cx.listener(Self::select_next_edit))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::thumbs_up))
.on_action(cx.listener(Self::thumbs_up_active))
.on_action(cx.listener(Self::thumbs_down_active))
.on_action(cx.listener(Self::focus_completions))
@@ -538,16 +526,16 @@ impl Render for RateCompletionModal {
.shadow_lg()
.child(
v_flex()
.w_72()
.h_full()
.border_r_1()
.border_color(border_color)
.w_96()
.h_full()
.flex_shrink_0()
.overflow_hidden()
.child(
h_flex()
.h_8()
.px_2()
.py_1()
.justify_between()
.border_b_1()
.border_color(border_color)
@@ -573,12 +561,12 @@ impl Render for RateCompletionModal {
div()
.p_2()
.child(
Label::new("No completions yet. Use the editor to generate some, and make sure to rate them!")
Label::new("No completions yet. Use the editor to generate some and rate them!")
.color(Color::Muted),
)
.into_any_element(),
)
.children(self.zeta.read(cx).shown_completions().cloned().enumerate().map(
.children(self.zeta.read(cx).recent_completions().cloned().enumerate().map(
|(index, completion)| {
let selected =
self.active_completion.as_ref().map_or(false, |selected| {
@@ -587,45 +575,27 @@ impl Render for RateCompletionModal {
let rated =
self.zeta.read(cx).is_completion_rated(completion.id);
let (icon_name, icon_color, tooltip_text) = match (rated, completion.edits.is_empty()) {
(true, _) => (IconName::Check, Color::Success, "Rated Completion"),
(false, true) => (IconName::File, Color::Muted, "No Edits Produced"),
(false, false) => (IconName::FileDiff, Color::Accent, "Edits Available"),
};
let file_name = completion.path.file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or("untitled".to_string());
let file_path = completion.path.parent().map(|p| p.to_string_lossy().to_string());
ListItem::new(completion.id)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.focused(index == self.selected_index)
.toggle_state(selected)
.start_slot(if rated {
Icon::new(IconName::Check).color(Color::Success).size(IconSize::Small)
} else if completion.edits.is_empty() {
Icon::new(IconName::File).color(Color::Muted).size(IconSize::Small)
} else {
Icon::new(IconName::FileDiff).color(Color::Accent).size(IconSize::Small)
})
.child(
h_flex()
.id("completion-content")
.gap_3()
.child(
Icon::new(icon_name)
.color(icon_color)
.size(IconSize::Small)
)
.child(
v_flex()
.child(
h_flex().gap_1()
.child(Label::new(file_name).size(LabelSize::Small))
.when_some(file_path, |this, p| this.child(Label::new(p).size(LabelSize::Small).color(Color::Muted)))
)
.child(Label::new(format!("{} ago, {:.2?}", format_time_ago(completion.response_received_at.elapsed()), completion.latency()))
.color(Color::Muted)
.size(LabelSize::XSmall)
)
v_flex()
.pl_1p5()
.child(Label::new(completion.path.to_string_lossy().to_string()).size(LabelSize::Small))
.child(Label::new(format!("{} ago, {:.2?}", format_time_ago(completion.response_received_at.elapsed()), completion.latency()))
.color(Color::Muted)
.size(LabelSize::XSmall)
)
)
.tooltip(move |cx| {
Tooltip::text(tooltip_text, cx)
})
.on_click(cx.listener(move |this, _, cx| {
this.select_completion(Some(completion.clone()), true, cx);
}))

View File

@@ -1,7 +1,5 @@
mod completion_diff_element;
mod rate_completion_modal;
pub(crate) use completion_diff_element::*;
pub use rate_completion_modal::*;
use anyhow::{anyhow, Context as _, Result};
@@ -32,7 +30,6 @@ use std::{
time::{Duration, Instant},
};
use telemetry_events::InlineCompletionRating;
use util::ResultExt;
use uuid::Uuid;
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
@@ -74,7 +71,6 @@ pub struct InlineCompletion {
id: InlineCompletionId,
path: Arc<Path>,
excerpt_range: Range<usize>,
cursor_offset: usize,
edits: Arc<[(Range<Anchor>, String)]>,
snapshot: BufferSnapshot,
input_outline: Arc<str>,
@@ -158,8 +154,9 @@ pub struct Zeta {
client: Arc<Client>,
events: VecDeque<Event>,
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
shown_completions: VecDeque<InlineCompletion>,
recent_completions: VecDeque<InlineCompletion>,
rated_completions: HashSet<InlineCompletionId>,
shown_completions: HashSet<InlineCompletionId>,
llm_token: LlmApiToken,
_llm_token_subscription: Subscription,
}
@@ -187,8 +184,9 @@ impl Zeta {
Self {
client,
events: VecDeque::new(),
shown_completions: VecDeque::new(),
recent_completions: VecDeque::new(),
rated_completions: HashSet::default(),
shown_completions: HashSet::default(),
registered_buffers: HashMap::default(),
llm_token: LlmApiToken::default(),
_llm_token_subscription: cx.subscribe(
@@ -207,7 +205,7 @@ impl Zeta {
}
fn push_event(&mut self, event: Event) {
const MAX_EVENT_COUNT: usize = 16;
const MAX_EVENT_COUNT: usize = 20;
if let Some(Event::BufferChange {
new_snapshot: last_new_snapshot,
@@ -233,8 +231,8 @@ impl Zeta {
}
self.events.push_back(event);
if self.events.len() >= MAX_EVENT_COUNT {
self.events.drain(..MAX_EVENT_COUNT / 2);
if self.events.len() > MAX_EVENT_COUNT {
self.events.pop_front();
}
}
@@ -293,13 +291,13 @@ impl Zeta {
let events = self.events.clone();
let path = snapshot
.file()
.map(|f| Arc::from(f.full_path(cx).as_path()))
.map(|f| f.path().clone())
.unwrap_or_else(|| Arc::from(Path::new("untitled")));
let client = self.client.clone();
let llm_token = self.llm_token.clone();
cx.spawn(|_, cx| async move {
cx.spawn(|this, mut cx| async move {
let request_sent_at = Instant::now();
let (input_events, input_excerpt, input_outline) = cx
@@ -338,11 +336,10 @@ impl Zeta {
let output_excerpt = response.output_excerpt;
log::debug!("completion response: {}", output_excerpt);
Self::process_completion_response(
let inline_completion = Self::process_completion_response(
output_excerpt,
&snapshot,
excerpt_range,
offset,
path,
input_outline,
input_events,
@@ -350,7 +347,20 @@ impl Zeta {
request_sent_at,
&cx,
)
.await
.await?;
this.update(&mut cx, |this, cx| {
this.recent_completions
.push_front(inline_completion.clone());
if this.recent_completions.len() > 50 {
let completion = this.recent_completions.pop_back().unwrap();
this.shown_completions.remove(&completion.id);
this.rated_completions.remove(&completion.id);
}
cx.notify();
})?;
Ok(inline_completion)
})
}
@@ -483,8 +493,8 @@ and then another
}
zeta.update(&mut cx, |zeta, _cx| {
zeta.shown_completions.get_mut(2).unwrap().edits = Arc::new([]);
zeta.shown_completions.get_mut(3).unwrap().edits = Arc::new([]);
zeta.recent_completions.get_mut(2).unwrap().edits = Arc::new([]);
zeta.recent_completions.get_mut(3).unwrap().edits = Arc::new([]);
})
.ok();
})
@@ -567,7 +577,6 @@ and then another
output_excerpt: String,
snapshot: &BufferSnapshot,
excerpt_range: Range<usize>,
cursor_offset: usize,
path: Arc<Path>,
input_outline: String,
input_events: String,
@@ -627,7 +636,6 @@ and then another
id: InlineCompletionId::new(),
path,
excerpt_range,
cursor_offset,
edits: edits.into(),
snapshot: snapshot.clone(),
input_outline: input_outline.into(),
@@ -710,13 +718,12 @@ and then another
self.rated_completions.contains(&completion_id)
}
pub fn completion_shown(&mut self, completion: &InlineCompletion, cx: &mut ModelContext<Self>) {
self.shown_completions.push_front(completion.clone());
if self.shown_completions.len() > 50 {
let completion = self.shown_completions.pop_back().unwrap();
self.rated_completions.remove(&completion.id);
}
cx.notify();
pub fn was_completion_shown(&self, completion_id: InlineCompletionId) -> bool {
self.shown_completions.contains(&completion_id)
}
pub fn completion_shown(&mut self, completion_id: InlineCompletionId) {
self.shown_completions.insert(completion_id);
}
pub fn rate_completion(
@@ -726,7 +733,6 @@ and then another
feedback: String,
cx: &mut ModelContext<Self>,
) {
self.rated_completions.insert(completion.id);
telemetry::event!(
"Inline Completion Rated",
rating,
@@ -740,12 +746,12 @@ and then another
cx.notify();
}
pub fn shown_completions(&self) -> impl DoubleEndedIterator<Item = &InlineCompletion> {
self.shown_completions.iter()
pub fn recent_completions(&self) -> impl DoubleEndedIterator<Item = &InlineCompletion> {
self.recent_completions.iter()
}
pub fn shown_completions_len(&self) -> usize {
self.shown_completions.len()
pub fn recent_completions_len(&self) -> usize {
self.recent_completions.len()
}
fn report_changes_for_buffer(
@@ -968,7 +974,7 @@ impl CurrentInlineCompletion {
struct PendingCompletion {
id: usize,
_task: Task<()>,
_task: Task<Result<()>>,
}
pub struct ZetaInlineCompletionProvider {
@@ -1046,16 +1052,13 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
})
});
let completion = match completion_request {
Ok(completion_request) => {
let completion_request = completion_request.await;
completion_request.map(|completion| CurrentInlineCompletion {
buffer_id: buffer.entity_id(),
completion,
})
}
Err(error) => Err(error),
};
let mut completion = None;
if let Ok(completion_request) = completion_request {
completion = Some(CurrentInlineCompletion {
buffer_id: buffer.entity_id(),
completion: completion_request.await?,
});
}
this.update(&mut cx, |this, cx| {
if this.pending_completions[0].id == pending_completion_id {
@@ -1064,27 +1067,27 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
this.pending_completions.clear();
}
if let Some(new_completion) = completion.context("zeta prediction failed").log_err()
{
if let Some(new_completion) = completion {
if let Some(old_completion) = this.current_completion.as_ref() {
let snapshot = buffer.read(cx).snapshot();
if new_completion.should_replace_completion(&old_completion, &snapshot) {
this.zeta.update(cx, |zeta, cx| {
zeta.completion_shown(&new_completion.completion, cx);
this.zeta.update(cx, |zeta, _cx| {
zeta.completion_shown(new_completion.completion.id)
});
this.current_completion = Some(new_completion);
}
} else {
this.zeta.update(cx, |zeta, cx| {
zeta.completion_shown(&new_completion.completion, cx);
this.zeta.update(cx, |zeta, _cx| {
zeta.completion_shown(new_completion.completion.id)
});
this.current_completion = Some(new_completion);
}
} else {
this.current_completion = None;
}
cx.notify();
})
.ok();
});
// We always maintain at most two pending completions. When we already
@@ -1209,7 +1212,6 @@ mod tests {
snapshot: buffer.read(cx).snapshot(),
id: InlineCompletionId::new(),
excerpt_range: 0..0,
cursor_offset: 0,
input_outline: "".into(),
input_events: "".into(),
input_excerpt: "".into(),

View File

@@ -2262,6 +2262,7 @@ Run the `theme selector: toggle` action in the command palette to see a current
"button": true,
"default_width": 240,
"dock": "left",
"entry_spacing": "comfortable",
"file_icons": true,
"folder_icons": true,
"git_status": true,
@@ -2303,6 +2304,30 @@ Run the `theme selector: toggle` action in the command palette to see a current
}
```
### Entry Spacing
- Description: Spacing between worktree entries
- Setting: `entry_spacing`
- Default: `comfortable`
**Options**
1. Comfortable entry spacing
```json
{
"entry_spacing": "comfortable"
}
```
2. Standard entry spacing
```json
{
"entry_spacing": "standard"
}
```
### Git Status
- Description: Indicates newly created and updated files

View File

@@ -124,3 +124,16 @@ Then clean and rebuild the project:
cargo clean
cargo run
```
## Tips & Tricks
If you are building Zed a lot, you may find that macOS continually verifies new
builds which can add a few seconds to your iteration cycles.
To fix this, you can:
- Run `sudo spctl developer-mode enable-terminal` to enable the Developer Tools panel in System Settings.
- In System Settings, search for "Developer Tools" and add your terminal (e.g. iTerm or Ghostty) to the list under "Allow applications to use developer tools"
- Restart your terminal.
Thanks to the nextest developers for publishing [this](https://nexte.st/docs/installation/macos/#gatekeeper).

View File

@@ -170,3 +170,13 @@ rm ~/.local/zed.app/lib/libcrypto.so.1.1
```
This will force zed to fallback to the system `libssl` and `libcrypto` libraries.
### Editing files requiring root access
When you try to edit files that require root access, Zed requires `pkexec` (part of polkit) to handle authentication prompts.
Polkit comes pre-installed with most desktop environments like GNOME and KDE. If you're using a minimal system and polkit is not installed, you can install it with:
- Ubuntu/Debian: `sudo apt install policykit-1`
- Fedora: `sudo dnf install polkit`
- Arch Linux: `sudo pacman -S polkit`

View File

@@ -2,4 +2,4 @@
channel = "1.81"
profile = "minimal"
components = [ "rustfmt", "clippy" ]
targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-unknown-linux-gnu", "wasm32-wasip1", "x86_64-pc-windows-msvc" ]
targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-unknown-linux-gnu", "wasm32-wasip2", "x86_64-pc-windows-msvc" ]