Compare commits

...

41 Commits

Author SHA1 Message Date
Peter Tripp
61b01a0ca9 zed 0.169.3 2025-01-21 11:45:26 -05:00
gcp-cherry-pick-bot[bot]
41a267d49b editor: Hide horizontal scrollbar if not visible (cherry-pick #23337) (#23338)
Cherry-picked editor: Hide horizontal scrollbar if not visible (#23337)

This PR fixes two visual issues, that were caused by the fact that we
were always painting the horizontal scrollbar even if there is no
horizontal scrolling possible

Obscuring deleted lines when using the inline assistant:


https://github.com/user-attachments/assets/f8460c3f-403e-40a6-8622-65268ba2d875

Cutting off text even when horizontal scrolling is not possible:


https://github.com/user-attachments/assets/23c909f7-1c23-4693-8edc-40a2f089d4a8

This issue was only present in some themes (e.g. Nord, Catpuccin)


Closes #22716

Release Notes:

- Fixed an issue where horizontal scrollbars of editors would always be
painted (even if there is no horizontal scrolling to be done)

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2025-01-20 10:48:23 +01:00
gcp-cherry-pick-bot[bot]
349fb5254d Fix accepting partial inline completion (cherry-pick #23312) (#23328)
Cherry-picked Fix accepting partial inline completion (#23312)

Release Notes:

- Fixed a bug that could prevent accepting a partial inline completion.

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-01-20 09:40:24 +01:00
Peter Tripp
05563d25b7 v0.169.x stable 2025-01-15 12:51:21 -05:00
Peter Tripp
efb6342e9d ci: Cleanup for disabled Merge Queue / merge_group (#23187)
Only run actions dependency-review-action if running in a PR action.

This broke when run as part of action for commit on main and on a
preview branch:
- https://github.com/zed-industries/zed/actions/runs/12793068921/job/35664998296
- https://github.com/zed-industries/zed/actions/runs/12793045639

Originally introduced in:
- https://github.com/zed-industries/zed/pull/21424

But was only tested with `merge_group` which has since been reverted.
2025-01-15 12:27:32 -05:00
Peter Tripp
68737cae56 Revert docs-only test-skipping with Merge Queue (#23180)
These checks were not functioning as intended. Notably tests were
skipped for today's hotfix release of Preview
[v0.169.2-pre](https://github.com/zed-industries/zed/actions/runs/12790602047):

Separately these checks were flawed as they would only be considered
"docs only" if the diff between the PR branch base and main also did not
have any subsequent non-docs changes.

Reverting until we can figure out something better.
2025-01-15 11:47:31 -05:00
Peter Tripp
9677a7d8ac Add ollama phi4 context size defaults (#23036)
Add `phi4` maximum context length (128K).
By default this clamps to `16384` but if you have enough video memory
you can set it higher or connect to a non-local machine via settings:

```json
"language_models": {
  "ollama": {
    "api_url": "http://localhost:11434",
    "available_models": [
      {
        "name": "phi4",
        "display_name": "Phi4 64K",
         "max_tokens": 65536
      }
    ]
  }
}
```

Release Notes:

- Improve support for Phi4 with ollama.
2025-01-15 11:15:51 -05:00
Peter Tripp
aa2d6378a4 Increase timeout for macos release builds (#23183)
Today's Preview hotfix timed out during notarization:
- https://github.com/zed-industries/zed/actions/runs/12790602047/job/35656767355
2025-01-15 11:02:23 -05:00
gcp-cherry-pick-bot[bot]
747c7e9b58 Revert "linux: Fix saving file with root ownership (#22045)" (cherry-pick #23162) (#23168)
Cherry-picked Revert "linux: Fix saving file with root ownership
(#22045)" (#23162)

Release Notes:

- (temporarily) Removes the linux "save file as root" feature while we
figure out bugs.

Updates https://github.com/zed-industries/zed/pull/22045

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-01-15 08:12:12 -07:00
Antonio Scandurra
458637f2e1 zed 0.169.2 2025-01-15 15:35:58 +01:00
Thorsten Ball
2a6f6ce5b6 edit prediction: Fix width of completion item (#23177)
Release Notes:

- N/A

Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>
2025-01-15 15:35:15 +01:00
Danilo Leal
395d2e7d46 zeta: Show keybinding in completion rating buttons in review modal (#22985)
This PR also removes the `ThumbsUp` action that wasn't being triggered
correctly. We didn't have it's counterpart `ThumbsDown`, too, so I
mostly assumed it would be harmless to remove `ThumbsUp` as well.

<img width="800" alt="Screenshot 2025-01-10 at 6 18 44 PM"
src="https://github.com/user-attachments/assets/9fd5da9f-9dff-454d-9f31-c02f1370b937"
/>

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
# Conflicts:
#	crates/zeta/src/rate_completion_modal.rs
2025-01-15 15:28:48 +01:00
Agus Zubiaga
6e1e392853 Check for predict-edits feature flag, remove is_staff check (#23165)
Release Notes:

- N/A

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2025-01-15 15:20:36 +01:00
Antonio Scandurra
8a33b2b450 Show loading state for predictions (#23172)
Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
# Conflicts:
#	crates/editor/src/code_context_menus.rs
2025-01-15 15:20:31 +01:00
Michael Sloan
96c5be5fa5 Make completion menu entries mutable (#22880)
Release Notes:

- N/A
2025-01-15 15:20:02 +01:00
Thorsten Ball
7fafc88706 vim: Fix inline completions not disappearing in normal mode (#23176)
Closes #23042

Release Notes:

- Fixed inline completions (Copilot, Supermaven, ...) still being
visible sometimes after leaving Vim's insert mode.
2025-01-15 15:16:42 +01:00
Bennet Bo Fenner
db503cf48b zeta: Allow viewing prompt details in rate completion modal (#23142)
Co-Authored-by: Danilo <danilo@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Danilo <danilo@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
# Conflicts:
#	crates/zeta/src/rate_completion_modal.rs
2025-01-15 15:16:32 +01:00
Thorsten Ball
745f6ceb3a settings: Rename 'zeta' to 'zed' (#23174)
Old:

```settings.json
{
  "features": {
    "inline_completion_provider": "zeta"
  }
}
```

New & cool:

```settings.json
{
  "features": {
    "inline_completion_provider": "zed"
  }
}
```

Release Notes:

- N/A
2025-01-15 15:15:55 +01:00
Thorsten Ball
8d807cedda Change tooltip to 'Edit Prediction' (#23139)
Release Notes:

- N/A
2025-01-15 15:15:50 +01:00
Bennet Bo Fenner
680e5f149b zeta: Rework displaying paths in completion rating modal (#23129)
Two issues i ran into while looking at the completion rating modal
- Single-file worktrees file names are not displayed at all
- Hard to see the filename when the path is long (lots of directories)

This PR fixes this by displaying the filename on the left, followed by
the full path (including the worktree name), similar to how we do it in
the file finder/assistant panel /file command
| Before | After |
|--------|--------|
| <img width="1067" alt="Screenshot 2025-01-14 at 16 09 05"
src="https://github.com/user-attachments/assets/628fde18-da9a-4d98-8ddf-ed0ab0cd8d35"
/> | <img width="1161" alt="Screenshot 2025-01-14 at 16 17 52"
src="https://github.com/user-attachments/assets/80c6a4e1-065d-4b0a-b9c0-5f3391af4557"
/> |

Release Notes:

- N/A
# Conflicts:
#	crates/zeta/src/rate_completion_modal.rs
2025-01-15 15:15:32 +01:00
Thorsten Ball
e4bcbc0eea zeta: Various product fixes before Preview release (#23125)
Various fixes for Zeta and one fix that's visible to non-Zeta-using
users of inline completions.

Release Notes:

- Changed inline completions (Copilot, Supermaven, ...) to not show up
in empty buffers.

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Bennet <bennet@zed.dev>
# Conflicts:
#	crates/zeta/src/rate_completion_modal.rs
2025-01-15 15:12:41 +01:00
Antonio Scandurra
7fe8a4449e Add more metrics for Fireworks Completion Requested (#23062)
Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-15 15:09:46 +01:00
Antonio Scandurra
a917a894bf Improve prompt caching for edit prediction (#23061)
This is achieved by halving the number of events instead of popping the
front.

Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-15 15:09:40 +01:00
Thorsten Ball
0d03674def zeta: Report Fireworks request data to Snowflake (#22973)
Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Conrad <conrad@zed.dev>
2025-01-15 15:09:08 +01:00
Antonio Scandurra
8fb1d135ad Log errors when a prediction fails (#22961)
Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-15 15:09:00 +01:00
Thorsten Ball
62e098b365 zeta: Fix completions not being marked as rated (#22952)
Seems like #22171 accidentally removed this line.

Now it's back and completions are marked as rated again.

![screenshot-2025-01-10-10 56
32@2x](https://github.com/user-attachments/assets/c68bff1b-5b97-493e-9062-390876fd757c)

Release Notes:

- N/A
2025-01-15 15:08:53 +01:00
Antonio Scandurra
fd6ac07fc2 Animate Zeta button while generating completions (#22899)
Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-15 15:08:47 +01:00
Antonio Scandurra
18b08e2eae Include outline when predicting edits with Zeta (#22895)
Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-15 15:08:39 +01:00
Antonio Scandurra
dc90aaa4cd Introduce UI affordances to make enabling/disabling inline completions easier (#22894)
Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
2025-01-15 15:08:25 +01:00
Thorsten Ball
142f949e73 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-15 15:08:22 +01:00
Thorsten Ball
9c0c853a2c 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-15 15:08:09 +01:00
gcp-cherry-pick-bot[bot]
e74e2863f9 Improve registration for Assistant code action providers (cherry-pick #23099) (#23136)
Cherry-picked Improve registration for Assistant code action providers
(#23099)

This PR is a follow-up to
https://github.com/zed-industries/zed/pull/22911 to further improve the
registration of code action providers for the Assistant in order to
prevent duplicates.

The `CodeActionProvider` trait now has an `id` method that is used to
return a unique ID for a code action provider. We use this to prevent
registering duplicates of the same provider.

The registration of the code action providers for Assistant1 and
Assistant2 have also been reworked. Previously we were not call the
registration function—and thus setting up the subscriptions—until we
resolved the feature flags. However, this could lead to the registration
happening too late for existing workspace items.

We now perform the registration right away and then remove the undesired
code action providers once the feature flags have been resolved.

Release Notes:

- N/A

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-14 18:45:52 +02:00
gcp-cherry-pick-bot[bot]
784c1280f3 Fix duplicated Fix with Assistant code actions (cherry-pick #22911) (#23118)
Cherry-picked Fix duplicated `Fix with Assistant` code actions (#22911)

This PR fixes the duplicated `Fix with Assistant` code actions that were
being shown in the code actions menu.

This fix isn't 100% ideal, as there is an edge case in buffers that are
already open when the workspace loads, as we may not observe the feature
flags in time to register the code action providers by the time we
receive the event that an item was added to the workspace.

Closes https://github.com/zed-industries/zed/issues/22400.

Release Notes:

- Fixed duplicate "Fix with Assistant" entries showing in the code
action list.

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-14 13:46:37 +02:00
Zed Bot
75f682b80f Bump to 0.169.1 for @osiewicz 2025-01-13 22:44:56 +00:00
gcp-cherry-pick-bot[bot]
8052cf8415 editor: Adjust offset of the opened jump target in the multibuffer (cherry-pick #23091) (#23101)
Cherry-picked editor: Adjust offset of the opened jump target in the
multibuffer (#23091)

This PR fixes an issue with jumping from multi_buffer to a file; namely,
the scroll offset of the opened buffer used to match the position within
the multibuffer, but it broke a while back. This is because we were
opening a buffer without providing the data about the origin scroll
offset.

Closes #ISSUE

Release Notes:

- Fixed a bug where the relative position of an excerpt within the
multibuffer was not accounted for while jumping to the buffer, causing
the clicked line to drastically change position on screen.

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-01-13 23:41:03 +01:00
gcp-cherry-pick-bot[bot]
d1bb1f14e0 terminal: Fix unresponsive buttons on load until center pane is clicked + Auto-focus docked terminal on load if no other item is focused (cherry-pick #23039) (#23094)
Cherry-picked terminal: Fix unresponsive buttons on load until center
pane is clicked + Auto-focus docked terminal on load if no other item is
focused (#23039)

Closes #23006

This PR should have been split into two, but since the changes are
related, I merged them into one.

1. On load, the title bar actions and bottom bar toggles are
unresponsive until the center pane is clicked. This happens because the
terminal captures focus (even if it's closed) long after the workspace
sets focus to itself during loading.

The issue was in the `focus_view` call used in the `new` method of
`TerminalPanel`. Since new terminal views can be created behind the
scenes (i.e., without the terminal being visible to the user), we
shouldn't handle focus for the terminal in this case. Removing
`focus_view` from the `new` method has no impact on the existing
terminal focusing logic. I've tested scenarios such as creating new
terminals, splitting terminals, zooming, etc., and everything works as
expected.

2. Currently, on load, docked terminals do not automatically focus when
they are only visible item to the user. This PR implements it.

Before/After:

1. When only the dock terminal is visible on load. Terminal is focused.

<img

src="https://github.com/user-attachments/assets/af8848aa-ccb5-4a3b-b2c6-486e8d588f09"
alt="image" height="280px" />

<img

src="https://github.com/user-attachments/assets/8f76ca2e-de29-4cc0-979b-749b50a00bbd"
alt="image" height="280px" />

2. When other items are visible along with the dock terminal on load.
Editor is focused.

<img

src="https://github.com/user-attachments/assets/d3248272-a75d-4763-9e99-defb8a369b68"
alt="image" height="280px" />

<img

src="https://github.com/user-attachments/assets/fba5184e-1ab2-406c-9669-b141aaf1c32f"
alt="image" height="280px" />

3. Multiple tabs along with split panes. Last terminal is focused.

<img

src="https://github.com/user-attachments/assets/7a10c3cf-8bb3-4b88-aacc-732b678bee19"
alt="image" height="270px" />

<img

src="https://github.com/user-attachments/assets/4d16e98f-9d7a-45f6-8701-d6652e411d3b"
alt="image" height="270px" />

Future:

When a docked terminal is in a zoomed state and Zed is loaded, we should
prioritize focusing on the terminal over the active item (e.g., an
editor) behind it. This hasn't been implemented in this PR because the
zoomed state during the load function is stale. The correct state is
received later via the workspace. I'm still investigating where exactly
this should be handled, so this will be a separate PR.

cc: @SomeoneToIgnore 

Release Notes:

- Fixed unresponsive buttons on load until the center pane is clicked.  
- Added auto-focus for the docked terminal on load when no other item is
focused.

Co-authored-by: tims <0xtimsb@gmail.com>
2025-01-13 23:28:54 +02:00
gcp-cherry-pick-bot[bot]
8d027b819d Reuse vtsls logic for completion details display (cherry-pick #23030) (#23034) 2025-01-12 16:45:45 +02:00
gcp-cherry-pick-bot[bot]
88f7071fc7 Do not try to activate the terminal panel twice (cherry-pick #23029) (#23032)
Cherry-picked Do not try to activate the terminal panel twice (#23029)

Closes https://github.com/zed-industries/zed/issues/23023

Fixes terminal pane button opening two terminals on click.

The culprit is in


61115bd047/crates/workspace/src/workspace.rs (L2412-L2417)

* We cannot get any panel by index from the Dock, only an active one
* Both `dock.activate_panel(panel_index, cx);` and `dock.set_open(true,
cx);` do `active_panel.panel.set_active(true, cx);`

So, follow other pane's impls that have `active: bool` property for this
case, e.g.

3ec52d8451/crates/assistant/src/inline_assistant.rs (L2687)

Release Notes:

- Fixed terminal pane button opening two terminals on click

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-01-12 15:57:09 +02:00
Peter Tripp
a297e19866 emacs: Fix emacs in embedded terminal on Linux too (#22969)
- Follow-up to #22779 (accidentially did macos only)
- Follow-up to: https://github.com/zed-industries/zed/pull/22590

Release Notes:

- N/A
2025-01-10 10:51:52 -05:00
Cole Miller
92ba505b92 Update reference to editor::OpenFile in keymap (#22827)
Follow-up to #22494

Release Notes:

- N/A
2025-01-08 13:22:20 -05:00
Peter Tripp
9cb86456a2 v0.169.x preview 2025-01-08 11:00:58 -05:00
50 changed files with 1399 additions and 601 deletions

View File

@@ -10,7 +10,6 @@ on:
pull_request:
branches:
- "**"
merge_group:
concurrency:
# Allow only one workflow per any non-`main` branch.
@@ -24,31 +23,6 @@ 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'
@@ -121,7 +95,6 @@ jobs:
runs-on:
- self-hosted
- test
needs: check_docs_only
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -129,35 +102,29 @@ 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' && needs.check_docs_only.outputs.docs_only == 'false'
if: github.event_name == 'pull_request'
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"
@@ -171,7 +138,6 @@ 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
@@ -182,26 +148,21 @@ 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
@@ -212,7 +173,6 @@ 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
@@ -223,18 +183,15 @@ 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
@@ -243,7 +200,6 @@ 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
@@ -254,23 +210,20 @@ 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: 60
timeout-minutes: 120
name: Create a macOS bundle
runs-on:
- self-hosted
@@ -359,9 +312,9 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-linux:
bundle-linux-x86_x64:
timeout-minutes: 60
name: Create a Linux bundle
name: Linux x86_x64 release bundle
runs-on:
- buildjet-16vcpu-ubuntu-2004
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
@@ -409,7 +362,7 @@ jobs:
bundle-linux-aarch64: # this runs on ubuntu22.04
timeout-minutes: 60
name: Create arm64 Linux bundle
name: Linux arm64 release bundle
runs-on:
- buildjet-16vcpu-ubuntu-2204-arm
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
@@ -458,7 +411,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, bundle-linux-aarch64]
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64]
runs-on:
- self-hosted
- bundle

View File

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

18
Cargo.lock generated
View File

@@ -2666,6 +2666,7 @@ dependencies = [
"envy",
"extension",
"file_finder",
"fireworks",
"fs",
"futures 0.3.31",
"git",
@@ -4590,6 +4591,17 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "fireworks"
version = "0.1.0"
dependencies = [
"anyhow",
"futures 0.3.31",
"http_client",
"serde",
"serde_json",
]
[[package]]
name = "fixedbitset"
version = "0.4.2"
@@ -4794,13 +4806,11 @@ dependencies = [
"rope",
"serde",
"serde_json",
"shlex",
"smol",
"tempfile",
"text",
"time",
"util",
"which 6.0.3",
"windows 0.58.0",
]
@@ -6297,6 +6307,7 @@ dependencies = [
"futures 0.3.31",
"gpui",
"indoc",
"inline_completion",
"language",
"lsp",
"paths",
@@ -16199,7 +16210,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.169.0"
version = "0.169.3"
dependencies = [
"activity_indicator",
"anyhow",
@@ -16653,6 +16664,7 @@ dependencies = [
"tree-sitter-go",
"tree-sitter-rust",
"ui",
"util",
"uuid",
"workspace",
"worktree",

View File

@@ -40,6 +40,7 @@ members = [
"crates/feedback",
"crates/file_finder",
"crates/file_icons",
"crates/fireworks",
"crates/fs",
"crates/fsevent",
"crates/fuzzy",
@@ -222,6 +223,7 @@ 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" }

View File

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

View File

@@ -55,7 +55,7 @@
}
},
{
"context": "Workspace && !Terminal",
"context": "Workspace",
"bindings": {
"ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal
"ctrl-x 5 0": "workspace::CloseWindow", // delete-frame
@@ -72,6 +72,18 @@
"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

@@ -110,7 +110,7 @@
"g y": "editor::GoToTypeDefinition",
"g shift-i": "editor::GoToImplementation",
"g x": "editor::OpenUrl",
"g f": "editor::OpenFile",
"g f": "editor::OpenSelectedFilename",
"g n": "vim::SelectNextMatch",
"g shift-n": "vim::SelectPreviousMatch",
"g l": "vim::SelectNext",

View File

@@ -16,7 +16,9 @@ use editor::{
EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot,
ToOffset as _, ToPoint,
};
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
use feature_flags::{
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedPro,
};
use fs::Fs;
use futures::{
channel::mpsc,
@@ -73,7 +75,16 @@ 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();
}
@@ -91,6 +102,7 @@ pub struct InlineAssistant {
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
fs: Arc<dyn Fs>,
is_assistant2_enabled: bool,
}
impl Global for InlineAssistant {}
@@ -112,6 +124,7 @@ impl InlineAssistant {
prompt_builder,
telemetry,
fs,
is_assistant2_enabled: false,
}
}
@@ -172,15 +185,22 @@ 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| {
editor.push_code_action_provider(
Rc::new(AssistantCodeActionProvider {
editor: cx.view().downgrade(),
workspace: workspace.downgrade(),
}),
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,
);
}
});
}
}
@@ -1184,6 +1204,7 @@ impl InlineAssistant {
editor.set_show_wrap_guides(false, cx);
editor.set_show_gutter(false, cx);
editor.scroll_manager.set_forbid_vertical_scroll(true);
editor.set_show_scrollbars(false, cx);
editor.set_read_only(true);
editor.set_show_inline_completions(Some(false), cx);
editor.highlight_rows::<DeletedLines>(
@@ -3426,7 +3447,13 @@ 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 @@ 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;
@@ -53,7 +54,16 @@ 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();
}
@@ -76,6 +86,7 @@ pub struct InlineAssistant {
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
fs: Arc<dyn Fs>,
is_assistant2_enabled: bool,
}
impl Global for InlineAssistant {}
@@ -97,6 +108,7 @@ impl InlineAssistant {
prompt_builder,
telemetry,
fs,
is_assistant2_enabled: false,
}
}
@@ -157,21 +169,31 @@ 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| {
let thread_store = workspace
.read(cx)
.panel::<AssistantPanel>(cx)
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
if is_assistant2_enabled {
let thread_store = workspace
.read(cx)
.panel::<AssistantPanel>(cx)
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
editor.push_code_action_provider(
Rc::new(AssistantCodeActionProvider {
editor: cx.view().downgrade(),
workspace: workspace.downgrade(),
thread_store,
}),
cx,
);
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);
}
});
}
}
@@ -1254,6 +1276,7 @@ impl InlineAssistant {
editor.set_show_wrap_guides(false, cx);
editor.set_show_gutter(false, cx);
editor.scroll_manager.set_forbid_vertical_scroll(true);
editor.set_show_scrollbars(false, cx);
editor.set_read_only(true);
editor.set_show_inline_completions(Some(false), cx);
editor.highlight_rows::<DeletedLines>(
@@ -1573,7 +1596,13 @@ 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

@@ -34,6 +34,7 @@ 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

@@ -440,8 +440,11 @@ async fn predict_edits(
_country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
Json(params): Json<PredictEditsParams>,
) -> Result<impl IntoResponse> {
if !claims.is_staff {
return Err(anyhow!("not found"))?;
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(),
));
}
let api_url = state
@@ -459,29 +462,66 @@ async fn predict_edits(
.prediction_model
.as_ref()
.context("no PREDICTION_MODEL configured on the server")?;
let outline_prefix = params
.outline
.as_ref()
.map(|outline| format!("### Outline for current file:\n{}\n", outline))
.unwrap_or_default();
let prompt = include_str!("./llm/prediction_prompt.md")
.replace("<outline>", &outline_prefix)
.replace("<events>", &params.input_events)
.replace("<excerpt>", &params.input_excerpt);
let mut response = open_ai::complete_text(
let request_start = std::time::Instant::now();
let mut response = fireworks::complete(
&state.http_client,
api_url,
api_key,
open_ai::CompletionRequest {
fireworks::CompletionRequest {
model: model.to_string(),
prompt: prompt.clone(),
max_tokens: 1024,
max_tokens: 2048,
temperature: 0.,
prediction: Some(open_ai::Prediction::Content {
prediction: Some(fireworks::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

@@ -1,3 +1,4 @@
<outline>## Task
Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
### Instruction:

View File

@@ -22,6 +22,8 @@ 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>,
@@ -37,6 +39,7 @@ 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>,
@@ -58,6 +61,7 @@ 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,6 +4025,7 @@ 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"))?
@@ -4061,6 +4062,7 @@ 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

@@ -17,8 +17,8 @@ pub struct CopilotCompletionProvider {
completions: Vec<Completion>,
active_completion_index: usize,
file_extension: Option<String>,
pending_refresh: Task<Result<()>>,
pending_cycling_refresh: Task<Result<()>>,
pending_refresh: Option<Task<Result<()>>>,
pending_cycling_refresh: Option<Task<Result<()>>>,
copilot: Model<Copilot>,
}
@@ -30,8 +30,8 @@ impl CopilotCompletionProvider {
completions: Vec::new(),
active_completion_index: 0,
file_extension: None,
pending_refresh: Task::ready(Ok(())),
pending_cycling_refresh: Task::ready(Ok(())),
pending_refresh: None,
pending_cycling_refresh: None,
copilot,
}
}
@@ -67,6 +67,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
false
}
fn is_refreshing(&self) -> bool {
self.pending_refresh.is_some()
}
fn is_enabled(
&self,
buffer: &Model<Buffer>,
@@ -92,7 +96,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
cx: &mut ModelContext<Self>,
) {
let copilot = self.copilot.clone();
self.pending_refresh = cx.spawn(|this, mut cx| async move {
self.pending_refresh = Some(cx.spawn(|this, mut cx| async move {
if debounce {
cx.background_executor()
.timer(COPILOT_DEBOUNCE_TIMEOUT)
@@ -108,7 +112,8 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
this.update(&mut cx, |this, cx| {
if !completions.is_empty() {
this.cycled = false;
this.pending_cycling_refresh = Task::ready(Ok(()));
this.pending_refresh = None;
this.pending_cycling_refresh = None;
this.completions.clear();
this.active_completion_index = 0;
this.buffer_id = Some(buffer.entity_id());
@@ -129,7 +134,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
})?;
Ok(())
});
}));
}
fn cycle(
@@ -161,7 +166,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
cx.notify();
} else {
let copilot = self.copilot.clone();
self.pending_cycling_refresh = cx.spawn(|this, mut cx| async move {
self.pending_cycling_refresh = Some(cx.spawn(|this, mut cx| async move {
let completions = copilot
.update(&mut cx, |copilot, cx| {
copilot.completions_cycling(&buffer, cursor_position, cx)
@@ -185,7 +190,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
})?;
Ok(())
});
}));
}
}

View File

@@ -1,8 +1,8 @@
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,
Model, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
UniformListScrollHandle, ViewContext, WeakView,
div, pulsating_between, px, uniform_list, Animation, AnimationExt, AnyElement,
BackgroundExecutor, Div, FontWeight, ListSizingBehavior, Model, ScrollStrategy, SharedString,
Size, StrikethroughStyle, StyledText, UniformListScrollHandle, ViewContext, WeakView,
};
use language::Buffer;
use language::{CodeLabel, Documentation};
@@ -10,6 +10,8 @@ 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},
@@ -158,7 +160,7 @@ pub struct CompletionsMenu {
pub buffer: Model<Buffer>,
pub completions: Rc<RefCell<Box<[Completion]>>>,
match_candidates: Rc<[StringMatchCandidate]>,
pub entries: Rc<[CompletionEntry]>,
pub entries: Rc<RefCell<Vec<CompletionEntry>>>,
pub selected_item: usize,
scroll_handle: UniformListScrollHandle,
resolve_completions: bool,
@@ -195,7 +197,7 @@ impl CompletionsMenu {
show_completion_documentation,
completions: RefCell::new(completions).into(),
match_candidates,
entries: Vec::new().into(),
entries: RefCell::new(Vec::new()).into(),
selected_item: 0,
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: true,
@@ -244,7 +246,7 @@ impl CompletionsMenu {
string: completion.clone(),
})
})
.collect();
.collect::<Vec<_>>();
Self {
id,
sort_completions,
@@ -252,7 +254,7 @@ impl CompletionsMenu {
buffer,
completions: RefCell::new(completions).into(),
match_candidates,
entries,
entries: RefCell::new(entries).into(),
selected_item: 0,
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: false,
@@ -290,7 +292,8 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.update_selection_index(self.entries.len() - 1, provider, cx);
let index = self.entries.borrow().len() - 1;
self.update_selection_index(index, provider, cx);
}
fn update_selection_index(
@@ -312,12 +315,12 @@ impl CompletionsMenu {
if self.selected_item > 0 {
self.selected_item - 1
} else {
self.entries.len() - 1
self.entries.borrow().len() - 1
}
}
fn next_match_index(&self) -> usize {
if self.selected_item + 1 < self.entries.len() {
if self.selected_item + 1 < self.entries.borrow().len() {
self.selected_item + 1
} else {
0
@@ -326,24 +329,15 @@ impl CompletionsMenu {
pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {
let hint = CompletionEntry::InlineCompletionHint(hint);
self.entries = match self.entries.first() {
let mut entries = self.entries.borrow_mut();
match entries.first() {
Some(CompletionEntry::InlineCompletionHint { .. }) => {
let mut entries = Vec::from(&*self.entries);
entries[0] = hint;
entries
}
_ => {
if self.selected_item != 0 {
self.selected_item += 1;
}
let mut entries = Vec::with_capacity(self.entries.len() + 1);
entries.push(hint);
entries.extend_from_slice(&self.entries);
entries
entries.insert(0, hint);
}
}
.into();
}
pub fn resolve_visible_completions(
@@ -369,13 +363,14 @@ impl CompletionsMenu {
let visible_count = last_rendered_range
.clone()
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
let entries = self.entries.borrow();
let entry_range = if self.selected_item == 0 {
0..min(visible_count, self.entries.len())
} else if self.selected_item == self.entries.len() - 1 {
self.entries.len().saturating_sub(visible_count)..self.entries.len()
0..min(visible_count, entries.len())
} else if self.selected_item == entries.len() - 1 {
entries.len().saturating_sub(visible_count)..entries.len()
} else {
last_rendered_range.map_or(0..0, |range| {
min(range.start, self.entries.len())..min(range.end, self.entries.len())
min(range.start, entries.len())..min(range.end, entries.len())
})
};
@@ -386,24 +381,25 @@ impl CompletionsMenu {
entry_range.clone(),
EXTRA_TO_RESOLVE,
EXTRA_TO_RESOLVE,
self.entries.len(),
entries.len(),
);
// Avoid work by sometimes filtering out completions that already have documentation.
// This filtering doesn't happen if the completions are currently being updated.
let completions = self.completions.borrow();
let candidate_ids = entry_indices
.flat_map(|i| Self::entry_candidate_id(&self.entries[i]))
.flat_map(|i| Self::entry_candidate_id(&entries[i]))
.filter(|i| completions[*i].documentation.is_none());
// Current selection is always resolved even if it already has documentation, to handle
// out-of-spec language servers that return more results later.
let candidate_ids = match Self::entry_candidate_id(&self.entries[self.selected_item]) {
let candidate_ids = match Self::entry_candidate_id(&entries[self.selected_item]) {
None => candidate_ids.collect::<Vec<usize>>(),
Some(selected_candidate_id) => iter::once(selected_candidate_id)
.chain(candidate_ids.filter(|id| *id != selected_candidate_id))
.collect::<Vec<usize>>(),
};
drop(entries);
if candidate_ids.is_empty() {
return;
@@ -432,7 +428,7 @@ impl CompletionsMenu {
}
pub fn visible(&self) -> bool {
!self.entries.is_empty()
!self.entries.borrow().is_empty()
}
fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
@@ -449,6 +445,7 @@ impl CompletionsMenu {
let show_completion_documentation = self.show_completion_documentation;
let widest_completion_ix = self
.entries
.borrow()
.iter()
.enumerate()
.max_by_key(|(_, mat)| match mat {
@@ -465,33 +462,38 @@ impl CompletionsMenu {
len
}
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
provider_name,
..
}) => provider_name.len(),
CompletionEntry::InlineCompletionHint(hint) => {
"Zed AI / ".chars().count() + hint.label().chars().count()
}
})
.map(|(ix, _)| ix);
drop(completions);
let selected_item = self.selected_item;
let completions = self.completions.clone();
let matches = self.entries.clone();
let entries = self.entries.clone();
let last_rendered_range = self.last_rendered_range.clone();
let style = style.clone();
let list = uniform_list(
cx.view().clone(),
"completions",
matches.len(),
self.entries.borrow().len(),
move |_editor, range, cx| {
last_rendered_range.borrow_mut().replace(range.clone());
let start_ix = range.start;
let completions_guard = completions.borrow_mut();
matches[range]
entries.borrow()[range]
.iter()
.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;
@@ -575,20 +577,57 @@ impl CompletionsMenu {
.end_slot::<Label>(documentation_label),
)
}
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
provider_name,
..
}) => div().min_w(px(250.)).max_w(px(500.)).child(
CompletionEntry::InlineCompletionHint(
hint @ InlineCompletionMenuHint::None,
) => 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(
StyledText::new(format!(
"{} Completion",
SharedString::new_static(provider_name)
))
.with_highlights(&style.text, None),
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),
),
)
.on_click(cx.listener(move |editor, _event, cx| {
cx.stop_propagation();
@@ -623,7 +662,7 @@ impl CompletionsMenu {
return None;
}
let multiline_docs = match &self.entries[self.selected_item] {
let multiline_docs = match &self.entries.borrow()[self.selected_item] {
CompletionEntry::Match(mat) => {
match self.completions.borrow_mut()[mat.candidate_id]
.documentation
@@ -645,19 +684,20 @@ impl CompletionsMenu {
Documentation::Undocumented => 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()),
},
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,
};
Some(
@@ -769,12 +809,14 @@ impl CompletionsMenu {
}
drop(completions);
let mut new_entries: Vec<_> = matches.into_iter().map(CompletionEntry::Match).collect();
if let Some(CompletionEntry::InlineCompletionHint(hint)) = self.entries.first() {
new_entries.insert(0, CompletionEntry::InlineCompletionHint(hint.clone()));
let mut entries = self.entries.borrow_mut();
if let Some(CompletionEntry::InlineCompletionHint(_)) = entries.first() {
entries.truncate(1);
} else {
entries.truncate(0);
}
entries.extend(matches.into_iter().map(CompletionEntry::Match));
self.entries = new_entries.into();
self.selected_item = 0;
}
}

View File

@@ -459,9 +459,21 @@ pub fn make_suggestion_styles(cx: &WindowContext) -> InlineCompletionStyles {
type CompletionId = usize;
#[derive(Debug, Clone)]
struct InlineCompletionMenuHint {
provider_name: &'static str,
text: InlineCompletionText,
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",
}
}
}
#[derive(Clone, Debug)]
@@ -1727,8 +1739,12 @@ impl Editor {
self.input_enabled = input_enabled;
}
pub fn set_inline_completions_enabled(&mut self, enabled: bool) {
pub fn set_inline_completions_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
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) {
@@ -1787,6 +1803,17 @@ impl Editor {
self.refresh_inline_completion(false, true, cx);
}
pub fn inline_completions_enabled(&self, cx: &AppContext) -> bool {
let cursor = self.selections.newest_anchor().head();
if let Some((buffer, buffer_position)) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)
{
self.should_show_inline_completions(&buffer, buffer_position, cx)
} else {
false
}
}
fn should_show_inline_completions(
&self,
buffer: &Model<Buffer>,
@@ -3808,6 +3835,26 @@ 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
@@ -3815,12 +3862,10 @@ impl Editor {
return None;
};
let mat = completions_menu
.entries
.get(item_ix.unwrap_or(completions_menu.selected_item))?;
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(())));
@@ -3832,12 +3877,14 @@ impl Editor {
mat
}
};
let candidate_id = mat.candidate_id;
drop(entries);
let buffer_handle = completions_menu.buffer;
let completion = completions_menu
.completions
.borrow()
.get(mat.candidate_id)?
.get(candidate_id)?
.clone();
cx.stop_propagation();
@@ -3986,7 +4033,7 @@ impl Editor {
let apply_edits = provider.apply_additional_edits_for_completion(
buffer_handle,
completions_menu.completions.clone(),
mat.candidate_id,
candidate_id,
true,
cx,
);
@@ -4283,15 +4330,29 @@ impl Editor {
self.available_code_actions.take();
}
pub fn push_code_action_provider(
pub fn add_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();
@@ -4481,7 +4542,8 @@ impl Editor {
if !user_requested
&& (!self.enable_inline_completions
|| !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx)
|| !self.is_focused(cx))
|| !self.is_focused(cx)
|| buffer.read(cx).is_empty())
{
self.discard_inline_completion(false, cx);
return None;
@@ -4571,6 +4633,23 @@ impl Editor {
_: &AcceptInlineCompletion,
cx: &mut ViewContext<Self>,
) {
let buffer = self.buffer.read(cx);
let snapshot = buffer.snapshot(cx);
let selection = self.selections.newest_adjusted(cx);
let cursor = selection.head();
let current_indent = snapshot.indent_size_for_line(MultiBufferRow(cursor.row));
let suggested_indents = snapshot.suggested_indents([cursor.row], cx);
if let Some(suggested_indent) = suggested_indents.get(&MultiBufferRow(cursor.row)).copied()
{
if cursor.column < suggested_indent.len
&& cursor.column <= current_indent.len
&& current_indent.len <= suggested_indent.len
{
self.tab(&Default::default(), cx);
return;
}
}
if self.show_inline_completions_in_menu(cx) {
self.hide_context_menu(cx);
}
@@ -4636,8 +4715,19 @@ impl Editor {
});
}
InlineCompletion::Edit(edits) => {
if edits.len() == 1 && edits[0].0.start == edits[0].0.end {
let text = edits[0].1.as_str();
// Find an insertion that starts at the cursor position.
let snapshot = self.buffer.read(cx).snapshot(cx);
let cursor_offset = self.selections.newest::<usize>(cx).head();
let insertion = edits.iter().find_map(|(range, text)| {
let range = range.to_offset(&snapshot);
if range.is_empty() && range.start == cursor_offset {
Some(text)
} else {
None
}
});
if let Some(text) = insertion {
let mut partial_completion = text
.chars()
.by_ref()
@@ -4660,6 +4750,8 @@ impl Editor {
self.refresh_inline_completion(true, true, cx);
cx.notify();
} else {
self.accept_inline_completion(&Default::default(), cx);
}
}
}
@@ -4734,6 +4826,7 @@ 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()
@@ -4856,8 +4949,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 {
@@ -4874,12 +4967,11 @@ impl Editor {
}
};
Some(InlineCompletionMenuHint {
provider_name,
text,
})
Some(InlineCompletionMenuHint::Loaded { text })
} else if provider.is_refreshing(cx) {
Some(InlineCompletionMenuHint::Loading)
} else {
None
Some(InlineCompletionMenuHint::None)
}
}
@@ -5110,9 +5202,11 @@ impl Editor {
.borrow()
.as_ref()
.map_or(false, |menu| match menu {
CodeContextMenu::Completions(menu) => menu.entries.first().map_or(false, |entry| {
matches!(entry, CompletionEntry::InlineCompletionHint(_))
}),
CodeContextMenu::Completions(menu) => {
menu.entries.borrow().first().map_or(false, |entry| {
matches!(entry, CompletionEntry::InlineCompletionHint(_))
})
}
CodeContextMenu::CodeActions(_) => false,
})
}
@@ -13529,6 +13623,8 @@ pub trait CompletionProvider {
}
pub trait CodeActionProvider {
fn id(&self) -> Arc<str>;
fn code_actions(
&self,
buffer: &Model<Buffer>,
@@ -13547,6 +13643,10 @@ pub trait CodeActionProvider {
}
impl CodeActionProvider for Model<Project> {
fn id(&self) -> Arc<str> {
"project".into()
}
fn code_actions(
&self,
buffer: &Model<Buffer>,

View File

@@ -8473,7 +8473,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(completion_menu_entries(&menu.entries), &["first", "last"]);
assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
} else {
panic!("expected completion menu to be open");
}
@@ -8566,7 +8566,7 @@ async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(
completion_menu_entries(&menu.entries),
completion_menu_entries(&menu),
&["r", "ret", "Range", "return"]
);
} else {
@@ -11080,6 +11080,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
assert_eq!(
completions_menu
.entries
.borrow()
.iter()
.flat_map(|c| match c {
CompletionEntry::Match(mat) => Some(mat.string.clone()),
@@ -11190,7 +11191,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(
completion_menu_entries(&menu.entries),
completion_menu_entries(&menu),
&["bg-red", "bg-blue", "bg-yellow"]
);
} else {
@@ -11203,10 +11204,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(
completion_menu_entries(&menu.entries),
&["bg-blue", "bg-yellow"]
);
assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
} else {
panic!("expected completion menu to be open");
}
@@ -11220,18 +11218,19 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(completion_menu_entries(&menu.entries), &["bg-yellow"]);
assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
} else {
panic!("expected completion menu to be open");
}
});
}
fn completion_menu_entries(entries: &[CompletionEntry]) -> Vec<&str> {
fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
let entries = menu.entries.borrow();
entries
.iter()
.flat_map(|e| match e {
CompletionEntry::Match(mat) => Some(mat.string.as_str()),
CompletionEntry::Match(mat) => Some(mat.string.clone()),
_ => None,
})
.collect()

View File

@@ -543,8 +543,29 @@ 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(&OpenExcerpts, cx);
editor.open_excerpts_common(
Some(JumpData::MultiBufferRow {
row: MultiBufferRow(multi_buffer_row),
line_offset_from_top,
}),
false,
cx,
);
return;
}
}
@@ -1312,11 +1333,15 @@ impl EditorElement {
total_text_units
.horizontal
.zip(track_bounds.horizontal)
.map(|(total_text_units_x, track_bounds_x)| {
.and_then(|(total_text_units_x, track_bounds_x)| {
if text_units_per_page.horizontal >= total_text_units_x {
return None;
}
let thumb_percent =
(text_units_per_page.horizontal / total_text_units_x).min(1.);
track_bounds_x.size.width * thumb_percent
Some(track_bounds_x.size.width * thumb_percent)
}),
total_text_units.vertical.zip(track_bounds.vertical).map(
|(total_text_units_y, track_bounds_y)| {

View File

@@ -1,8 +1,9 @@
use gpui::{prelude::*, Model};
use indoc::indoc;
use inline_completion::InlineCompletionProvider;
use language::{Language, LanguageConfig};
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
use std::ops::Range;
use std::{num::NonZeroU32, ops::Range, sync::Arc};
use text::{Point, ToOffset};
use crate::{
@@ -122,6 +123,54 @@ async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) {
"});
}
#[gpui::test]
async fn test_indentation(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.tab_size = NonZeroU32::new(4)
});
let language = Arc::new(
Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_indents_query(r#"(_ "(" ")" @end) @indent"#)
.unwrap(),
);
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
assign_editor_completion_provider(provider.clone(), &mut cx);
cx.set_state(indoc! {"
const a: A = (
ˇ
);
"});
propose_edits(
&provider,
vec![(Point::new(1, 0)..Point::new(1, 0), " const function()")],
&mut cx,
);
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
assert_editor_active_edit_completion(&mut cx, |_, edits| {
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].1.as_str(), " const function()");
});
// When the cursor is before the suggested indentation level, accepting a
// completion should just indent.
accept_completion(&mut cx);
cx.assert_editor_state(indoc! {"
const a: A = (
ˇ
);
"});
}
#[gpui::test]
async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -338,6 +387,10 @@ impl InlineCompletionProvider for FakeInlineCompletionProvider {
true
}
fn is_refreshing(&self) -> bool {
false
}
fn refresh(
&mut self,
_buffer: gpui::Model<language::Buffer>,

View File

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

View File

@@ -0,0 +1,19 @@
[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

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

View File

@@ -0,0 +1,173 @@
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,12 +47,9 @@ 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,9 +9,6 @@ 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)]
@@ -521,7 +518,24 @@ impl Fs for RealFs {
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
smol::unblock(move || {
let mut tmp_file = create_temp_file(&path)?;
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()
}?;
tmp_file.write_all(data.as_bytes())?;
tmp_file.persist(path)?;
Ok::<(), anyhow::Error>(())
@@ -536,43 +550,13 @@ impl Fs for RealFs {
if let Some(path) = path.parent() {
self.create_dir(path).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()),
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?;
}
writer.flush().await?;
Ok(())
}
async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
@@ -1979,84 +1963,6 @@ 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, TOOLTIP_DELAY,
WrappedLine, WrappedLineLayout, TOOLTIP_DELAY,
};
use anyhow::anyhow;
use parking_lot::{Mutex, MutexGuard};
@@ -443,6 +443,36 @@ 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

@@ -28,6 +28,7 @@ pub trait InlineCompletionProvider: 'static + Sized {
cursor_position: language::Anchor,
cx: &AppContext,
) -> bool;
fn is_refreshing(&self) -> bool;
fn refresh(
&mut self,
buffer: Model<Buffer>,
@@ -63,6 +64,7 @@ pub trait InlineCompletionProviderHandle {
) -> bool;
fn show_completions_in_menu(&self) -> bool;
fn show_completions_in_normal_mode(&self) -> bool;
fn is_refreshing(&self, cx: &AppContext) -> bool;
fn refresh(
&self,
buffer: Model<Buffer>,
@@ -116,6 +118,10 @@ where
self.read(cx).is_enabled(buffer, cursor_position, cx)
}
fn is_refreshing(&self, cx: &AppContext) -> bool {
self.read(cx).is_refreshing()
}
fn refresh(
&self,
buffer: Model<Buffer>,

View File

@@ -19,6 +19,7 @@ editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
gpui.workspace = true
inline_completion.workspace = true
language.workspace = true
paths.workspace = true
settings.workspace = true

View File

@@ -1,11 +1,12 @@
use anyhow::Result;
use copilot::{Copilot, Status};
use editor::{scroll::Autoscroll, Editor};
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag};
use fs::Fs;
use gpui::{
actions, div, Action, AppContext, AsyncWindowContext, Corner, Entity, IntoElement,
ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext,
actions, div, pulsating_between, Action, Animation, AnimationExt, AppContext,
AsyncWindowContext, Corner, Entity, IntoElement, ParentElement, Render, Subscription, View,
ViewContext, WeakView, WindowContext,
};
use language::{
language_settings::{
@@ -14,7 +15,7 @@ use language::{
File, Language,
};
use settings::{update_settings_file, Settings, SettingsStore};
use std::{path::Path, sync::Arc};
use std::{path::Path, sync::Arc, time::Duration};
use supermaven::{AccountStatus, Supermaven};
use workspace::{
create_and_open_local_file,
@@ -39,6 +40,7 @@ pub struct InlineCompletionButton {
editor_enabled: Option<bool>,
language: Option<Arc<Language>>,
file: Option<Arc<dyn File>>,
inline_completion_provider: Option<Arc<dyn inline_completion::InlineCompletionProviderHandle>>,
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
}
@@ -199,29 +201,40 @@ impl Render for InlineCompletionButton {
);
}
InlineCompletionProvider::Zeta => {
if !cx.has_flag::<ZetaFeatureFlag>() {
InlineCompletionProvider::Zed => {
if !cx.has_flag::<PredictEditsFeatureFlag>() {
return div();
}
div().child(
IconButton::new("zeta", IconName::ZedPredict)
.tooltip(|cx| {
Tooltip::with_meta(
"Zed Predict",
Some(&RateCompletions),
"Click to rate completions",
cx,
)
})
.on_click(cx.listener(|this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
RateCompletionModal::toggle(workspace, cx)
});
}
})),
)
let this = cx.view().clone();
let button = IconButton::new("zeta", IconName::ZedPredict)
.tooltip(|cx| Tooltip::text("Edit Prediction", cx));
let is_refreshing = self
.inline_completion_provider
.as_ref()
.map_or(false, |provider| provider.is_refreshing(cx));
let mut popover_menu = PopoverMenu::new("zeta")
.menu(move |cx| {
Some(this.update(cx, |this, cx| this.build_zeta_context_menu(cx)))
})
.anchor(Corner::BottomRight);
if is_refreshing {
popover_menu = popover_menu.trigger(
button.with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.2, 1.0)),
|icon_button, delta| icon_button.alpha(delta),
),
);
} else {
popover_menu = popover_menu.trigger(button);
}
div().child(popover_menu.into_any_element())
}
}
}
@@ -245,6 +258,7 @@ impl InlineCompletionButton {
editor_enabled: None,
language: None,
file: None,
inline_completion_provider: None,
workspace,
fs,
}
@@ -360,6 +374,25 @@ impl InlineCompletionButton {
})
}
fn build_zeta_context_menu(&self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
let workspace = self.workspace.clone();
ContextMenu::build(cx, |menu, cx| {
self.build_language_settings_menu(menu, cx)
.separator()
.entry(
"Rate Completions",
Some(RateCompletions.boxed_clone()),
move |cx| {
workspace
.update(cx, |workspace, cx| {
RateCompletionModal::toggle(workspace, cx)
})
.ok();
},
)
})
}
pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
@@ -377,6 +410,7 @@ impl InlineCompletionButton {
),
)
};
self.inline_completion_provider = editor.inline_completion_provider();
self.language = language.cloned();
self.file = file;

View File

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

View File

@@ -212,9 +212,18 @@ impl LspAdapter for TypeScriptLspAdapter {
_ => None,
}?;
let text = match &item.detail {
Some(detail) => format!("{} {}", item.label, detail),
None => item.label.clone(),
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()
};
Some(language::CodeLabel {

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" | "command-r" | "deepseek-coder-v2" | "yi-coder"
| "llama3.2" => 128000,
"llama3.1" | "phi3" | "phi3.5" | "phi4" | "command-r" | "deepseek-coder-v2"
| "yi-coder" | "llama3.2" => 128000,
_ => DEFAULT_TOKENS,
}
.clamp(1, MAXIMUM_TOKENS)

View File

@@ -36,6 +36,7 @@ pub struct PerformCompletionParams {
#[derive(Debug, Serialize, Deserialize)]
pub struct PredictEditsParams {
pub outline: Option<String>,
pub input_events: String,
pub input_excerpt: String,
}

View File

@@ -19,7 +19,7 @@ pub struct SupermavenCompletionProvider {
buffer_id: Option<EntityId>,
completion_id: Option<SupermavenCompletionStateId>,
file_extension: Option<String>,
pending_refresh: Task<Result<()>>,
pending_refresh: Option<Task<Result<()>>>,
}
impl SupermavenCompletionProvider {
@@ -29,7 +29,7 @@ impl SupermavenCompletionProvider {
buffer_id: None,
completion_id: None,
file_extension: None,
pending_refresh: Task::ready(Ok(())),
pending_refresh: None,
}
}
}
@@ -122,6 +122,10 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx)
}
fn is_refreshing(&self) -> bool {
self.pending_refresh.is_some()
}
fn refresh(
&mut self,
buffer_handle: Model<Buffer>,
@@ -135,7 +139,7 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
return;
};
self.pending_refresh = cx.spawn(|this, mut cx| async move {
self.pending_refresh = Some(cx.spawn(|this, mut cx| async move {
if debounce {
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
}
@@ -152,11 +156,12 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
.to_string(),
)
});
this.pending_refresh = None;
cx.notify();
})?;
}
Ok(())
});
}));
}
fn cycle(
@@ -169,12 +174,12 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
}
fn accept(&mut self, _cx: &mut ModelContext<Self>) {
self.pending_refresh = Task::ready(Ok(()));
self.pending_refresh = None;
self.completion_id = None;
}
fn discard(&mut self, _cx: &mut ModelContext<Self>) {
self.pending_refresh = Task::ready(Ok(()));
self.pending_refresh = None;
self.completion_id = None;
}

View File

@@ -146,13 +146,16 @@ 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 {
let activate_item = Some(item.item_id().as_u64()) == active_item;
if Some(item.item_id().as_u64()) == active_item {
active_item_index = Some(item_index);
}
pane.add_item(Box::new(item), false, false, None, cx);
item_index += 1;
if activate_item {
pane.activate_item(item_index, false, false, cx);
}
}
if let Some(index) = active_item_index {
pane.activate_item(index, false, false, cx);
}
}

View File

@@ -31,7 +31,7 @@ use ui::{
};
use util::{ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
dock::{DockPosition, Panel, PanelEvent, PanelHandle},
item::SerializableItem,
move_active_item, move_item, pane,
ui::IconName,
@@ -75,6 +75,7 @@ pub struct TerminalPanel {
deferred_tasks: HashMap<TaskId, Task<()>>,
assistant_enabled: bool,
assistant_tab_bar_button: Option<AnyView>,
active: bool,
}
impl TerminalPanel {
@@ -82,7 +83,6 @@ 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,6 +95,7 @@ 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
@@ -281,6 +282,25 @@ 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)
}
@@ -1339,7 +1359,9 @@ impl Panel for TerminalPanel {
}
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if !active || !self.has_no_terminals(cx) {
let old_active = self.active;
self.active = active;
if !active || old_active == active || !self.has_no_terminals(cx) {
return;
}
cx.defer(|this, cx| {

View File

@@ -22,6 +22,7 @@ pub struct IconButton {
icon_size: IconSize,
icon_color: Color,
selected_icon: Option<IconName>,
alpha: Option<f32>,
}
impl IconButton {
@@ -33,6 +34,7 @@ impl IconButton {
icon_size: IconSize::default(),
icon_color: Color::Default,
selected_icon: None,
alpha: None,
};
this.base.base = this.base.base.debug_selector(|| format!("ICON-{:?}", icon));
this
@@ -53,6 +55,11 @@ impl IconButton {
self
}
pub fn alpha(mut self, alpha: f32) -> Self {
self.alpha = Some(alpha);
self
}
pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
self.selected_icon = icon.into();
self
@@ -146,6 +153,7 @@ impl RenderOnce for IconButton {
let is_selected = self.base.selected;
let selected_style = self.base.selected_style;
let color = self.icon_color.color(cx).opacity(self.alpha.unwrap_or(1.0));
self.base
.map(|this| match self.shape {
IconButtonShape::Square => {
@@ -161,7 +169,7 @@ impl RenderOnce for IconButton {
.selected_icon(self.selected_icon)
.when_some(selected_style, |this, style| this.selected_style(style))
.size(self.icon_size)
.color(self.icon_color),
.color(Color::Custom(color)),
)
}
}

View File

@@ -15,6 +15,28 @@ pub trait PopoverTrigger: IntoElement + Clickable + Toggleable + 'static {}
impl<T: IntoElement + Clickable + Toggleable + 'static> PopoverTrigger for T {}
impl<T: Clickable> Clickable for gpui::AnimationElement<T>
where
T: Clickable + 'static,
{
fn on_click(self, handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static) -> Self {
self.map_element(|e| e.on_click(handler))
}
fn cursor_style(self, cursor_style: gpui::CursorStyle) -> Self {
self.map_element(|e| e.cursor_style(cursor_style))
}
}
impl<T: Toggleable> Toggleable for gpui::AnimationElement<T>
where
T: Toggleable + 'static,
{
fn toggle_state(self, selected: bool) -> Self {
self.map_element(|e| e.toggle_state(selected))
}
}
pub struct PopoverMenuHandle<M>(Rc<RefCell<Option<PopoverMenuHandleState<M>>>>);
impl<M> Clone for PopoverMenuHandle<M> {

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);
editor.set_inline_completions_enabled(enable_inline_completions, cx);
});
cx.notify()
}

View File

@@ -2295,6 +2295,19 @@ 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,

View File

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

View File

@@ -1 +1 @@
dev
stable

View File

@@ -4,7 +4,7 @@ use client::Client;
use collections::HashMap;
use copilot::{Copilot, CopilotCompletionProvider};
use editor::{Editor, EditorMode};
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag};
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::<ZetaFeatureFlag>() {
if cx.has_flag::<PredictEditsFeatureFlag>() {
cx.on_action(clear_zeta_edit_history);
}
cx.observe_flag::<ZetaFeatureFlag, _>({
cx.observe_flag::<PredictEditsFeatureFlag, _>({
let editors = editors.clone();
let client = client.clone();
move |active, cx| {
@@ -164,8 +164,11 @@ fn assign_inline_completion_provider(
editor.set_inline_completion_provider(Some(provider), cx);
}
}
language::language_settings::InlineCompletionProvider::Zeta => {
if cx.has_flag::<ZetaFeatureFlag>() || cfg!(debug_assertions) {
language::language_settings::InlineCompletionProvider::Zed => {
if cx.has_flag::<PredictEditsFeatureFlag>()
|| (cfg!(debug_assertions) && client.status().borrow().is_connected())
{
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

@@ -94,6 +94,7 @@ impl Render for QuickActionBar {
git_blame_inline_enabled,
show_git_blame_gutter,
auto_signature_help_enabled,
inline_completions_enabled,
) = {
let editor = editor.read(cx);
let selection_menu_enabled = editor.selection_menu_enabled(cx);
@@ -102,6 +103,7 @@ impl Render for QuickActionBar {
let git_blame_inline_enabled = editor.git_blame_inline_enabled();
let show_git_blame_gutter = editor.show_git_blame_gutter();
let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx);
let inline_completions_enabled = editor.inline_completions_enabled(cx);
(
selection_menu_enabled,
@@ -110,6 +112,7 @@ impl Render for QuickActionBar {
git_blame_inline_enabled,
show_git_blame_gutter,
auto_signature_help_enabled,
inline_completions_enabled,
)
};
@@ -283,6 +286,26 @@ impl Render for QuickActionBar {
},
);
menu = menu.toggleable_entry(
"Inline Completions",
inline_completions_enabled,
IconPosition::Start,
Some(editor::actions::ToggleInlineCompletions.boxed_clone()),
{
let editor = editor.clone();
move |cx| {
editor
.update(cx, |editor, cx| {
editor.toggle_inline_completions(
&editor::actions::ToggleInlineCompletions,
cx,
);
})
.ok();
}
},
);
menu = menu.separator();
menu = menu.toggleable_entry(

View File

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

View File

@@ -0,0 +1,161 @@
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,13 +1,11 @@
use crate::{InlineCompletion, InlineCompletionRating, Zeta};
use crate::{CompletionDiffElement, InlineCompletion, InlineCompletionRating, Zeta};
use editor::Editor;
use gpui::{
actions, prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
HighlightStyle, Model, StyledText, TextStyle, View, ViewContext,
actions, prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
View, ViewContext,
};
use language::{language_settings, OffsetRangeExt};
use settings::Settings;
use language::language_settings;
use std::time::Duration;
use theme::ThemeSettings;
use ui::{prelude::*, KeyBinding, List, ListItem, ListItemSpacing, Tooltip};
use workspace::{ModalView, Workspace};
@@ -15,8 +13,6 @@ actions!(
zeta,
[
RateCompletions,
ThumbsUp,
ThumbsDown,
ThumbsUpActiveCompletion,
ThumbsDownActiveCompletion,
NextEdit,
@@ -41,6 +37,7 @@ pub struct RateCompletionModal {
selected_index: usize,
focus_handle: FocusHandle,
_subscription: gpui::Subscription,
current_view: RateCompletionView,
}
struct ActiveCompletion {
@@ -48,6 +45,21 @@ 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) {
@@ -57,12 +69,14 @@ 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,
}
}
@@ -74,7 +88,7 @@ impl RateCompletionModal {
self.selected_index += 1;
self.selected_index = usize::min(
self.selected_index,
self.zeta.read(cx).recent_completions().count(),
self.zeta.read(cx).shown_completions().count(),
);
cx.notify();
}
@@ -88,7 +102,7 @@ impl RateCompletionModal {
let next_index = self
.zeta
.read(cx)
.recent_completions()
.shown_completions()
.skip(self.selected_index)
.enumerate()
.skip(1) // Skip straight to the next item
@@ -103,12 +117,12 @@ impl RateCompletionModal {
fn select_prev_edit(&mut self, _: &PreviousEdit, cx: &mut ViewContext<Self>) {
let zeta = self.zeta.read(cx);
let completions_len = zeta.recent_completions_len();
let completions_len = zeta.shown_completions_len();
let prev_index = self
.zeta
.read(cx)
.recent_completions()
.shown_completions()
.rev()
.skip((completions_len - 1) - self.selected_index)
.enumerate()
@@ -129,28 +143,7 @@ impl RateCompletionModal {
}
fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
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);
self.selected_index = self.zeta.read(cx).shown_completions_len() - 1;
cx.notify();
}
@@ -177,7 +170,11 @@ impl RateCompletionModal {
cx.notify();
}
fn thumbs_down_active(&mut self, _: &ThumbsDownActiveCompletion, cx: &mut ViewContext<Self>) {
pub 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;
@@ -213,7 +210,7 @@ impl RateCompletionModal {
let completion = self
.zeta
.read(cx)
.recent_completions()
.shown_completions()
.skip(self.selected_index)
.take(1)
.next()
@@ -226,7 +223,7 @@ impl RateCompletionModal {
let completion = self
.zeta
.read(cx)
.recent_completions()
.shown_completions()
.skip(self.selected_index)
.take(1)
.next()
@@ -246,7 +243,7 @@ impl RateCompletionModal {
self.selected_index = self
.zeta
.read(cx)
.recent_completions()
.shown_completions()
.enumerate()
.find(|(_, completion_b)| completion.id == completion_b.id)
.map(|(ix, _)| ix)
@@ -286,99 +283,127 @@ 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 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 border_color = cx.theme().colors().border;
let bg_color = cx.theme().colors().editor_background;
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 border_color = cx.theme().colors().border;
let bg_color = cx.theme().colors().editor_background;
let label_container = || h_flex().pl_1().gap_1p5();
let label_container = h_flex().pl_1().gap_1p5();
Some(
v_flex()
.size_full()
.overflow_hidden()
.relative()
.child(
div()
.id("diff")
.py_4()
.px_6()
v_flex()
.size_full()
.bg(bg_color)
.overflow_scroll()
.child(StyledText::new(diff).with_highlights(&text_style, diff_highlights)),
.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))
)
.when_some((!rated).then(|| ()), |this, _| {
.when(!rated, |this| {
this.child(
h_flex()
.p_2()
.gap_2()
.border_y_1()
.border_color(border_color)
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
@@ -390,14 +415,14 @@ impl RateCompletionModal {
.pr_2()
.flex_wrap()
.child(
Label::new("Ensure you explain why this completion is negative or positive. In case it's negative, report what you expected instead.")
Label::new("Explain why this completion is good or bad. If it's negative, describe what you expected instead.")
.size(LabelSize::Small)
.color(Color::Muted)
)
)
)
})
.when_some((!rated).then(|| ()), |this, _| {
.when(!rated, |this| {
this.child(
div()
.h_40()
@@ -417,7 +442,7 @@ impl RateCompletionModal {
.justify_between()
.children(if rated {
Some(
label_container()
label_container
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
@@ -427,7 +452,7 @@ impl RateCompletionModal {
)
} else if active_completion.completion.edits.is_empty() {
Some(
label_container()
label_container
.child(
Icon::new(IconName::Warning)
.size(IconSize::Small)
@@ -435,30 +460,14 @@ 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)
@@ -468,6 +477,11 @@ 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,
@@ -477,16 +491,15 @@ 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);
})),
@@ -512,7 +525,6 @@ 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))
@@ -526,16 +538,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)
@@ -561,12 +573,12 @@ impl Render for RateCompletionModal {
div()
.p_2()
.child(
Label::new("No completions yet. Use the editor to generate some and rate them!")
Label::new("No completions yet. Use the editor to generate some, and make sure to rate them!")
.color(Color::Muted),
)
.into_any_element(),
)
.children(self.zeta.read(cx).recent_completions().cloned().enumerate().map(
.children(self.zeta.read(cx).shown_completions().cloned().enumerate().map(
|(index, completion)| {
let selected =
self.active_completion.as_ref().map_or(false, |selected| {
@@ -575,27 +587,45 @@ 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(
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)
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)
)
)
)
.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,5 +1,7 @@
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};
@@ -30,6 +32,7 @@ use std::{
time::{Duration, Instant},
};
use telemetry_events::InlineCompletionRating;
use util::ResultExt;
use uuid::Uuid;
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
@@ -71,6 +74,7 @@ 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>,
@@ -154,9 +158,8 @@ pub struct Zeta {
client: Arc<Client>,
events: VecDeque<Event>,
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
recent_completions: VecDeque<InlineCompletion>,
shown_completions: VecDeque<InlineCompletion>,
rated_completions: HashSet<InlineCompletionId>,
shown_completions: HashSet<InlineCompletionId>,
llm_token: LlmApiToken,
_llm_token_subscription: Subscription,
}
@@ -184,9 +187,8 @@ impl Zeta {
Self {
client,
events: VecDeque::new(),
recent_completions: VecDeque::new(),
shown_completions: VecDeque::new(),
rated_completions: HashSet::default(),
shown_completions: HashSet::default(),
registered_buffers: HashMap::default(),
llm_token: LlmApiToken::default(),
_llm_token_subscription: cx.subscribe(
@@ -205,7 +207,7 @@ impl Zeta {
}
fn push_event(&mut self, event: Event) {
const MAX_EVENT_COUNT: usize = 20;
const MAX_EVENT_COUNT: usize = 16;
if let Some(Event::BufferChange {
new_snapshot: last_new_snapshot,
@@ -231,8 +233,8 @@ impl Zeta {
}
self.events.push_back(event);
if self.events.len() > MAX_EVENT_COUNT {
self.events.pop_front();
if self.events.len() >= MAX_EVENT_COUNT {
self.events.drain(..MAX_EVENT_COUNT / 2);
}
}
@@ -291,38 +293,44 @@ impl Zeta {
let events = self.events.clone();
let path = snapshot
.file()
.map(|f| f.path().clone())
.map(|f| Arc::from(f.full_path(cx).as_path()))
.unwrap_or_else(|| Arc::from(Path::new("untitled")));
let client = self.client.clone();
let llm_token = self.llm_token.clone();
cx.spawn(|this, mut cx| async move {
cx.spawn(|_, cx| async move {
let request_sent_at = Instant::now();
let input_events = cx
let (input_events, input_excerpt, input_outline) = cx
.background_executor()
.spawn(async move {
let mut input_events = String::new();
for event in events {
if !input_events.is_empty() {
input_events.push('\n');
input_events.push('\n');
.spawn({
let snapshot = snapshot.clone();
let excerpt_range = excerpt_range.clone();
async move {
let mut input_events = String::new();
for event in events {
if !input_events.is_empty() {
input_events.push('\n');
input_events.push('\n');
}
input_events.push_str(&event.to_prompt());
}
input_events.push_str(&event.to_prompt());
let input_excerpt = prompt_for_excerpt(&snapshot, &excerpt_range, offset);
let input_outline = prompt_for_outline(&snapshot);
(input_events, input_excerpt, input_outline)
}
input_events
})
.await;
let input_excerpt = prompt_for_excerpt(&snapshot, &excerpt_range, offset);
let input_outline = prompt_for_outline(&snapshot);
log::debug!("Events:\n{}\nExcerpt:\n{}", input_events, input_excerpt);
let body = PredictEditsParams {
input_events: input_events.clone(),
input_excerpt: input_excerpt.clone(),
outline: Some(input_outline.clone()),
};
let response = perform_predict_edits(client, llm_token, body).await?;
@@ -330,10 +338,11 @@ impl Zeta {
let output_excerpt = response.output_excerpt;
log::debug!("completion response: {}", output_excerpt);
let inline_completion = Self::process_completion_response(
Self::process_completion_response(
output_excerpt,
&snapshot,
excerpt_range,
offset,
path,
input_outline,
input_events,
@@ -341,20 +350,7 @@ impl Zeta {
request_sent_at,
&cx,
)
.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)
.await
})
}
@@ -487,8 +483,8 @@ and then another
}
zeta.update(&mut cx, |zeta, _cx| {
zeta.recent_completions.get_mut(2).unwrap().edits = Arc::new([]);
zeta.recent_completions.get_mut(3).unwrap().edits = Arc::new([]);
zeta.shown_completions.get_mut(2).unwrap().edits = Arc::new([]);
zeta.shown_completions.get_mut(3).unwrap().edits = Arc::new([]);
})
.ok();
})
@@ -571,6 +567,7 @@ and then another
output_excerpt: String,
snapshot: &BufferSnapshot,
excerpt_range: Range<usize>,
cursor_offset: usize,
path: Arc<Path>,
input_outline: String,
input_events: String,
@@ -582,9 +579,34 @@ and then another
cx.background_executor().spawn(async move {
let content = output_excerpt.replace(CURSOR_MARKER, "");
let codefence_start = content
.find(EDITABLE_REGION_START_MARKER)
.context("could not find start marker")?;
let start_markers = content
.match_indices(EDITABLE_REGION_START_MARKER)
.collect::<Vec<_>>();
anyhow::ensure!(
start_markers.len() == 1,
"expected exactly one start marker, found {}",
start_markers.len()
);
let end_markers = content
.match_indices(EDITABLE_REGION_END_MARKER)
.collect::<Vec<_>>();
anyhow::ensure!(
end_markers.len() == 1,
"expected exactly one end marker, found {}",
end_markers.len()
);
let sof_markers = content
.match_indices(START_OF_FILE_MARKER)
.collect::<Vec<_>>();
anyhow::ensure!(
sof_markers.len() <= 1,
"expected at most one start-of-file marker, found {}",
sof_markers.len()
);
let codefence_start = start_markers[0].0;
let content = &content[codefence_start..];
let newline_ix = content.find('\n').context("could not find newline")?;
@@ -605,6 +627,7 @@ and then another
id: InlineCompletionId::new(),
path,
excerpt_range,
cursor_offset,
edits: edits.into(),
snapshot: snapshot.clone(),
input_outline: input_outline.into(),
@@ -687,12 +710,13 @@ and then another
self.rated_completions.contains(&completion_id)
}
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 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 rate_completion(
@@ -702,6 +726,7 @@ and then another
feedback: String,
cx: &mut ModelContext<Self>,
) {
self.rated_completions.insert(completion.id);
telemetry::event!(
"Inline Completion Rated",
rating,
@@ -715,12 +740,12 @@ and then another
cx.notify();
}
pub fn recent_completions(&self) -> impl DoubleEndedIterator<Item = &InlineCompletion> {
self.recent_completions.iter()
pub fn shown_completions(&self) -> impl DoubleEndedIterator<Item = &InlineCompletion> {
self.shown_completions.iter()
}
pub fn recent_completions_len(&self) -> usize {
self.recent_completions.len()
pub fn shown_completions_len(&self) -> usize {
self.shown_completions.len()
}
fn report_changes_for_buffer(
@@ -943,7 +968,7 @@ impl CurrentInlineCompletion {
struct PendingCompletion {
id: usize,
_task: Task<Result<()>>,
_task: Task<()>,
}
pub struct ZetaInlineCompletionProvider {
@@ -996,6 +1021,10 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx)
}
fn is_refreshing(&self) -> bool {
!self.pending_completions.is_empty()
}
fn refresh(
&mut self,
buffer: Model<Buffer>,
@@ -1017,13 +1046,16 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
})
});
let mut completion = None;
if let Ok(completion_request) = completion_request {
completion = Some(CurrentInlineCompletion {
buffer_id: buffer.entity_id(),
completion: completion_request.await?,
});
}
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),
};
this.update(&mut cx, |this, cx| {
if this.pending_completions[0].id == pending_completion_id {
@@ -1032,27 +1064,27 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
this.pending_completions.clear();
}
if let Some(new_completion) = completion {
if let Some(new_completion) = completion.context("zeta prediction failed").log_err()
{
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.id)
this.zeta.update(cx, |zeta, cx| {
zeta.completion_shown(&new_completion.completion, cx);
});
this.current_completion = Some(new_completion);
}
} else {
this.zeta.update(cx, |zeta, _cx| {
zeta.completion_shown(new_completion.completion.id)
this.zeta.update(cx, |zeta, cx| {
zeta.completion_shown(&new_completion.completion, cx);
});
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
@@ -1177,6 +1209,7 @@ 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

@@ -170,13 +170,3 @@ 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`