Compare commits

...

42 Commits

Author SHA1 Message Date
Joseph T. Lyons
76db69d7de zed 0.172.11 2025-02-10 15:25:00 -05:00
Max Brunsfeld
7d5aea2d6b Fix panic when outline items have no name (#24574)
Closes #23787

Release Notes:

- Fixed a crash when searching the outline view in certain Ruby files.
2025-02-10 11:20:24 -08:00
Zed Bot
b487d48b65 Bump to 0.172.10 for @maxdeviant 2025-02-07 19:27:25 +00:00
gcp-cherry-pick-bot[bot]
d60d102772 assistant_context_editor: Fix patch block not rendering due to window reborrow (cherry-pick #24461) (#24464)
Cherry-picked assistant_context_editor: Fix patch block not rendering
due to window reborrow (#24461)

This PR fixes an issue where the Assistant patch block was not being
rendered when using "Suggest Edits".

The issue was that the `BlockContext` already has a borrow of the
`Window`, so we can't use `update_in` to reborrow the window.

The fix is to reuse the existing `&mut Window` reference from the
`BlockContext` so we don't need to `update_in`.

Closes #24169.

Release Notes:

- Assistant: Fixed an issue where the patch block was not being rendered
when using "Suggest Edits".

---------

Co-authored-by: Max <max@zed.dev>

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Max <max@zed.dev>
2025-02-07 13:22:27 -05:00
IaVashik
37913f5337 google_ai: Add support for Gemini 2.0 models (#24448)
Add support for the newly released Gemini 2.0 models from Google announced this new family of models earlier this week (2025-02-05).

Release Notes:

- Added support for Google's new Gemini 2.0 models.
2025-02-07 11:19:45 -05:00
Zed Bot
6416231e37 Bump to 0.172.9 for @maxdeviant 2025-02-06 18:16:40 +00:00
gcp-cherry-pick-bot[bot]
af707c512b gpui: Render SVGs at 2x size when rendered in an img (cherry-pick #24332) (#24337)
Cherry-picked gpui: Render SVGs at 2x size when rendered in an `img`
(#24332)

This PR adjusts the rendering of SVGs when used with the `img` element
such that they are rendered at 2x their displayed size.

This results in much crisper icons for icons loaded by icon themes:

<img width="1136" alt="Screenshot 2025-02-05 at 7 39 48 PM"

src="https://github.com/user-attachments/assets/47d1fcee-c54d-4717-8fca-9b9d2bc8da9a"
/>

<img width="1136" alt="Screenshot 2025-02-05 at 7 40 01 PM"

src="https://github.com/user-attachments/assets/3061157c-8c88-41c1-a5dc-83ef9cd341cb"
/>

Release Notes:

- Improved the resolution of icons rendered by icon themes.

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-02-05 20:41:46 -05:00
Cole Miller
43ccb51f54 Cherry-pick char_index_for_point fix to v0.172.x (#24286)
Manual cherry-pick of #23989 

Release Notes:

- Fixed a bug where Zed could crash with certain input sources on macOS

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Louis Brunner <louis.brunner.fr@gmail.com>
Co-authored-by: ben <ben@zed.dev>
2025-02-05 11:56:56 -05:00
gcp-cherry-pick-bot[bot]
3dd6dfb829 Fix the worktree's repository_for_path (cherry-pick #24279) (#24283)
Cherry-picked Fix the worktree's repository_for_path (#24279)

Go back to a less optimized implementation for now since the custom
cursor target seems to have some bugs.

Release Notes:

- Fixed missing git blame and status output in some projects with
multiple git repositories

Co-authored-by: Cole Miller <cole@zed.dev>
2025-02-05 11:15:16 -05:00
gcp-cherry-pick-bot[bot]
3c405afff4 Fix panic when deleting an empty line after a deleted hunk (cherry-pick #24255) (#24280)
Cherry-picked Fix panic when deleting an empty line after a deleted hunk
(#24255)

Release Notes:

- Fix a panic when deleting text after a deletion hunk

Co-authored-by: Cole Miller <cole@zed.dev>
2025-02-05 10:58:08 -05:00
Peter Tripp
b41e3a6b17 v0.172.x stable 2025-02-04 14:36:32 -05:00
Kirill Bulatov
b1f2d8155c Disallow multiple quit confirmations (#24180)
Closes https://github.com/zed-industries/zed/issues/10192 , again.

Release Notes:

- Fixed multiple save modals appearing when app is being closed multiple
times
2025-02-04 11:16:38 +02:00
Michael Sloan
39d77b250e Fix missing modifier changed events on Linux X11 (#24154)
Release Notes:

- Fixed some modifier changed events not being present on Linux X11.
This affected things like the project search palette, where holding ctrl
would not cause the split options to appear.
2025-02-04 03:29:45 -05:00
Mikayla
2126501be8 zed 0.172.8 2025-02-03 22:07:46 -08:00
Nathan Sobo
d4531af8aa Invalidate GPUI views regardless of draw phase (#24164)
We think this could fix issues around view invalidation during focus
handling.

I want to run CI on this and see.

cc @mikayla-maki @maxbrunsfeld 

Release Notes:

- N/A
2025-02-03 22:04:57 -08:00
Zed Bot
4a2b99f7fa Bump to 0.172.7 for @maxbrunsfeld 2025-02-04 00:45:27 +00:00
Max Brunsfeld
623b6a2da6 themes: Make background colors partly transparent by default (#24151)
Certain themes define the `created` and `deleted` status colors, but not
`created_background` and `deleted_background`. Previously, Zed would use
`created` and `deleted` colors, and apply a hard-coded opacity change,
but *not* use `created_background` and `deleted_background`, but that
behavior was inadvertently changed in
https://github.com/zed-industries/zed/pull/22994.

This PR restores the old behavior as a fallback. If a theme defines a
status color, but not the corresponding background color, we'll use a
75% transparent version of the foreground color as a fallback.

Release Notes:

- Fixed an issue in certain themes where diffs would render with the
wrong red and green colors for deletions and insertions.
2025-02-03 15:14:17 -08:00
Zed Bot
092261ac3b Bump to 0.172.6 for @maxdeviant 2025-02-03 21:35:24 +00:00
Marshall Bowers
d7d7d4c46b extensions_ui: Show the filtered icon theme selector when installing an icon theme (#23992)
This PR makes it so when you install an extension with icon themes it
will deploy the icon theme selector filtered down to the newly-installed
icon themes.

This is similar to what we do when installing an extension with themes.

Because we can only have one picker open at a time, when installing an
extension that has _both_ themes and icon themes, the theme selector
will take precedence.

Release Notes:

- N/A
2025-02-03 16:28:22 -05:00
Marshall Bowers
841a71184a Add support for icon themes (#23987)
This PR adds support for icon themes.

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

Here is Zed with Material Icons:

<img width="1136" alt="Screenshot 2025-01-30 at 7 02 06 PM"
src="https://github.com/user-attachments/assets/57d8a0e0-ff38-44d9-8628-af58a60a7c9a"
/>

### Extensions

Extensions can provide icon themes as well as the icons used in those
themes.

Icon themes are defined as JSON files in the `icon_themes` directory,
and icons included in the `icons` directory will be packaged up with the
extension.

All icon paths within an icon theme are interpreted relative to the root
of the extension.

See the [Material Icon
Theme](https://github.com/zed-extensions/material-icon-theme) extension
for an example.

Release Notes:

- Added support for icon themes.
  - Extensions can now provide icon themes.
- Use the `icon theme selector: toggle` action to switch between
installed icon themes.
2025-02-03 16:28:17 -05:00
Marshall Bowers
6ba3b2d340 theme: Properly resolve directory and chevron icons from icon themes (#23984)
This PR fixes an issue where we weren't properly resolving directory and
chevron icons from icon themes the way we were for file icons.

We need to interpret the icon paths as relative to the extension
directory.

Release Notes:

- N/A
2025-02-03 16:28:11 -05:00
Marshall Bowers
86fb7c8630 Add icon theme selector (#23976)
This PR adds an icon theme selector for switching between icon themes:


https://github.com/user-attachments/assets/2cdc7ab7-d9f4-4968-a2e9-724e8ad4ef4d

Release Notes:

- N/A
2025-02-03 16:27:57 -05:00
Peter Tripp
356f90cc1d Switch GitHub Copilot Chat from o1-mini to o3-mini (#24080)
Co-authored-by: SkywardSyntax <87048477+SkywardSyntax@users.noreply.github.com>
2025-02-01 12:49:40 -05:00
Peter Tripp
480fd23b1c zed 0.172.5 2025-02-01 12:09:45 -05:00
Roshan Padaki
a3c9f94d40 assistant: Use GPT 4 tokenizer for o3-mini (#24068)
Sorry to dump an unsolicited PR for a hot feature! I'm sure someone else
was taking a look at this.

I noticed that token counting was disabled and I was getting error logs
of the form `[2025-01-31T22:59:01-05:00 ERROR assistant_context_editor]
No tokenizer found for model o3-mini` when using the new model. To fix
the issue, this PR registers the `gpt-4` tokenizer for this model.

Release Notes:

- openai: Fixed Assistant token counts for `o3-mini` models
2025-02-01 12:09:04 -05:00
Peter Tripp
efb55f46ab zed 0.172.4 2025-01-31 17:31:58 -05:00
Peter Tripp
6a0417670c lmstudio: Support missing quantization in model metadata (#24054)
- Closes https://github.com/zed-industries/zed/issues/23764

Certain models do not include `quantization` parameter from lm studio rest API.
2025-01-31 17:30:58 -05:00
Peter Tripp
0a8882f86b chore: Fix default.json formatting (#24053)
Forgot to run default.json through prettier in #24051. Oops.
2025-01-31 17:30:43 -05:00
Peter Tripp
29d8db76e1 Improve inline_completions.disabled_globs in default.json (#24051)
Make sure that inline completions (Copilot, etc) are disabled for more secret globs (matches `private_files`)
2025-01-31 17:30:40 -05:00
Peter Tripp
9c5c5d4ec5 Add OpenAI o3-mini support (#24044)
Release Notes:

- Add support for OpenAI o3-mini
2025-01-31 15:49:34 -05:00
Zed Bot
f10e44f05c Bump to 0.172.3 for @osiewicz 2025-01-31 10:37:11 +00:00
João Marcos
1be2281ccc Fix data collection permission asked multiple times for same worktree (#24016)
After the user confirmation, only the current instance of the
completions provider had the answer stored.

In this PR, the changes are propagated by having each provider have an
`Entity<choice>`, and having a lookup map with one `Entity<choice>` for
each worktree that `Zeta` has seen.

Release Notes:

- N/A
2025-01-31 11:34:07 +01:00
Piotr Osiewicz
edf69b3923 workspace: Make "New Window" bring app to foreground (#24015)
Closes #ISSUE

Release Notes:

- "New Window" action will now bring App to foreground.
2025-01-31 11:32:52 +01:00
Max Brunsfeld
2d1d5b823e Fix two bugs in new diff hunk handling (#23990)
Closes https://github.com/zed-industries/zed/issues/23981

Release Notes:

- Fixed a crash that could happen when expanding certain diff hunks
- Fixed a bug where diff hunks were not syntax highlighted when
reopening a project with previously-opened buffers.
2025-01-30 17:12:33 -08:00
Zed Bot
08e363cc35 Bump to 0.172.2 for @maxbrunsfeld 2025-01-31 01:10:36 +00:00
Agus Zubiaga
e87ff54699 Fix formatting 2025-01-30 18:08:02 -03:00
Agus Zubiaga
8436dfc15a Fix ttest fake_completion closure 2025-01-30 17:48:28 -03:00
Joseph T. Lyons
2609a70a05 zed 0.172.1 2025-01-30 15:17:00 -05:00
Agus Zubiaga
b0b9e64452 zeta: Onboarding and title bar banner (#23797)
Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Danilo <danilo@zed.dev>
Co-authored-by: João Marcos <joao@zed.dev>
2025-01-30 17:03:10 -03:00
Richard Feldman
0d2e6fdcd9 Revise "Hide/Show Inline Completions" menu (#23808)
> **Note:** https://github.com/zed-industries/zed/pull/23813 should be
merged first!

@nathansobo and I paired on revising this menu, including adding the
"Predict Edits at Cursor" menu item (to make the keyboard shortcut more
discoverable; clicking it makes the inline edits show up, as shown in
the second screenshot) and switching from "Hide/Show" language to
checkboxes.

## Before
<img width="282" alt="Screenshot 2025-01-28 at 4 51 37 PM"
src="https://github.com/user-attachments/assets/309c82c1-8fb5-44db-950e-1a8789a63993"
/>

## After
<img width="1138" alt="Screenshot 2025-01-28 at 4 50 05 PM"
src="https://github.com/user-attachments/assets/302a126c-9389-42a4-bb7d-2896bce859e7"
/>

We also switched to use `SharedString` in more places, where it made
more sense.

@danilo-leal This isn't necessarily *exactly* what we want, but we were
pairing and decided to get it in a state where we can actually try it
out and tweak from here.

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-01-30 16:57:01 -03:00
Joseph T. Lyons
e716eb6d46 Revert "Attempt to suppress embeds in Discord webhook (#23807)" (#23855)
Didn't work.

Release Notes:

- N/A
2025-01-29 15:08:56 -05:00
Joseph T. Lyons
dc808ae2c1 v0.172.x preview 2025-01-29 11:47:22 -05:00
75 changed files with 2054 additions and 502 deletions

View File

@@ -29,8 +29,7 @@ jobs:
maxLength: 2000
truncationSymbol: "..."
- name: Discord Webhook Action
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4 # v6.0.0
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: ${{ steps.get-content.outputs.string }}
flags: 4 # suppress embeds - https://discord.com/developers/docs/resources/message#message-object-message-flags

21
Cargo.lock generated
View File

@@ -4023,7 +4023,7 @@ dependencies = [
"util",
"uuid",
"workspace",
"zed_predict_tos",
"zed_predict_onboarding",
]
[[package]]
@@ -6388,7 +6388,7 @@ dependencies = [
"ui",
"workspace",
"zed_actions",
"zed_predict_tos",
"zed_predict_onboarding",
"zeta",
]
@@ -13412,6 +13412,7 @@ dependencies = [
"windows 0.58.0",
"workspace",
"zed_actions",
"zed_predict_onboarding",
]
[[package]]
@@ -16286,7 +16287,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.172.0"
version = "0.172.11"
dependencies = [
"activity_indicator",
"anyhow",
@@ -16409,7 +16410,7 @@ dependencies = [
"winresource",
"workspace",
"zed_actions",
"zed_predict_tos",
"zed_predict_onboarding",
"zeta",
]
@@ -16524,13 +16525,21 @@ dependencies = [
]
[[package]]
name = "zed_predict_tos"
name = "zed_predict_onboarding"
version = "0.1.0"
dependencies = [
"chrono",
"client",
"db",
"feature_flags",
"fs",
"gpui",
"language",
"menu",
"settings",
"theme",
"ui",
"util",
"workspace",
]
@@ -16731,6 +16740,7 @@ dependencies = [
"collections",
"command_palette_hooks",
"ctor",
"db",
"editor",
"env_logger 0.11.6",
"feature_flags",
@@ -16745,6 +16755,7 @@ dependencies = [
"menu",
"reqwest_client",
"rpc",
"serde",
"serde_json",
"settings",
"similar",

View File

@@ -2,7 +2,6 @@
resolver = "2"
members = [
"crates/activity_indicator",
"crates/zed_predict_tos",
"crates/anthropic",
"crates/assets",
"crates/assistant",
@@ -151,6 +150,7 @@ members = [
"crates/worktree",
"crates/zed",
"crates/zed_actions",
"crates/zed_predict_onboarding",
"crates/zeta",
"crates/git_ui",
@@ -201,7 +201,6 @@ edition = "2021"
activity_indicator = { path = "crates/activity_indicator" }
ai = { path = "crates/ai" }
zed_predict_tos = { path = "crates/zed_predict_tos" }
anthropic = { path = "crates/anthropic" }
assets = { path = "crates/assets" }
assistant = { path = "crates/assistant" }
@@ -349,6 +348,7 @@ workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zed_predict_onboarding = { path = "crates/zed_predict_onboarding" }
zeta = { path = "crates/zeta" }
#

View File

@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 8.9V11C5.34478 11 4.65522 11 3 11V10.4L7 5.6V5H3V7.1" stroke="black" stroke-width="1.5"/>
<path d="M12 5L14 8L12 11" stroke="black" stroke-width="1.5"/>
<path d="M10 6.5L11 8L10 9.5" stroke="black" stroke-width="1.5"/>
<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 334 B

After

Width:  |  Height:  |  Size: 342 B

View File

@@ -0,0 +1,19 @@
<svg width="420" height="128" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="tilePattern" width="22" height="22" patternUnits="userSpaceOnUse">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5L14 8L12 11" stroke="black" stroke-width="1.5"/>
<path d="M10 6.5L11 8L10 9.5" stroke="black" stroke-width="1.5"/>
<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
</svg>
</pattern>
<linearGradient id="fade" y2="1" x2="0">
<stop offset="0" stop-color="white" stop-opacity=".24"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<mask id="fadeMask" maskContentUnits="objectBoundingBox">
<rect width="1" height="1" fill="url(#fade)"/>
</mask>
</defs>
<rect width="100%" height="100%" fill="url(#tilePattern)" mask="url(#fadeMask)"/>
</svg>

After

Width:  |  Height:  |  Size: 971 B

View File

@@ -821,5 +821,12 @@
"shift-end": "terminal::ScrollToBottom",
"ctrl-shift-space": "terminal::ToggleViMode"
}
},
{
"context": "ZedPredictModal",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel"
}
}
]

View File

@@ -881,7 +881,7 @@
}
},
{
"context": "ZedPredictTos",
"context": "ZedPredictModal",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel"

View File

@@ -10,6 +10,7 @@
"light": "One Light",
"dark": "One Dark"
},
"icon_theme": "Zed (Default)",
// The name of a base set of key bindings to use.
// This setting can take four values, each named after another
// text editor:
@@ -775,7 +776,14 @@
"load_direnv": "direct",
"inline_completions": {
// A list of globs representing files that inline completions should be disabled for.
"disabled_globs": [".env"]
"disabled_globs": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/secrets.yml"
]
},
// Settings specific to journaling
"journal": {

View File

@@ -979,12 +979,13 @@ impl ContextEditor {
let render_block: RenderBlock = Arc::new({
let this = this.clone();
let patch_range = range.clone();
move |cx: &mut BlockContext<'_, '_>| {
move |cx: &mut BlockContext| {
let max_width = cx.max_width;
let gutter_width = cx.gutter_dimensions.full_width();
let block_id = cx.block_id;
let selected = cx.selected;
this.update_in(cx, |this, window, cx| {
let window = &mut cx.window;
this.update(cx.app, |this, cx| {
this.render_patch_block(
patch_range.clone(),
max_width,

View File

@@ -121,9 +121,7 @@ pub enum Event {
},
ShowContacts,
ParticipantIndicesChanged,
TermsStatusUpdated {
accepted: bool,
},
PrivateUserInfoUpdated,
}
#[derive(Clone, Copy)]
@@ -227,9 +225,7 @@ impl UserStore {
};
this.set_current_user_accepted_tos_at(accepted_tos_at);
cx.emit(Event::TermsStatusUpdated {
accepted: accepted_tos_at.is_some(),
});
cx.emit(Event::PrivateUserInfoUpdated);
})
} else {
anyhow::Ok(())
@@ -244,6 +240,8 @@ impl UserStore {
Status::SignedOut => {
current_user_tx.send(None).await.ok();
this.update(&mut cx, |this, cx| {
this.accepted_tos_at = None;
cx.emit(Event::PrivateUserInfoUpdated);
cx.notify();
this.clear_contacts()
})?
@@ -714,7 +712,7 @@ impl UserStore {
this.update(&mut cx, |this, cx| {
this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at));
cx.emit(Event::TermsStatusUpdated { accepted: true });
cx.emit(Event::PrivateUserInfoUpdated);
})
} else {
Err(anyhow!("client not found"))

View File

@@ -447,7 +447,7 @@ async fn predict_edits(
));
}
let sample_input_output = claims.is_staff && rand::random::<f32>() < 0.1;
let should_sample = claims.is_staff || params.can_collect_data;
let api_url = state
.config
@@ -541,7 +541,7 @@ async fn predict_edits(
let output = choice.text.clone();
async move {
let properties = if sample_input_output {
let properties = if should_sample {
json!({
"model": model.to_string(),
"headers": response.headers,

View File

@@ -36,8 +36,8 @@ pub enum Model {
Gpt3_5Turbo,
#[serde(alias = "o1", rename = "o1")]
O1,
#[serde(alias = "o1-mini", rename = "o1-mini")]
O1Mini,
#[serde(alias = "o1-mini", rename = "o3-mini")]
O3Mini,
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3.5-sonnet")]
Claude3_5Sonnet,
}
@@ -46,7 +46,7 @@ impl Model {
pub fn uses_streaming(&self) -> bool {
match self {
Self::Gpt4o | Self::Gpt4 | Self::Gpt3_5Turbo | Self::Claude3_5Sonnet => true,
Self::O1Mini | Self::O1 => false,
Self::O3Mini | Self::O1 => false,
}
}
@@ -56,7 +56,7 @@ impl Model {
"gpt-4" => Ok(Self::Gpt4),
"gpt-3.5-turbo" => Ok(Self::Gpt3_5Turbo),
"o1" => Ok(Self::O1),
"o1-mini" => Ok(Self::O1Mini),
"o3-mini" => Ok(Self::O3Mini),
"claude-3-5-sonnet" => Ok(Self::Claude3_5Sonnet),
_ => Err(anyhow!("Invalid model id: {}", id)),
}
@@ -67,7 +67,7 @@ impl Model {
Self::Gpt3_5Turbo => "gpt-3.5-turbo",
Self::Gpt4 => "gpt-4",
Self::Gpt4o => "gpt-4o",
Self::O1Mini => "o1-mini",
Self::O3Mini => "o3-mini",
Self::O1 => "o1",
Self::Claude3_5Sonnet => "claude-3-5-sonnet",
}
@@ -78,7 +78,7 @@ impl Model {
Self::Gpt3_5Turbo => "GPT-3.5",
Self::Gpt4 => "GPT-4",
Self::Gpt4o => "GPT-4o",
Self::O1Mini => "o1-mini",
Self::O3Mini => "o3-mini",
Self::O1 => "o1",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
}
@@ -89,7 +89,7 @@ impl Model {
Self::Gpt4o => 64000,
Self::Gpt4 => 32768,
Self::Gpt3_5Turbo => 12288,
Self::O1Mini => 20000,
Self::O3Mini => 20000,
Self::O1 => 20000,
Self::Claude3_5Sonnet => 200_000,
}

View File

@@ -88,7 +88,7 @@ url.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
zed_predict_tos.workspace = true
zed_predict_onboarding.workspace = true
[dev-dependencies]
ctor.workspace = true

View File

@@ -652,7 +652,7 @@ impl CompletionsMenu {
)
.on_click(cx.listener(move |editor, _event, window, cx| {
cx.stop_propagation();
editor.toggle_zed_predict_tos(window, cx);
editor.toggle_zed_predict_onboarding(window, cx);
})),
),

View File

@@ -1140,12 +1140,7 @@ impl DisplaySnapshot {
}
pub fn line_indent_for_buffer_row(&self, buffer_row: MultiBufferRow) -> LineIndent {
let (buffer, range) = self
.buffer_snapshot
.buffer_line_for_row(buffer_row)
.unwrap();
buffer.line_indent_for_row(range.start.row)
self.buffer_snapshot.line_indent_for_row(buffer_row)
}
pub fn line_len(&self, row: DisplayRow) -> u32 {

View File

@@ -63,13 +63,13 @@ pub use editor_settings::{
CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine, SearchSettings, ShowScrollbar,
};
pub use editor_settings_controls::*;
use element::LineWithInvisibles;
pub use element::{
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
};
use element::{LineWithInvisibles, PositionMap};
use futures::{future, FutureExt};
use fuzzy::StringMatchCandidate;
use zed_predict_tos::ZedPredictTos;
use zed_predict_onboarding::ZedPredictModal;
use code_context_menus::{
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
@@ -723,6 +723,7 @@ pub struct Editor {
>,
>,
last_bounds: Option<Bounds<Pixels>>,
last_position_map: Option<Rc<PositionMap>>,
expect_bounds_change: Option<Bounds<Pixels>>,
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
tasks_update_task: Option<Task<()>>,
@@ -1382,6 +1383,7 @@ impl Editor {
gutter_hovered: false,
pixel_position_of_newest_cursor: None,
last_bounds: None,
last_position_map: None,
expect_bounds_change: None,
gutter_dimensions: GutterDimensions::default(),
style: None,
@@ -3947,12 +3949,21 @@ impl Editor {
self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx)
}
fn toggle_zed_predict_tos(&mut self, window: &mut Window, cx: &mut Context<Self>) {
fn toggle_zed_predict_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let (Some(workspace), Some(project)) = (self.workspace(), self.project.as_ref()) else {
return;
};
ZedPredictTos::toggle(workspace, project.read(cx).user_store().clone(), window, cx);
let project = project.read(cx);
ZedPredictModal::toggle(
workspace,
project.user_store().clone(),
project.client().clone(),
project.fs().clone(),
window,
cx,
);
}
fn do_completion(
@@ -3984,7 +3995,7 @@ impl Editor {
)) => {
drop(entries);
drop(context_menu);
self.toggle_zed_predict_tos(window, cx);
self.toggle_zed_predict_onboarding(window, cx);
return Some(Task::ready(Ok(())));
}
_ => {}
@@ -14086,7 +14097,7 @@ impl Editor {
.and_then(|item| item.to_any().downcast_ref::<T>())
}
fn character_size(&self, window: &mut Window) -> gpui::Point<Pixels> {
fn character_size(&self, window: &mut Window) -> gpui::Size<Pixels> {
let text_layout_details = self.text_layout_details(window);
let style = &text_layout_details.editor_style;
let font_id = window.text_system().resolve_font(&style.text.font());
@@ -14100,7 +14111,7 @@ impl Editor {
.size
.width;
gpui::Point::new(em_width, line_height)
gpui::Size::new(em_width, line_height)
}
}
@@ -15601,9 +15612,9 @@ impl EntityInputHandler for Editor {
cx: &mut Context<Self>,
) -> Option<gpui::Bounds<Pixels>> {
let text_layout_details = self.text_layout_details(window);
let gpui::Point {
x: em_width,
y: line_height,
let gpui::Size {
width: em_width,
height: line_height,
} = self.character_size(window);
let snapshot = self.snapshot(window, cx);
@@ -15621,6 +15632,24 @@ impl EntityInputHandler for Editor {
size: size(em_width, line_height),
})
}
fn character_index_for_point(
&mut self,
point: gpui::Point<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
let position_map = self.last_position_map.as_ref()?;
if !position_map.text_hitbox.contains(&point) {
return None;
}
let display_point = position_map.point_for_position(point).previous_valid;
let anchor = position_map
.snapshot
.display_point_to_anchor(display_point, Bias::Left);
let utf16_offset = anchor.to_offset_utf16(&position_map.snapshot.buffer_snapshot);
Some(utf16_offset.0)
}
}
trait SelectionExt {

View File

@@ -502,7 +502,6 @@ impl EditorElement {
let position_map = layout.position_map.clone();
window.on_key_event({
let editor = self.editor.clone();
let text_hitbox = layout.text_hitbox.clone();
move |event: &ModifiersChangedEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble {
return;
@@ -511,7 +510,7 @@ impl EditorElement {
if editor.hover_state.focused(window, cx) {
return;
}
Self::modifiers_changed(editor, event, &position_map, &text_hitbox, window, cx)
Self::modifiers_changed(editor, event, &position_map, window, cx)
})
}
});
@@ -521,17 +520,16 @@ impl EditorElement {
editor: &mut Editor,
event: &ModifiersChangedEvent,
position_map: &PositionMap,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let mouse_position = window.mouse_position();
if !text_hitbox.is_hovered(window) {
if !position_map.text_hitbox.is_hovered(window) {
return;
}
editor.update_hovered_link(
position_map.point_for_position(text_hitbox.bounds, mouse_position),
position_map.point_for_position(mouse_position),
&position_map.snapshot,
event.modifiers,
window,
@@ -539,14 +537,11 @@ impl EditorElement {
)
}
#[allow(clippy::too_many_arguments)]
fn mouse_left_down(
editor: &mut Editor,
event: &MouseDownEvent,
hovered_hunk: Option<Range<Anchor>>,
position_map: &PositionMap,
text_hitbox: &Hitbox,
gutter_hitbox: &Hitbox,
line_numbers: &HashMap<MultiBufferRow, LineNumberLayout>,
window: &mut Window,
cx: &mut Context<Editor>,
@@ -555,6 +550,8 @@ impl EditorElement {
return;
}
let text_hitbox = &position_map.text_hitbox;
let gutter_hitbox = &position_map.gutter_hitbox;
let mut click_count = event.click_count;
let mut modifiers = event.modifiers;
@@ -611,8 +608,7 @@ impl EditorElement {
}
}
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let point_for_position = position_map.point_for_position(event.position);
let position = point_for_position.previous_valid;
if modifiers.shift && modifiers.alt {
editor.select(
@@ -687,15 +683,13 @@ impl EditorElement {
editor: &mut Editor,
event: &MouseDownEvent,
position_map: &PositionMap,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut Context<Editor>,
) {
if !text_hitbox.is_hovered(window) {
if !position_map.text_hitbox.is_hovered(window) {
return;
}
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let point_for_position = position_map.point_for_position(event.position);
mouse_context_menu::deploy_context_menu(
editor,
Some(event.position),
@@ -710,16 +704,14 @@ impl EditorElement {
editor: &mut Editor,
event: &MouseDownEvent,
position_map: &PositionMap,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut Context<Editor>,
) {
if !text_hitbox.is_hovered(window) || window.default_prevented() {
if !position_map.text_hitbox.is_hovered(window) || window.default_prevented() {
return;
}
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let point_for_position = position_map.point_for_position(event.position);
let position = point_for_position.previous_valid;
editor.select(
@@ -737,10 +729,10 @@ impl EditorElement {
editor: &mut Editor,
event: &MouseUpEvent,
position_map: &PositionMap,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let text_hitbox = &position_map.text_hitbox;
let end_selection = editor.has_pending_selection();
let pending_nonempty_selections = editor.has_pending_nonempty_selection();
@@ -755,7 +747,7 @@ impl EditorElement {
};
if !pending_nonempty_selections && multi_cursor_modifier && text_hitbox.is_hovered(window) {
let point = position_map.point_for_position(text_hitbox.bounds, event.position);
let point = position_map.point_for_position(event.position);
editor.handle_click_hovered_link(point, event.modifiers, window, cx);
cx.stop_propagation();
@@ -771,8 +763,7 @@ impl EditorElement {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if EditorSettings::get_global(cx).middle_click_paste {
if let Some(text) = cx.read_from_primary().and_then(|item| item.text()) {
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let point_for_position = position_map.point_for_position(event.position);
let position = point_for_position.previous_valid;
editor.select(
@@ -795,7 +786,6 @@ impl EditorElement {
editor: &mut Editor,
event: &MouseMoveEvent,
position_map: &PositionMap,
text_bounds: Bounds<Pixels>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
@@ -803,7 +793,8 @@ impl EditorElement {
return;
}
let point_for_position = position_map.point_for_position(text_bounds, event.position);
let text_bounds = position_map.text_hitbox.bounds;
let point_for_position = position_map.point_for_position(event.position);
let mut scroll_delta = gpui::Point::<f32>::default();
let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);
let top = text_bounds.origin.y + vertical_margin;
@@ -855,19 +846,18 @@ impl EditorElement {
editor: &mut Editor,
event: &MouseMoveEvent,
position_map: &PositionMap,
text_hitbox: &Hitbox,
gutter_hitbox: &Hitbox,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let text_hitbox = &position_map.text_hitbox;
let gutter_hitbox = &position_map.gutter_hitbox;
let modifiers = event.modifiers;
let gutter_hovered = gutter_hitbox.is_hovered(window);
editor.set_gutter_hovered(gutter_hovered, cx);
// Don't trigger hover popover if mouse is hovering over context menu
if text_hitbox.is_hovered(window) {
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let point_for_position = position_map.point_for_position(event.position);
editor.update_hovered_link(
point_for_position,
@@ -3789,8 +3779,7 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Vec<AnyElement> {
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, window.mouse_position());
let point_for_position = position_map.point_for_position(window.mouse_position());
let mut controls = vec![];
@@ -3927,7 +3916,10 @@ impl EditorElement {
let scroll_top = layout.position_map.snapshot.scroll_position().y;
let gutter_bg = cx.theme().colors().editor_gutter_background;
window.paint_quad(fill(layout.gutter_hitbox.bounds, gutter_bg));
window.paint_quad(fill(layout.text_hitbox.bounds, self.style.background));
window.paint_quad(fill(
layout.position_map.text_hitbox.bounds,
self.style.background,
));
if let EditorMode::Full = layout.mode {
let mut active_rows = layout.active_rows.iter().peekable();
@@ -3952,8 +3944,8 @@ impl EditorElement {
end: layout.gutter_hitbox.right(),
}),
CurrentLineHighlight::Line => Some(Range {
start: layout.text_hitbox.bounds.left(),
end: layout.text_hitbox.bounds.right(),
start: layout.position_map.text_hitbox.bounds.left(),
end: layout.position_map.text_hitbox.bounds.right(),
}),
CurrentLineHighlight::All => Some(Range {
start: layout.hitbox.left(),
@@ -4027,7 +4019,7 @@ impl EditorElement {
layout.position_map.snapshot.scroll_position().x * layout.position_map.em_width;
for (wrap_position, active) in layout.wrap_guides.iter() {
let x = (layout.text_hitbox.origin.x
let x = (layout.position_map.text_hitbox.origin.x
+ *wrap_position
+ layout.position_map.em_width / 2.)
- scroll_left;
@@ -4039,7 +4031,7 @@ impl EditorElement {
|| scrollbar_y.as_ref().map_or(false, |sy| sy.visible)
};
if x < layout.text_hitbox.origin.x
if x < layout.position_map.text_hitbox.origin.x
|| (show_scrollbars && x > self.scrollbar_left(&layout.hitbox.bounds))
{
continue;
@@ -4052,8 +4044,8 @@ impl EditorElement {
};
window.paint_quad(fill(
Bounds {
origin: point(x, layout.text_hitbox.origin.y),
size: size(px(1.), layout.text_hitbox.size.height),
origin: point(x, layout.position_map.text_hitbox.origin.y),
size: size(px(1.), layout.position_map.text_hitbox.size.height),
},
color,
));
@@ -4428,7 +4420,7 @@ impl EditorElement {
fn paint_text(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
window.with_content_mask(
Some(ContentMask {
bounds: layout.text_hitbox.bounds,
bounds: layout.position_map.text_hitbox.bounds,
}),
|window| {
let cursor_style = if self
@@ -4442,7 +4434,7 @@ impl EditorElement {
} else {
CursorStyle::IBeam
};
window.set_cursor_style(cursor_style, &layout.text_hitbox);
window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
let invisible_display_ranges = self.paint_highlights(layout, window);
self.paint_lines(&invisible_display_ranges, layout, window, cx);
@@ -4464,7 +4456,7 @@ impl EditorElement {
layout: &mut EditorLayout,
window: &mut Window,
) -> SmallVec<[Range<DisplayPoint>; 32]> {
window.paint_layer(layout.text_hitbox.bounds, |window| {
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
let line_end_overshoot = 0.15 * layout.position_map.line_height;
for (range, color) in &layout.highlighted_ranges {
@@ -4543,7 +4535,7 @@ impl EditorElement {
// A softer than perfect black
let redaction_color = gpui::rgb(0x0e1111);
window.paint_layer(layout.text_hitbox.bounds, |window| {
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
for range in layout.redacted_ranges.iter() {
self.paint_highlighted_range(
range.clone(),
@@ -5117,13 +5109,13 @@ impl EditorElement {
.collect(),
};
highlighted_range.paint(layout.text_hitbox.bounds, window);
highlighted_range.paint(layout.position_map.text_hitbox.bounds, window);
}
}
fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
if let Some(mut inline_blame) = layout.inline_blame.take() {
window.paint_layer(layout.text_hitbox.bounds, |window| {
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
inline_blame.paint(window, cx);
})
}
@@ -5242,8 +5234,6 @@ impl EditorElement {
window.on_mouse_event({
let position_map = layout.position_map.clone();
let editor = self.editor.clone();
let text_hitbox = layout.text_hitbox.clone();
let gutter_hitbox = layout.gutter_hitbox.clone();
let multi_buffer_range =
layout
.display_hunks
@@ -5275,32 +5265,16 @@ impl EditorElement {
event,
multi_buffer_range.clone(),
&position_map,
&text_hitbox,
&gutter_hitbox,
line_numbers.as_ref(),
window,
cx,
);
}),
MouseButton::Right => editor.update(cx, |editor, cx| {
Self::mouse_right_down(
editor,
event,
&position_map,
&text_hitbox,
window,
cx,
);
Self::mouse_right_down(editor, event, &position_map, window, cx);
}),
MouseButton::Middle => editor.update(cx, |editor, cx| {
Self::mouse_middle_down(
editor,
event,
&position_map,
&text_hitbox,
window,
cx,
);
Self::mouse_middle_down(editor, event, &position_map, window, cx);
}),
_ => {}
};
@@ -5311,12 +5285,11 @@ impl EditorElement {
window.on_mouse_event({
let editor = self.editor.clone();
let position_map = layout.position_map.clone();
let text_hitbox = layout.text_hitbox.clone();
move |event: &MouseUpEvent, phase, window, cx| {
if phase == DispatchPhase::Bubble {
editor.update(cx, |editor, cx| {
Self::mouse_up(editor, event, &position_map, &text_hitbox, window, cx)
Self::mouse_up(editor, event, &position_map, window, cx)
});
}
}
@@ -5324,8 +5297,6 @@ impl EditorElement {
window.on_mouse_event({
let position_map = layout.position_map.clone();
let editor = self.editor.clone();
let text_hitbox = layout.text_hitbox.clone();
let gutter_hitbox = layout.gutter_hitbox.clone();
move |event: &MouseMoveEvent, phase, window, cx| {
if phase == DispatchPhase::Bubble {
@@ -5336,25 +5307,10 @@ impl EditorElement {
if event.pressed_button == Some(MouseButton::Left)
|| event.pressed_button == Some(MouseButton::Middle)
{
Self::mouse_dragged(
editor,
event,
&position_map,
text_hitbox.bounds,
window,
cx,
)
Self::mouse_dragged(editor, event, &position_map, window, cx)
}
Self::mouse_moved(
editor,
event,
&position_map,
&text_hitbox,
&gutter_hitbox,
window,
cx,
)
Self::mouse_moved(editor, event, &position_map, window, cx)
});
}
}
@@ -7159,6 +7115,12 @@ impl Element for EditorElement {
em_width,
em_advance,
snapshot,
gutter_hitbox: gutter_hitbox.clone(),
text_hitbox: text_hitbox.clone(),
});
self.editor.update(cx, |editor, _| {
editor.last_position_map = Some(position_map.clone())
});
let hunk_controls = self.layout_diff_hunk_controls(
@@ -7182,7 +7144,6 @@ impl Element for EditorElement {
wrap_guides,
indent_guides,
hitbox,
text_hitbox,
gutter_hitbox,
display_hunks,
content_origin,
@@ -7354,7 +7315,6 @@ impl IntoElement for EditorElement {
pub struct EditorLayout {
position_map: Rc<PositionMap>,
hitbox: Hitbox,
text_hitbox: Hitbox,
gutter_hitbox: Hitbox,
content_origin: gpui::Point<Pixels>,
scrollbars_layout: AxisPair<Option<ScrollbarLayout>>,
@@ -7534,15 +7494,17 @@ struct CreaseTrailerLayout {
bounds: Bounds<Pixels>,
}
struct PositionMap {
size: Size<Pixels>,
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
scroll_max: gpui::Point<f32>,
em_width: Pixels,
em_advance: Pixels,
line_layouts: Vec<LineWithInvisibles>,
snapshot: EditorSnapshot,
pub(crate) struct PositionMap {
pub size: Size<Pixels>,
pub line_height: Pixels,
pub scroll_pixel_position: gpui::Point<Pixels>,
pub scroll_max: gpui::Point<f32>,
pub em_width: Pixels,
pub em_advance: Pixels,
pub line_layouts: Vec<LineWithInvisibles>,
pub snapshot: EditorSnapshot,
pub text_hitbox: Hitbox,
pub gutter_hitbox: Hitbox,
}
#[derive(Debug, Copy, Clone)]
@@ -7564,11 +7526,8 @@ impl PointForPosition {
}
impl PositionMap {
fn point_for_position(
&self,
text_bounds: Bounds<Pixels>,
position: gpui::Point<Pixels>,
) -> PointForPosition {
pub(crate) fn point_for_position(&self, position: gpui::Point<Pixels>) -> PointForPosition {
let text_bounds = self.text_hitbox.bounds;
let scroll_position = self.snapshot.scroll_position();
let position = position - text_bounds.origin;
let y = position.y.max(px(0.)).min(self.size.height);

View File

@@ -1040,10 +1040,10 @@ impl SerializableItem for Editor {
} => window.spawn(cx, |mut cx| {
let project = project.clone();
async move {
let language = if let Some(language_name) = language {
let language_registry =
project.update(&mut cx, |project, _| project.languages().clone())?;
let language_registry =
project.update(&mut cx, |project, _| project.languages().clone())?;
let language = if let Some(language_name) = language {
// We don't fail here, because we'd rather not set the language if the name changed
// than fail to restore the buffer.
language_registry
@@ -1061,6 +1061,7 @@ impl SerializableItem for Editor {
// Then set the text so that the dirty bit is set correctly
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language_registry(language_registry);
if let Some(language) = language {
buffer.set_language(Some(language), cx);
}

View File

@@ -229,9 +229,10 @@ pub fn deploy_context_menu(
cx,
),
None => {
let character_size = editor.character_size(window);
let menu_position = MenuPosition::PinnedToEditor {
source: source_anchor,
offset: editor.character_size(window),
offset: gpui::point(character_size.width, character_size.height),
};
Some(MouseContextMenu::new(
menu_position,

View File

@@ -87,8 +87,8 @@ define_connection!(
// mtime_seconds: Option<i64>,
// mtime_nanos: Option<i32>,
// )
pub static ref DB: EditorDb<WorkspaceDb> =
&[sql! (
pub static ref DB: EditorDb<WorkspaceDb> = &[
sql! (
CREATE TABLE editors(
item_id INTEGER NOT NULL,
workspace_id INTEGER NOT NULL,
@@ -134,7 +134,7 @@ define_connection!(
ALTER TABLE editors ADD COLUMN mtime_seconds INTEGER DEFAULT NULL;
ALTER TABLE editors ADD COLUMN mtime_nanos INTEGER DEFAULT NULL;
),
];
];
);
impl EditorDb {

View File

@@ -444,6 +444,23 @@ impl ExtensionStore {
.filter_map(|(name, theme)| theme.extension.as_ref().eq(extension_id).then_some(name))
}
/// Returns the names of icon themes provided by extensions.
pub fn extension_icon_themes<'a>(
&'a self,
extension_id: &'a str,
) -> impl Iterator<Item = &'a Arc<str>> {
self.extension_index
.icon_themes
.iter()
.filter_map(|(name, icon_theme)| {
icon_theme
.extension
.as_ref()
.eq(extension_id)
.then_some(name)
})
}
pub fn fetch_extensions(
&self,
search: Option<&str>,

View File

@@ -295,6 +295,25 @@ impl ExtensionsPage {
);
})
.ok();
return;
}
let icon_themes = extension_store
.extension_icon_themes(extension_id)
.map(|name| name.to_string())
.collect::<Vec<_>>();
if !icon_themes.is_empty() {
workspace
.update(cx, |_workspace, cx| {
window.dispatch_action(
zed_actions::icon_theme_selector::Toggle {
themes_filter: Some(icon_themes),
}
.boxed_clone(),
cx,
);
})
.ok();
}
}

View File

@@ -305,8 +305,14 @@ pub enum Model {
Gemini15Pro,
#[serde(rename = "gemini-1.5-flash")]
Gemini15Flash,
#[serde(rename = "gemini-2.0-flash-exp")]
#[serde(rename = "gemini-2.0-pro-exp")]
Gemini20Pro,
#[serde(rename = "gemini-2.0-flash")]
Gemini20Flash,
#[serde(rename = "gemini-2.0-flash-thinking-exp")]
Gemini20FlashThinking,
#[serde(rename = "gemini-2.0-flash-lite-preview")]
Gemini20FlashLite,
#[serde(rename = "custom")]
Custom {
name: String,
@@ -321,7 +327,10 @@ impl Model {
match self {
Model::Gemini15Pro => "gemini-1.5-pro",
Model::Gemini15Flash => "gemini-1.5-flash",
Model::Gemini20Flash => "gemini-2.0-flash-exp",
Model::Gemini20Pro => "gemini-2.0-pro-exp",
Model::Gemini20Flash => "gemini-2.0-flash",
Model::Gemini20FlashThinking => "gemini-2.0-flash-thinking-exp",
Model::Gemini20FlashLite => "gemini-2.0-flash-lite-preview",
Model::Custom { name, .. } => name,
}
}
@@ -330,7 +339,10 @@ impl Model {
match self {
Model::Gemini15Pro => "Gemini 1.5 Pro",
Model::Gemini15Flash => "Gemini 1.5 Flash",
Model::Gemini20Pro => "Gemini 2.0 Pro",
Model::Gemini20Flash => "Gemini 2.0 Flash",
Model::Gemini20FlashThinking => "Gemini 2.0 Flash Thinking",
Model::Gemini20FlashLite => "Gemini 2.0 Flash Lite",
Self::Custom {
name, display_name, ..
} => display_name.as_ref().unwrap_or(name),
@@ -341,7 +353,10 @@ impl Model {
match self {
Model::Gemini15Pro => 2_000_000,
Model::Gemini15Flash => 1_000_000,
Model::Gemini20Pro => 2_000_000,
Model::Gemini20Flash => 1_000_000,
Model::Gemini20FlashThinking => 1_000_000,
Model::Gemini20FlashLite => 1_000_000,
Model::Custom { max_tokens, .. } => *max_tokens,
}
}

View File

@@ -364,6 +364,20 @@ impl EntityInputHandler for TextInput {
),
))
}
fn character_index_for_point(
&mut self,
point: gpui::Point<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
let line_point = self.last_bounds?.localize(&point)?;
let last_layout = self.last_layout.as_ref()?;
assert_eq!(last_layout.text, self.content);
let utf8_index = last_layout.index_for_x(point.x - line_point.x)?;
Some(self.offset_to_utf16(utf8_index))
}
}
struct TextElement {

View File

@@ -3,6 +3,7 @@ use crate::{
DefiniteLength, Element, ElementId, GlobalElementId, Hitbox, Image, InteractiveElement,
Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task, Window,
SMOOTH_SVG_SCALE_FACTOR,
};
use anyhow::{anyhow, Result};
@@ -610,7 +611,7 @@ impl Asset for ImageAssetLoader {
} else {
let pixmap =
// TODO: Can we make svgs always rescale?
svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(1.0))?;
svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(SMOOTH_SVG_SCALE_FACTOR))?;
let mut buffer =
ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();

View File

@@ -217,6 +217,19 @@ impl Point<Pixels> {
}
}
impl<T> Point<T>
where
T: Sub<T, Output = T> + Debug + Clone + Default,
{
/// Get the position of this point, relative to the given origin
pub fn relative_to(&self, origin: &Point<T>) -> Point<T> {
point(
self.x.clone() - origin.x.clone(),
self.y.clone() - origin.y.clone(),
)
}
}
impl<T, Rhs> Mul<Rhs> for Point<T>
where
T: Mul<Rhs, Output = T> + Clone + Default + Debug,
@@ -376,6 +389,13 @@ pub struct Size<T: Clone + Default + Debug> {
pub height: T,
}
impl<T: Clone + Default + Debug> Size<T> {
/// Create a new Size, a synonym for [`size`]
pub fn new(width: T, height: T) -> Self {
size(width, height)
}
}
/// Constructs a new `Size<T>` with the provided width and height.
///
/// # Arguments
@@ -1456,6 +1476,17 @@ where
}
}
impl<T> Bounds<T>
where
T: Add<T, Output = T> + PartialOrd + Clone + Default + Debug + Sub<T, Output = T>,
{
/// Convert a point to the coordinate space defined by this Bounds
pub fn localize(&self, point: &Point<T>) -> Option<Point<T>> {
self.contains(point)
.then(|| point.relative_to(&self.origin))
}
}
/// Checks if the bounds represent an empty area.
///
/// # Returns

View File

@@ -62,6 +62,14 @@ pub trait EntityInputHandler: 'static + Sized {
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>>;
/// See [`InputHandler::character_index_for_point`] for details
fn character_index_for_point(
&mut self,
point: crate::Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<usize>;
}
/// The canonical implementation of [`PlatformInputHandler`]. Call [`WindowContext::handle_input`]
@@ -159,4 +167,15 @@ impl<V: EntityInputHandler> InputHandler for ElementInputHandler<V> {
view.bounds_for_range(range_utf16, self.element_bounds, window, cx)
})
}
fn character_index_for_point(
&mut self,
point: crate::Point<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Option<usize> {
self.view.update(cx, |view, cx| {
view.character_index_for_point(point, window, cx)
})
}
}

View File

@@ -789,6 +789,14 @@ impl PlatformInputHandler {
cx,
)
}
#[allow(unused)]
pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
self.cx
.update(|window, cx| self.handler.character_index_for_point(point, window, cx))
.ok()
.flatten()
}
}
/// A struct representing a selection in a text buffer, in UTF16 characters.
@@ -879,6 +887,16 @@ pub trait InputHandler: 'static {
cx: &mut App,
) -> Option<Bounds<Pixels>>;
/// Get the character offset for the given point in terms of UTF16 characters
///
/// Corresponds to [characterIndexForPoint:](https://developer.apple.com/documentation/appkit/nstextinputclient/characterindex(for:))
fn character_index_for_point(
&mut self,
point: Point<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Option<usize>;
/// Allows a given input context to opt into getting raw key repeats instead of
/// sending these to the platform.
/// TODO: Ideally we should be able to set ApplePressAndHoldEnabled in NSUserDefaults

View File

@@ -186,6 +186,8 @@ pub struct X11ClientState {
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
pub(crate) xim_handler: Option<XimHandler>,
pub modifiers: Modifiers,
// TODO: Can the other updates to `modifiers` be removed so that this is unnecessary?
pub last_modifiers_changed_event: Modifiers,
pub(crate) compose_state: Option<xkbc::compose::State>,
pub(crate) pre_edit_text: Option<String>,
@@ -434,6 +436,7 @@ impl X11Client {
X11Client(Rc::new(RefCell::new(X11ClientState {
modifiers: Modifiers::default(),
last_modifiers_changed_event: Modifiers::default(),
event_loop: Some(event_loop),
loop_handle: handle,
common,
@@ -867,11 +870,12 @@ impl X11Client {
}
let modifiers = Modifiers::from_xkb(&state.xkb);
if state.modifiers == modifiers {
if state.last_modifiers_changed_event == modifiers {
drop(state);
} else {
let focused_window_id = state.keyboard_focused_window?;
state.modifiers = modifiers;
state.last_modifiers_changed_event = modifiers;
drop(state);
let focused_window = self.get_window(focused_window_id)?;

View File

@@ -17,8 +17,8 @@ use cocoa::{
},
base::{id, nil},
foundation::{
NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSPoint, NSRect,
NSSize, NSString, NSUInteger,
NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSNotFound,
NSPoint, NSRect, NSSize, NSString, NSUInteger,
},
};
use core_graphics::display::{CGDirectDisplayID, CGPoint, CGRect};
@@ -223,6 +223,11 @@ unsafe fn build_classes() {
accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL,
);
decl.add_method(
sel!(characterIndexForPoint:),
character_index_for_point as extern "C" fn(&Object, Sel, NSPoint) -> u64,
);
decl.register()
};
}
@@ -1683,17 +1688,7 @@ extern "C" fn first_rect_for_character_range(
range: NSRange,
_: id,
) -> NSRect {
let frame: NSRect = unsafe {
let state = get_window_state(this);
let lock = state.lock();
let mut frame = NSWindow::frame(lock.native_window);
let content_layout_rect: CGRect = msg_send![lock.native_window, contentLayoutRect];
let style_mask: NSWindowStyleMask = msg_send![lock.native_window, styleMask];
if !style_mask.contains(NSWindowStyleMask::NSFullSizeContentViewWindowMask) {
frame.origin.y -= frame.size.height - content_layout_rect.size.height;
}
frame
};
let frame = get_frame(this);
with_input_handler(this, |input_handler| {
input_handler.bounds_for_range(range.to_range()?)
})
@@ -1714,6 +1709,20 @@ extern "C" fn first_rect_for_character_range(
)
}
fn get_frame(this: &Object) -> NSRect {
unsafe {
let state = get_window_state(this);
let lock = state.lock();
let mut frame = NSWindow::frame(lock.native_window);
let content_layout_rect: CGRect = msg_send![lock.native_window, contentLayoutRect];
let style_mask: NSWindowStyleMask = msg_send![lock.native_window, styleMask];
if !style_mask.contains(NSWindowStyleMask::NSFullSizeContentViewWindowMask) {
frame.origin.y -= frame.size.height - content_layout_rect.size.height;
}
frame
}
}
extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) {
unsafe {
let is_attributed_string: BOOL =
@@ -1827,6 +1836,24 @@ extern "C" fn accepts_first_mouse(this: &Object, _: Sel, _: id) -> BOOL {
YES
}
extern "C" fn character_index_for_point(this: &Object, _: Sel, position: NSPoint) -> u64 {
let position = screen_point_to_gpui_point(this, position);
with_input_handler(this, |input_handler| {
input_handler.character_index_for_point(position)
})
.flatten()
.map(|index| index as u64)
.unwrap_or(NSNotFound as u64)
}
fn screen_point_to_gpui_point(this: &Object, position: NSPoint) -> Point<Pixels> {
let frame = get_frame(this);
let window_x = position.x - frame.origin.x;
let window_y = frame.size.height - (position.y - frame.origin.y);
let position = point(px(window_x as f32), px(window_y as f32));
position
}
extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
let window_state = unsafe { get_window_state(this) };
let position = drag_event_position(&window_state, dragging_info);

View File

@@ -3,6 +3,9 @@ use anyhow::anyhow;
use resvg::tiny_skia::Pixmap;
use std::{hash::Hash, sync::Arc};
/// When rendering SVGs, we render them at twice the size to get a higher-quality result.
pub const SMOOTH_SVG_SCALE_FACTOR: f32 = 2.;
#[derive(Clone, PartialEq, Hash, Eq)]
pub(crate) struct RenderSvgParams {
pub(crate) path: SharedString,

View File

@@ -13,7 +13,7 @@ use crate::{
Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, TransformationMatrix,
Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
SUBPIXEL_VARIANTS,
SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS,
};
use anyhow::{anyhow, Context as _, Result};
use collections::{FxHashMap, FxHashSet};
@@ -107,9 +107,9 @@ impl WindowInvalidator {
pub fn invalidate_view(&self, entity: EntityId, cx: &mut App) -> bool {
let mut inner = self.inner.borrow_mut();
inner.dirty_views.insert(entity);
if inner.draw_phase == DrawPhase::None {
inner.dirty = true;
inner.dirty_views.insert(entity);
cx.push_effect(Effect::Notify { emitter: entity });
true
} else {
@@ -2567,12 +2567,11 @@ impl Window {
let element_opacity = self.element_opacity();
let scale_factor = self.scale_factor();
let bounds = bounds.scale(scale_factor);
// Render the SVG at twice the size to get a higher quality result.
let params = RenderSvgParams {
path,
size: bounds
.size
.map(|pixels| DevicePixels::from((pixels.0 * 2.).ceil() as i32)),
size: bounds.size.map(|pixels| {
DevicePixels::from((pixels.0 * SMOOTH_SVG_SCALE_FACTOR).ceil() as i32)
}),
};
let Some(tile) =

View File

@@ -17,6 +17,31 @@ pub struct InlineCompletion {
pub edits: Vec<(Range<language::Anchor>, String)>,
}
pub enum DataCollectionState {
/// The provider doesn't support data collection.
Unsupported,
/// When there's a file not saved yet. In this case, we can't tell to which project it belongs.
Unknown,
/// Data collection is enabled
Enabled,
/// Data collection is disabled or unanswered.
Disabled,
}
impl DataCollectionState {
pub fn is_supported(&self) -> bool {
!matches!(self, DataCollectionState::Unsupported)
}
pub fn is_unknown(&self) -> bool {
matches!(self, DataCollectionState::Unknown)
}
pub fn is_enabled(&self) -> bool {
matches!(self, DataCollectionState::Enabled)
}
}
pub trait InlineCompletionProvider: 'static + Sized {
fn name() -> &'static str;
fn display_name() -> &'static str;
@@ -25,6 +50,10 @@ pub trait InlineCompletionProvider: 'static + Sized {
fn show_tab_accept_marker() -> bool {
false
}
fn data_collection_state(&self, _cx: &App) -> DataCollectionState {
DataCollectionState::Unsupported
}
fn toggle_data_collection(&mut self, _cx: &mut App) {}
fn is_enabled(
&self,
buffer: &Entity<Buffer>,
@@ -71,6 +100,8 @@ pub trait InlineCompletionProviderHandle {
fn show_completions_in_menu(&self) -> bool;
fn show_completions_in_normal_mode(&self) -> bool;
fn show_tab_accept_marker(&self) -> bool;
fn data_collection_state(&self, cx: &App) -> DataCollectionState;
fn toggle_data_collection(&self, cx: &mut App);
fn needs_terms_acceptance(&self, cx: &App) -> bool;
fn is_refreshing(&self, cx: &App) -> bool;
fn refresh(
@@ -121,6 +152,14 @@ where
T::show_tab_accept_marker()
}
fn data_collection_state(&self, cx: &App) -> DataCollectionState {
self.read(cx).data_collection_state(cx)
}
fn toggle_data_collection(&self, cx: &mut App) {
self.update(cx, |this, cx| this.toggle_data_collection(cx))
}
fn is_enabled(
&self,
buffer: &Entity<Buffer>,

View File

@@ -29,7 +29,7 @@ workspace.workspace = true
zed_actions.workspace = true
zeta.workspace = true
client.workspace = true
zed_predict_tos.workspace = true
zed_predict_onboarding.workspace = true
[dev-dependencies]
copilot = { workspace = true, features = ["test-support"] }

View File

@@ -1,14 +1,15 @@
use anyhow::Result;
use client::UserStore;
use client::{Client, UserStore};
use copilot::{Copilot, Status};
use editor::{scroll::Autoscroll, Editor};
use editor::{actions::ShowInlineCompletion, scroll::Autoscroll, Editor};
use feature_flags::{
FeatureFlagAppExt, PredictEditsFeatureFlag, PredictEditsRateCompletionsFeatureFlag,
};
use fs::Fs;
use gpui::{
actions, div, pulsating_between, Action, Animation, AnimationExt, App, AsyncWindowContext,
Corner, Entity, IntoElement, ParentElement, Render, Subscription, WeakEntity,
Corner, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Subscription,
WeakEntity,
};
use language::{
language_settings::{
@@ -19,18 +20,16 @@ use language::{
use settings::{update_settings_file, Settings, SettingsStore};
use std::{path::Path, sync::Arc, time::Duration};
use supermaven::{AccountStatus, Supermaven};
use ui::{prelude::*, ButtonLike, Color, Icon, IconWithIndicator, Indicator, PopoverMenuHandle};
use ui::{
prelude::*, ButtonLike, Clickable, ContextMenu, ContextMenuEntry, IconButton,
IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip,
};
use workspace::{
create_and_open_local_file,
item::ItemHandle,
notifications::NotificationId,
ui::{
ButtonCommon, Clickable, ContextMenu, IconButton, IconName, IconSize, PopoverMenu, Tooltip,
},
StatusItemView, Toast, Workspace,
create_and_open_local_file, item::ItemHandle, notifications::NotificationId, StatusItemView,
Toast, Workspace,
};
use zed_actions::OpenBrowser;
use zed_predict_tos::ZedPredictTos;
use zed_predict_onboarding::ZedPredictModal;
use zeta::RateCompletionModal;
actions!(zeta, [RateCompletions]);
@@ -43,9 +42,11 @@ struct CopilotErrorToast;
pub struct InlineCompletionButton {
editor_subscription: Option<(Subscription, usize)>,
editor_enabled: Option<bool>,
editor_focus_handle: Option<FocusHandle>,
language: Option<Arc<Language>>,
file: Option<Arc<dyn File>>,
inline_completion_provider: Option<Arc<dyn inline_completion::InlineCompletionProviderHandle>>,
client: Arc<Client>,
fs: Arc<dyn Fs>,
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
@@ -229,14 +230,16 @@ impl Render for InlineCompletionButton {
return div();
}
if !self
.user_store
.read(cx)
.current_user_has_accepted_terms()
.unwrap_or(false)
{
let current_user_terms_accepted =
self.user_store.read(cx).current_user_has_accepted_terms();
if !current_user_terms_accepted.unwrap_or(false) {
let workspace = self.workspace.clone();
let user_store = self.user_store.clone();
let client = self.client.clone();
let fs = self.fs.clone();
let signed_in = current_user_terms_accepted.is_some();
return div().child(
ButtonLike::new("zeta-pending-tos-icon")
@@ -250,20 +253,29 @@ impl Render for InlineCompletionButton {
))
.into_any_element(),
)
.tooltip(|window, cx| {
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Edit Predictions",
None,
"Read Terms of Service",
if signed_in {
"Read Terms of Service"
} else {
"Sign in to use"
},
window,
cx,
)
})
.on_click(cx.listener(move |_, _, window, cx| {
let user_store = user_store.clone();
if let Some(workspace) = workspace.upgrade() {
ZedPredictTos::toggle(workspace, user_store, window, cx);
ZedPredictModal::toggle(
workspace,
user_store.clone(),
client.clone(),
fs.clone(),
window,
cx,
);
}
})),
);
@@ -316,6 +328,7 @@ impl InlineCompletionButton {
workspace: WeakEntity<Workspace>,
fs: Arc<dyn Fs>,
user_store: Entity<UserStore>,
client: Arc<Client>,
popover_menu_handle: PopoverMenuHandle<ContextMenu>,
cx: &mut Context<Self>,
) -> Self {
@@ -329,11 +342,13 @@ impl InlineCompletionButton {
Self {
editor_subscription: None,
editor_enabled: None,
editor_focus_handle: None,
language: None,
file: None,
inline_completion_provider: None,
popover_menu_handle,
workspace,
client,
fs,
user_store,
}
@@ -364,21 +379,26 @@ impl InlineCompletionButton {
})
}
// Predict Edits at Cursor alt-tab
// Automatically Predict:
// ✓ PATH
// ✓ Rust
// ✓ All Files
pub fn build_language_settings_menu(&self, mut menu: ContextMenu, cx: &mut App) -> ContextMenu {
let fs = self.fs.clone();
menu = menu.header("Predict Edits For:");
if let Some(language) = self.language.clone() {
let fs = fs.clone();
let language_enabled =
language_settings::language_settings(Some(language.name()), None, cx)
.show_inline_completions;
menu = menu.entry(
format!(
"{} Inline Completions for {}",
if language_enabled { "Hide" } else { "Show" },
language.name()
),
menu = menu.toggleable_entry(
language.name(),
language_enabled,
IconPosition::Start,
None,
move |_, cx| {
toggle_inline_completions_for_language(language.clone(), fs.clone(), cx)
@@ -387,16 +407,14 @@ impl InlineCompletionButton {
}
let settings = AllLanguageSettings::get_global(cx);
if let Some(file) = &self.file {
let path = file.path().clone();
let path_enabled = settings.inline_completions_enabled_for_path(&path);
menu = menu.entry(
format!(
"{} Inline Completions for This Path",
if path_enabled { "Hide" } else { "Show" }
),
menu = menu.toggleable_entry(
"This File",
path_enabled,
IconPosition::Start,
None,
move |window, cx| {
if let Some(workspace) = window.root().flatten() {
@@ -416,15 +434,48 @@ impl InlineCompletionButton {
}
let globally_enabled = settings.inline_completions_enabled(None, None, cx);
menu.entry(
if globally_enabled {
"Hide Inline Completions for All Files"
} else {
"Show Inline Completions for All Files"
},
menu = menu.toggleable_entry(
"All Files",
globally_enabled,
IconPosition::Start,
None,
move |_, cx| toggle_inline_completions_globally(fs.clone(), cx),
)
);
if let Some(provider) = &self.inline_completion_provider {
let data_collection = provider.data_collection_state(cx);
if data_collection.is_supported() {
let provider = provider.clone();
menu = menu.separator().item(
ContextMenuEntry::new("Data Collection")
.toggleable(IconPosition::Start, data_collection.is_enabled())
.disabled(data_collection.is_unknown())
.handler(move |_, cx| {
provider.toggle_data_collection(cx);
}),
);
}
}
if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
menu = menu
.separator()
.entry(
"Predict Edit at Cursor",
Some(Box::new(ShowInlineCompletion)),
{
let editor_focus_handle = editor_focus_handle.clone();
move |window, cx| {
editor_focus_handle.dispatch_action(&ShowInlineCompletion, window, cx);
}
},
)
.context(editor_focus_handle);
}
menu
}
fn build_copilot_context_menu(
@@ -468,7 +519,7 @@ impl InlineCompletionButton {
self.build_language_settings_menu(menu, cx).when(
cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>(),
|this| {
this.separator().entry(
this.entry(
"Rate Completions",
Some(RateCompletions.boxed_clone()),
move |window, cx| {
@@ -504,6 +555,7 @@ impl InlineCompletionButton {
self.inline_completion_provider = editor.inline_completion_provider();
self.language = language.cloned();
self.file = file;
self.editor_focus_handle = Some(editor.focus_handle(cx));
cx.notify();
}

View File

@@ -146,7 +146,9 @@ impl<T> Outline<T> {
}
} else {
let mut name_ranges = outline_match.name_ranges.iter();
let mut name_range = name_ranges.next().unwrap();
let Some(mut name_range) = name_ranges.next() else {
continue;
};
let mut preceding_ranges_len = 0;
for position in &mut string_match.positions {
while *position >= preceding_ranges_len + name_range.len() {
@@ -194,6 +196,40 @@ impl<T> Outline<T> {
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
#[gpui::test]
async fn test_entries_with_no_names(cx: &mut TestAppContext) {
let outline = Outline::new(vec![
OutlineItem {
depth: 0,
range: Point::new(0, 0)..Point::new(5, 0),
text: "class Foo".to_string(),
highlight_ranges: vec![],
name_ranges: vec![6..9],
body_range: None,
annotation_range: None,
},
OutlineItem {
depth: 0,
range: Point::new(2, 0)..Point::new(2, 7),
text: "private".to_string(),
highlight_ranges: vec![],
name_ranges: vec![],
body_range: None,
annotation_range: None,
},
]);
assert_eq!(
outline
.search(" ", cx.executor())
.await
.into_iter()
.map(|mat| mat.string)
.collect::<Vec<String>>(),
vec!["class Foo".to_string()]
);
}
#[test]
fn test_find_most_similar_with_low_similarity() {

View File

@@ -82,6 +82,7 @@ impl CloudModel {
| open_ai::Model::O1Mini
| open_ai::Model::O1Preview
| open_ai::Model::O1
| open_ai::Model::O3Mini
| open_ai::Model::Custom { .. } => {
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
}
@@ -89,7 +90,10 @@ impl CloudModel {
Self::Google(model) => match model {
google_ai::Model::Gemini15Pro
| google_ai::Model::Gemini15Flash
| google_ai::Model::Gemini20Pro
| google_ai::Model::Gemini20Flash
| google_ai::Model::Gemini20FlashThinking
| google_ai::Model::Gemini20FlashLite
| google_ai::Model::Custom { .. } => {
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
}

View File

@@ -179,7 +179,7 @@ impl LanguageModel for CopilotChatLanguageModel {
CopilotChatModel::Gpt4o => open_ai::Model::FourOmni,
CopilotChatModel::Gpt4 => open_ai::Model::Four,
CopilotChatModel::Gpt3_5Turbo => open_ai::Model::ThreePointFiveTurbo,
CopilotChatModel::O1 | CopilotChatModel::O1Mini => open_ai::Model::Four,
CopilotChatModel::O1 | CopilotChatModel::O3Mini => open_ai::Model::Four,
CopilotChatModel::Claude3_5Sonnet => unreachable!(),
};
count_open_ai_tokens(request, model, cx)

View File

@@ -361,7 +361,10 @@ pub fn count_open_ai_tokens(
.collect::<Vec<_>>();
match model {
open_ai::Model::Custom { .. } | open_ai::Model::O1Mini | open_ai::Model::O1 => {
open_ai::Model::Custom { .. }
| open_ai::Model::O1Mini
| open_ai::Model::O1
| open_ai::Model::O3Mini => {
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages)
}
_ => tiktoken_rs::num_tokens_from_messages(model.id(), &messages),

View File

@@ -192,7 +192,7 @@ pub struct ModelEntry {
pub publisher: String,
pub arch: Option<String>,
pub compatibility_type: CompatibilityType,
pub quantization: String,
pub quantization: Option<String>,
pub state: ModelState,
pub max_context_length: Option<u32>,
pub loaded_context_length: Option<u32>,

View File

@@ -3484,7 +3484,8 @@ impl MultiBufferSnapshot {
let region = cursor.region()?;
let buffer_start = if region.is_main_buffer {
let start_overshoot = range.start.saturating_sub(region.range.start.key);
region.buffer_range.start.key + start_overshoot
(region.buffer_range.start.key + start_overshoot)
.min(region.buffer_range.end.key)
} else {
cursor.main_buffer_position()?.key
};

View File

@@ -78,6 +78,8 @@ pub enum Model {
O1Preview,
#[serde(rename = "o1-mini", alias = "o1-mini")]
O1Mini,
#[serde(rename = "o3-mini", alias = "o3-mini")]
O3Mini,
#[serde(rename = "custom")]
Custom {
@@ -115,6 +117,7 @@ impl Model {
Self::O1 => "o1",
Self::O1Preview => "o1-preview",
Self::O1Mini => "o1-mini",
Self::O3Mini => "o3-mini",
Self::Custom { name, .. } => name,
}
}
@@ -129,6 +132,7 @@ impl Model {
Self::O1 => "o1",
Self::O1Preview => "o1-preview",
Self::O1Mini => "o1-mini",
Self::O3Mini => "o3-mini",
Self::Custom {
name, display_name, ..
} => display_name.as_ref().unwrap_or(name),
@@ -145,6 +149,7 @@ impl Model {
Self::O1 => 200000,
Self::O1Preview => 128000,
Self::O1Mini => 128000,
Self::O3Mini => 200000,
Self::Custom { max_tokens, .. } => *max_tokens,
}
}

View File

@@ -2223,7 +2223,9 @@ impl BufferChangeSet {
let base_text = cx.background_executor().spawn(snapshot).await;
this.update(&mut cx, |this, cx| {
this.base_text = Some(base_text);
cx.notify();
cx.emit(BufferChangeSetEvent::DiffChanged {
changed_range: text::Anchor::MIN..text::Anchor::MAX,
});
})
}));
}

View File

@@ -39,6 +39,9 @@ pub struct PredictEditsParams {
pub outline: Option<String>,
pub input_events: String,
pub input_excerpt: String,
/// Whether the user provided consent for sampling this interaction.
#[serde(default)]
pub can_collect_data: bool,
}
#[derive(Debug, Serialize, Deserialize)]

View File

@@ -1075,6 +1075,15 @@ impl InputHandler for TerminalInputHandler {
fn apple_press_and_hold_enabled(&mut self) -> bool {
false
}
fn character_index_for_point(
&mut self,
_point: Point<Pixels>,
_window: &mut Window,
_cx: &mut App,
) -> Option<usize> {
None
}
}
pub fn is_blank(cell: &IndexedCell) -> bool {

View File

@@ -3,8 +3,9 @@ use std::sync::Arc;
use gpui::{hsla, FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearance};
use crate::{
default_color_scales, AccentColors, Appearance, PlayerColors, StatusColors, SyntaxTheme,
SystemColors, Theme, ThemeColors, ThemeFamily, ThemeStyles,
default_color_scales, AccentColors, Appearance, PlayerColors, StatusColors,
StatusColorsRefinement, SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeFamily,
ThemeStyles,
};
/// The default theme family for Zed.
@@ -21,6 +22,26 @@ pub fn zed_default_themes() -> ThemeFamily {
}
}
// If a theme customizes a foreground version of a status color, but does not
// customize the background color, then use a partly-transparent version of the
// foreground color for the background color.
pub(crate) fn apply_status_color_defaults(status: &mut StatusColorsRefinement) {
for (fg_color, bg_color) in [
(&status.deleted, &mut status.deleted_background),
(&status.created, &mut status.created_background),
(&status.modified, &mut status.modified_background),
(&status.conflict, &mut status.conflict_background),
(&status.error, &mut status.error_background),
(&status.hidden, &mut status.hidden_background),
] {
if bg_color.is_none() {
if let Some(fg_color) = fg_color {
*bg_color = Some(fg_color.opacity(0.25));
}
}
}
}
pub(crate) fn zed_default_dark() -> Theme {
let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.);
let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.);

View File

@@ -212,6 +212,19 @@ impl ThemeRegistry {
self.get_icon_theme(DEFAULT_ICON_THEME_NAME)
}
/// Returns the metadata of all icon themes in the registry.
pub fn list_icon_themes(&self) -> Vec<ThemeMeta> {
self.state
.read()
.icon_themes
.values()
.map(|theme| ThemeMeta {
name: theme.name.clone(),
appearance: theme.appearance,
})
.collect()
}
/// Returns the icon theme with the specified name.
pub fn get_icon_theme(&self, name: &str) -> Result<Arc<IconTheme>> {
self.state
@@ -242,6 +255,14 @@ impl ThemeRegistry {
) -> Result<()> {
let icon_theme_family = read_icon_theme(icon_theme_path, fs).await?;
let resolve_icon_path = |path: SharedString| {
icons_root_dir
.join(path.as_ref())
.to_string_lossy()
.to_string()
.into()
};
let mut state = self.state.write();
for icon_theme in icon_theme_family.themes {
let icon_theme = IconTheme {
@@ -252,23 +273,21 @@ impl ThemeRegistry {
AppearanceContent::Dark => Appearance::Dark,
},
directory_icons: DirectoryIcons {
collapsed: icon_theme.directory_icons.collapsed,
expanded: icon_theme.directory_icons.expanded,
collapsed: icon_theme.directory_icons.collapsed.map(resolve_icon_path),
expanded: icon_theme.directory_icons.expanded.map(resolve_icon_path),
},
chevron_icons: ChevronIcons {
collapsed: icon_theme.chevron_icons.collapsed,
expanded: icon_theme.chevron_icons.expanded,
collapsed: icon_theme.chevron_icons.collapsed.map(resolve_icon_path),
expanded: icon_theme.chevron_icons.expanded.map(resolve_icon_path),
},
file_icons: icon_theme
.file_icons
.into_iter()
.map(|(key, icon)| {
let path = icons_root_dir.join(icon.path.as_ref());
(
key,
IconDefinition {
path: path.to_string_lossy().to_string().into(),
path: resolve_icon_path(icon.path),
},
)
})

View File

@@ -319,9 +319,6 @@ pub struct ThemeSettingsContent {
#[serde(default)]
pub theme: Option<ThemeSelection>,
/// The name of the icon theme to use.
///
/// Currently not exposed to the user.
#[serde(skip)]
#[serde(default)]
pub icon_theme: Option<String>,

View File

@@ -102,10 +102,10 @@ impl StatusColors {
conflict_background: red().dark().step_9(),
conflict_border: red().dark().step_9(),
created: grass().dark().step_9(),
created_background: grass().dark().step_9(),
created_background: grass().dark().step_9().opacity(0.25),
created_border: grass().dark().step_9(),
deleted: red().dark().step_9(),
deleted_background: red().dark().step_9(),
deleted_background: red().dark().step_9().opacity(0.25),
deleted_border: red().dark().step_9(),
error: red().dark().step_9(),
error_background: red().dark().step_9(),
@@ -123,7 +123,7 @@ impl StatusColors {
info_background: blue().dark().step_9(),
info_border: blue().dark().step_9(),
modified: yellow().dark().step_9(),
modified_background: yellow().dark().step_9(),
modified_background: yellow().dark().step_9().opacity(0.25),
modified_border: yellow().dark().step_9(),
predictive: neutral().dark_alpha().step_9(),
predictive_background: neutral().dark_alpha().step_9(),

View File

@@ -24,6 +24,7 @@ use std::sync::Arc;
use ::settings::Settings;
use anyhow::Result;
use fallback_themes::apply_status_color_defaults;
use fs::Fs;
use gpui::{
px, App, AssetSource, HighlightStyle, Hsla, Pixels, Refineable, SharedString, WindowAppearance,
@@ -155,7 +156,9 @@ impl ThemeFamily {
AppearanceContent::Light => StatusColors::light(),
AppearanceContent::Dark => StatusColors::dark(),
};
refined_status_colors.refine(&theme.style.status_colors_refinement());
let mut status_colors_refinement = theme.style.status_colors_refinement();
apply_status_color_defaults(&mut status_colors_refinement);
refined_status_colors.refine(&status_colors_refinement);
let mut refined_player_colors = match theme.appearance {
AppearanceContent::Light => PlayerColors::light(),

View File

@@ -0,0 +1,274 @@
use fs::Fs;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, UpdateGlobal, WeakEntity,
Window,
};
use picker::{Picker, PickerDelegate};
use settings::{update_settings_file, Settings as _, SettingsStore};
use std::sync::Arc;
use theme::{IconTheme, ThemeMeta, ThemeRegistry, ThemeSettings};
use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::{ui::HighlightedLabel, ModalView};
pub(crate) struct IconThemeSelector {
picker: Entity<Picker<IconThemeSelectorDelegate>>,
}
impl EventEmitter<DismissEvent> for IconThemeSelector {}
impl Focusable for IconThemeSelector {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.picker.focus_handle(cx)
}
}
impl ModalView for IconThemeSelector {}
impl IconThemeSelector {
pub fn new(
delegate: IconThemeSelectorDelegate,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl Render for IconThemeSelector {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
}
}
pub(crate) struct IconThemeSelectorDelegate {
fs: Arc<dyn Fs>,
themes: Vec<ThemeMeta>,
matches: Vec<StringMatch>,
original_theme: Arc<IconTheme>,
selection_completed: bool,
selected_index: usize,
selector: WeakEntity<IconThemeSelector>,
}
impl IconThemeSelectorDelegate {
pub fn new(
selector: WeakEntity<IconThemeSelector>,
fs: Arc<dyn Fs>,
themes_filter: Option<&Vec<String>>,
cx: &mut Context<IconThemeSelector>,
) -> Self {
let theme_settings = ThemeSettings::get_global(cx);
let original_theme = theme_settings.active_icon_theme.clone();
let registry = ThemeRegistry::global(cx);
let mut themes = registry
.list_icon_themes()
.into_iter()
.filter(|meta| {
if let Some(theme_filter) = themes_filter {
theme_filter.contains(&meta.name.to_string())
} else {
true
}
})
.collect::<Vec<_>>();
themes.sort_unstable_by(|a, b| {
a.appearance
.is_light()
.cmp(&b.appearance.is_light())
.then(a.name.cmp(&b.name))
});
let matches = themes
.iter()
.map(|meta| StringMatch {
candidate_id: 0,
score: 0.0,
positions: Default::default(),
string: meta.name.to_string(),
})
.collect();
let mut this = Self {
fs,
themes,
matches,
original_theme: original_theme.clone(),
selected_index: 0,
selection_completed: false,
selector,
};
this.select_if_matching(&original_theme.name);
this
}
fn show_selected_theme(&mut self, cx: &mut Context<Picker<IconThemeSelectorDelegate>>) {
if let Some(mat) = self.matches.get(self.selected_index) {
let registry = ThemeRegistry::global(cx);
match registry.get_icon_theme(&mat.string) {
Ok(theme) => {
Self::set_icon_theme(theme, cx);
}
Err(err) => {
log::error!("error loading icon theme {}: {err}", mat.string);
}
}
}
}
fn select_if_matching(&mut self, theme_name: &str) {
self.selected_index = self
.matches
.iter()
.position(|mat| mat.string == theme_name)
.unwrap_or(self.selected_index);
}
fn set_icon_theme(theme: Arc<IconTheme>, cx: &mut App) {
SettingsStore::update_global(cx, |store, cx| {
let mut theme_settings = store.get::<ThemeSettings>(None).clone();
theme_settings.active_icon_theme = theme;
store.override_global(theme_settings);
cx.refresh_windows();
});
}
}
impl PickerDelegate for IconThemeSelectorDelegate {
type ListItem = ui::ListItem;
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Select Icon Theme...".into()
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn confirm(
&mut self,
_: bool,
_window: &mut Window,
cx: &mut Context<Picker<IconThemeSelectorDelegate>>,
) {
self.selection_completed = true;
let theme_settings = ThemeSettings::get_global(cx);
let theme_name = theme_settings.active_icon_theme.name.clone();
telemetry::event!(
"Settings Changed",
setting = "icon_theme",
value = theme_name
);
update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
settings.icon_theme = Some(theme_name.to_string());
});
self.selector
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<IconThemeSelectorDelegate>>) {
if !self.selection_completed {
Self::set_icon_theme(self.original_theme.clone(), cx);
self.selection_completed = true;
}
self.selector
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_: &mut Window,
cx: &mut Context<Picker<IconThemeSelectorDelegate>>,
) {
self.selected_index = ix;
self.show_selected_theme(cx);
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<IconThemeSelectorDelegate>>,
) -> gpui::Task<()> {
let background = cx.background_executor().clone();
let candidates = self
.themes
.iter()
.enumerate()
.map(|(id, meta)| StringMatchCandidate::new(id, &meta.name))
.collect::<Vec<_>>();
cx.spawn_in(window, |this, mut cx| async move {
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
background,
)
.await
};
this.update(&mut cx, |this, cx| {
this.delegate.matches = matches;
this.delegate.selected_index = this
.delegate
.selected_index
.min(this.delegate.matches.len().saturating_sub(1));
this.delegate.show_selected_theme(cx);
})
.log_err();
})
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let theme_match = &self.matches[ix];
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(HighlightedLabel::new(
theme_match.string.clone(),
theme_match.positions.clone(),
)),
)
}
}

View File

@@ -1,3 +1,5 @@
mod icon_theme_selector;
use fs::Fs;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
@@ -11,22 +13,25 @@ use theme::{Appearance, Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::{ui::HighlightedLabel, ModalView, Workspace};
use zed_actions::theme_selector::Toggle;
use crate::icon_theme_selector::{IconThemeSelector, IconThemeSelectorDelegate};
actions!(theme_selector, [Reload]);
pub fn init(cx: &mut App) {
cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
workspace.register_action(toggle);
workspace
.register_action(toggle_theme_selector)
.register_action(toggle_icon_theme_selector);
},
)
.detach();
}
pub fn toggle(
fn toggle_theme_selector(
workspace: &mut Workspace,
toggle: &Toggle,
toggle: &zed_actions::theme_selector::Toggle,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
@@ -42,9 +47,27 @@ pub fn toggle(
});
}
fn toggle_icon_theme_selector(
workspace: &mut Workspace,
toggle: &zed_actions::icon_theme_selector::Toggle,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let fs = workspace.app_state().fs.clone();
workspace.toggle_modal(window, cx, |window, cx| {
let delegate = IconThemeSelectorDelegate::new(
cx.entity().downgrade(),
fs,
toggle.themes_filter.as_ref(),
cx,
);
IconThemeSelector::new(delegate, window, cx)
});
}
impl ModalView for ThemeSelector {}
pub struct ThemeSelector {
struct ThemeSelector {
picker: Entity<Picker<ThemeSelectorDelegate>>,
}
@@ -73,7 +96,7 @@ impl ThemeSelector {
}
}
pub struct ThemeSelectorDelegate {
struct ThemeSelectorDelegate {
fs: Arc<dyn Fs>,
themes: Vec<ThemeMeta>,
matches: Vec<StringMatch>,

View File

@@ -48,6 +48,7 @@ telemetry.workspace = true
workspace.workspace = true
zed_actions.workspace = true
git_ui.workspace = true
zed_predict_onboarding.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View File

@@ -37,6 +37,7 @@ use ui::{
use util::ResultExt;
use workspace::{notifications::NotifyResultExt, Workspace};
use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
use zed_predict_onboarding::ZedPredictBanner;
#[cfg(feature = "stories")]
pub use stories::*;
@@ -113,6 +114,7 @@ pub struct TitleBar {
application_menu: Option<Entity<ApplicationMenu>>,
_subscriptions: Vec<Subscription>,
git_ui_enabled: Arc<AtomicBool>,
zed_predict_banner: Entity<ZedPredictBanner>,
}
impl Render for TitleBar {
@@ -196,6 +198,7 @@ impl Render for TitleBar {
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()),
)
.child(self.render_collaborator_list(window, cx))
.child(self.zed_predict_banner.clone())
.child(
h_flex()
.gap_1()
@@ -271,6 +274,7 @@ impl TitleBar {
let project = workspace.project().clone();
let user_store = workspace.app_state().user_store.clone();
let client = workspace.app_state().client.clone();
let fs = workspace.app_state().fs.clone();
let active_call = ActiveCall::global(cx);
let platform_style = PlatformStyle::platform();
@@ -306,6 +310,16 @@ impl TitleBar {
}
}));
let zed_predict_banner = cx.new(|cx| {
ZedPredictBanner::new(
workspace.weak_handle(),
user_store.clone(),
client.clone(),
fs.clone(),
cx,
)
});
Self {
platform_style,
content: div().id(id.into()),
@@ -319,6 +333,7 @@ impl TitleBar {
client,
_subscriptions: subscriptions,
git_ui_enabled: is_git_ui_enabled,
zed_predict_banner,
}
}

View File

@@ -64,6 +64,11 @@ impl ContextMenuEntry {
}
}
pub fn toggleable(mut self, toggle_position: IconPosition, toggled: bool) -> Self {
self.toggle = Some((toggle_position, toggled));
self
}
pub fn icon(mut self, icon: IconName) -> Self {
self.icon = Some(icon);
self

View File

@@ -49,7 +49,7 @@ impl RenderOnce for ListSubHeader {
.px(DynamicSpacing::Base02.rems(cx))
.child(
div()
.h_6()
.h_5()
.when(self.inset, |this| this.px_2())
.when(self.selected, |this| {
this.bg(cx.theme().colors().ghost_element_selected)
@@ -70,7 +70,11 @@ impl RenderOnce for ListSubHeader {
Icon::new(i).color(Color::Muted).size(IconSize::Small)
}),
)
.child(Label::new(self.label.clone()).color(Color::Muted)),
.child(
Label::new(self.label.clone())
.color(Color::Muted)
.size(LabelSize::Small),
),
),
)
}

View File

@@ -379,6 +379,12 @@ pub mod simple_message_notification {
click_message: Option<SharedString>,
secondary_click_message: Option<SharedString>,
secondary_on_click: Option<Arc<dyn Fn(&mut Window, &mut Context<Self>)>>,
tertiary_click_message: Option<SharedString>,
tertiary_on_click: Option<Arc<dyn Fn(&mut Window, &mut Context<Self>)>>,
more_info_message: Option<SharedString>,
more_info_url: Option<Arc<str>>,
show_close_button: bool,
title: Option<SharedString>,
}
impl EventEmitter<DismissEvent> for MessageNotification {}
@@ -402,6 +408,12 @@ pub mod simple_message_notification {
click_message: None,
secondary_on_click: None,
secondary_click_message: None,
tertiary_on_click: None,
tertiary_click_message: None,
more_info_message: None,
more_info_url: None,
show_close_button: true,
title: None,
}
}
@@ -437,31 +449,85 @@ pub mod simple_message_notification {
self
}
pub fn with_tertiary_click_message<S>(mut self, message: S) -> Self
where
S: Into<SharedString>,
{
self.tertiary_click_message = Some(message.into());
self
}
pub fn on_tertiary_click<F>(mut self, on_click: F) -> Self
where
F: 'static + Fn(&mut Window, &mut Context<Self>),
{
self.tertiary_on_click = Some(Arc::new(on_click));
self
}
pub fn more_info_message<S>(mut self, message: S) -> Self
where
S: Into<SharedString>,
{
self.more_info_message = Some(message.into());
self
}
pub fn more_info_url<S>(mut self, url: S) -> Self
where
S: Into<Arc<str>>,
{
self.more_info_url = Some(url.into());
self
}
pub fn dismiss(&mut self, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
pub fn show_close_button(mut self, show: bool) -> Self {
self.show_close_button = show;
self
}
pub fn with_title<S>(mut self, title: S) -> Self
where
S: Into<SharedString>,
{
self.title = Some(title.into());
self
}
}
impl Render for MessageNotification {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.p_3()
.gap_2()
.gap_3()
.elevation_3(cx)
.child(
h_flex()
.gap_4()
.justify_between()
.items_start()
.child(div().max_w_96().child((self.build_content)(window, cx)))
.child(
IconButton::new("close", IconName::Close)
.on_click(cx.listener(|this, _, _, cx| this.dismiss(cx))),
),
v_flex()
.gap_0p5()
.when_some(self.title.clone(), |element, title| {
element.child(Label::new(title))
})
.child(div().max_w_96().child((self.build_content)(window, cx))),
)
.when(self.show_close_button, |this| {
this.child(
IconButton::new("close", IconName::Close)
.on_click(cx.listener(|this, _, _, cx| this.dismiss(cx))),
)
}),
)
.child(
h_flex()
.gap_2()
.gap_1()
.children(self.click_message.iter().map(|message| {
Button::new(message.clone(), message.clone())
.label_size(LabelSize::Small)
@@ -489,7 +555,40 @@ pub mod simple_message_notification {
};
this.dismiss(cx)
}))
})),
}))
.child(
h_flex()
.w_full()
.gap_1()
.justify_end()
.children(self.tertiary_click_message.iter().map(|message| {
Button::new(message.clone(), message.clone())
.label_size(LabelSize::Small)
.on_click(cx.listener(|this, _, window, cx| {
if let Some(on_click) = this.tertiary_on_click.as_ref()
{
(on_click)(window, cx)
};
this.dismiss(cx)
}))
}))
.children(
self.more_info_message
.iter()
.zip(self.more_info_url.iter())
.map(|(message, url)| {
let url = url.clone();
Button::new(message.clone(), message.clone())
.label_size(LabelSize::Small)
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::Indicator)
.icon_color(Color::Muted)
.on_click(cx.listener(move |_, _, _, cx| {
cx.open_url(&url);
}))
}),
),
),
)
}
}

View File

@@ -58,7 +58,9 @@ use persistence::{
SerializedWindowBounds, DB,
};
use postage::stream::Stream;
use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree};
use project::{
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
};
use remote::{ssh_session::ConnectionIdentifier, SshClientDelegate, SshConnectionOptions};
use schemars::JsonSchema;
use serde::Deserialize;
@@ -1282,7 +1284,10 @@ impl Workspace {
.unwrap_or_default();
window
.update(&mut cx, |_, window, _| window.activate_window())
.update(&mut cx, |_, window, cx| {
window.activate_window();
cx.activate(true);
})
.log_err();
Ok((window, opened_items))
})
@@ -2200,6 +2205,18 @@ impl Workspace {
}
}
pub fn absolute_path_of_worktree(
&self,
worktree_id: WorktreeId,
cx: &mut Context<Self>,
) -> Option<PathBuf> {
self.project
.read(cx)
.worktree_for_id(worktree_id, cx)
// TODO: use `abs_path` or `root_dir`
.map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
}
fn add_folder_to_project(
&mut self,
_: &AddFolderToProject,

View File

@@ -2670,21 +2670,10 @@ impl Snapshot {
/// Get the repository whose work directory contains the given path.
pub fn repository_for_path(&self, path: &Path) -> Option<&RepositoryEntry> {
let mut cursor = self.repositories.cursor::<PathProgress>(&());
let mut repository = None;
// Git repositories may contain other git repositories. As a side effect of
// lexicographic sorting by path, deeper repositories will be after higher repositories
// So, let's loop through every matching repository until we can't find any more to find
// the deepest repository that could contain this path.
while cursor.seek_forward(&PathTarget::Contains(path), Bias::Left, &())
&& cursor.item().is_some()
{
repository = cursor.item();
cursor.next(&());
}
repository
self.repositories
.iter()
.filter(|repo| repo.work_directory.directory_contains(path))
.last()
}
/// Given an ordered iterator of entries, returns an iterator of those entries,
@@ -2756,6 +2745,8 @@ impl Snapshot {
self.entry_for_path("")
}
/// TODO: what's the difference between `root_dir` and `abs_path`?
/// is there any? if so, document it.
pub fn root_dir(&self) -> Option<Arc<Path>> {
self.root_entry()
.filter(|entry| entry.is_dir())
@@ -5974,7 +5965,6 @@ impl<'a> Iterator for Traversal<'a> {
enum PathTarget<'a> {
Path(&'a Path),
Successor(&'a Path),
Contains(&'a Path),
}
impl<'a> PathTarget<'a> {
@@ -5988,13 +5978,6 @@ impl<'a> PathTarget<'a> {
Ordering::Equal
}
}
PathTarget::Contains(path) => {
if path.starts_with(other) {
Ordering::Equal
} else {
Ordering::Greater
}
}
}
}
}

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.172.0"
version = "0.172.11"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]
@@ -16,7 +16,7 @@ path = "src/main.rs"
[dependencies]
activity_indicator.workspace = true
zed_predict_tos.workspace = true
zed_predict_onboarding.workspace = true
anyhow.workspace = true
assets.workspace = true
assistant.workspace = true

View File

@@ -1 +1 @@
dev
stable

View File

@@ -434,6 +434,7 @@ fn main() {
inline_completion_registry::init(
app_state.client.clone(),
app_state.user_store.clone(),
app_state.fs.clone(),
cx,
);
let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx);

View File

@@ -45,6 +45,7 @@ use settings::{
};
use std::any::TypeId;
use std::path::PathBuf;
use std::sync::atomic::{self, AtomicBool};
use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
use terminal_view::terminal_panel::{self, TerminalPanel};
use theme::{ActiveTheme, ThemeSettings};
@@ -176,6 +177,7 @@ pub fn initialize_workspace(
workspace.weak_handle(),
app_state.fs.clone(),
app_state.user_store.clone(),
app_state.client.clone(),
popover_menu_handle.clone(),
cx,
)
@@ -964,7 +966,12 @@ fn install_cli(
.detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None);
}
static WAITING_QUIT_CONFIRMATION: AtomicBool = AtomicBool::new(false);
fn quit(_: &Quit, cx: &mut App) {
if WAITING_QUIT_CONFIRMATION.load(atomic::Ordering::Acquire) {
return;
}
let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
cx.spawn(|mut cx| async move {
let mut workspace_windows = cx.update(|cx| {
@@ -981,23 +988,27 @@ fn quit(_: &Quit, cx: &mut App) {
})
.log_err();
if let (true, Some(workspace)) = (should_confirm, workspace_windows.first().copied()) {
let answer = workspace
.update(&mut cx, |_, window, cx| {
window.prompt(
PromptLevel::Info,
"Are you sure you want to quit?",
None,
&["Quit", "Cancel"],
cx,
)
})
.log_err();
if should_confirm {
if let Some(workspace) = workspace_windows.first() {
let answer = workspace
.update(&mut cx, |_, window, cx| {
window.prompt(
PromptLevel::Info,
"Are you sure you want to quit?",
None,
&["Quit", "Cancel"],
cx,
)
})
.log_err();
if let Some(answer) = answer {
let answer = answer.await.ok();
if answer != Some(0) {
return Ok(());
if let Some(answer) = answer {
WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release);
let answer = answer.await.ok();
WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release);
if answer != Some(0) {
return Ok(());
}
}
}
}

View File

@@ -5,13 +5,17 @@ use collections::HashMap;
use copilot::{Copilot, CopilotCompletionProvider};
use editor::{Editor, EditorMode};
use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag};
use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity, Window};
use fs::Fs;
use gpui::{AnyWindowHandle, App, AppContext, Context, Entity, WeakEntity};
use language::language_settings::{all_language_settings, InlineCompletionProvider};
use settings::SettingsStore;
use supermaven::{Supermaven, SupermavenCompletionProvider};
use zed_predict_tos::ZedPredictTos;
use ui::Window;
use workspace::Workspace;
use zed_predict_onboarding::ZedPredictModal;
use zeta::ProviderDataCollection;
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, fs: Arc<dyn Fs>, cx: &mut App) {
let editors: Rc<RefCell<HashMap<WeakEntity<Editor>, AnyWindowHandle>>> = Rc::default();
cx.observe_new({
let editors = editors.clone();
@@ -37,6 +41,7 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
}
})
.detach();
editors
.borrow_mut()
.insert(editor_handle, window.window_handle());
@@ -91,6 +96,7 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
let editors = editors.clone();
let client = client.clone();
let user_store = user_store.clone();
let fs = fs.clone();
move |cx| {
let new_provider = all_language_settings(None, cx).inline_completions.provider;
if new_provider != provider {
@@ -123,9 +129,11 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
window
.update(cx, |_, window, cx| {
ZedPredictTos::toggle(
ZedPredictModal::toggle(
workspace,
user_store.clone(),
client.clone(),
fs.clone(),
window,
cx,
);
@@ -214,17 +222,19 @@ fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut Context<Ed
fn assign_inline_completion_provider(
editor: &mut Editor,
provider: language::language_settings::InlineCompletionProvider,
provider: InlineCompletionProvider,
client: &Arc<Client>,
user_store: Entity<UserStore>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let singleton_buffer = editor.buffer().read(cx).as_singleton();
match provider {
language::language_settings::InlineCompletionProvider::None => {}
language::language_settings::InlineCompletionProvider::Copilot => {
InlineCompletionProvider::None => {}
InlineCompletionProvider::Copilot => {
if let Some(copilot) = Copilot::global(cx) {
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
if let Some(buffer) = singleton_buffer {
if buffer.read(cx).file().is_some() {
copilot.update(cx, |copilot, cx| {
copilot.register_buffer(&buffer, cx);
@@ -235,26 +245,35 @@ fn assign_inline_completion_provider(
editor.set_inline_completion_provider(Some(provider), window, cx);
}
}
language::language_settings::InlineCompletionProvider::Supermaven => {
InlineCompletionProvider::Supermaven => {
if let Some(supermaven) = Supermaven::global(cx) {
let provider = cx.new(|_| SupermavenCompletionProvider::new(supermaven));
editor.set_inline_completion_provider(Some(provider), window, cx);
}
}
language::language_settings::InlineCompletionProvider::Zed => {
InlineCompletionProvider::Zed => {
if cx.has_flag::<PredictEditsFeatureFlag>()
|| (cfg!(debug_assertions) && client.status().borrow().is_connected())
{
let zeta = zeta::Zeta::register(client.clone(), user_store, cx);
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
if let Some(buffer) = &singleton_buffer {
if buffer.read(cx).file().is_some() {
zeta.update(cx, |zeta, cx| {
zeta.register_buffer(&buffer, cx);
});
}
}
let provider = cx.new(|_| zeta::ZetaInlineCompletionProvider::new(zeta));
let data_collection = ProviderDataCollection::new(
zeta.clone(),
window.root::<Workspace>().flatten(),
singleton_buffer,
cx,
);
let provider =
cx.new(|_| zeta::ZetaInlineCompletionProvider::new(zeta, data_collection));
editor.set_inline_completion_provider(Some(provider), window, cx);
}
}

View File

@@ -77,6 +77,20 @@ pub mod theme_selector {
impl_actions!(theme_selector, [Toggle]);
}
pub mod icon_theme_selector {
use gpui::impl_actions;
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
pub struct Toggle {
/// A list of icon theme names to filter the theme selector down to.
pub themes_filter: Option<Vec<String>>,
}
impl_actions!(icon_theme_selector, [Toggle]);
}
pub mod assistant {
use gpui::{actions, impl_actions};
use schemars::JsonSchema;

View File

@@ -1,5 +1,5 @@
[package]
name = "zed_predict_tos"
name = "zed_predict_onboarding"
version = "0.1.0"
edition = "2021"
publish = false
@@ -9,15 +9,23 @@ license = "GPL-3.0-or-later"
workspace = true
[lib]
path = "src/zed_predict_tos.rs"
path = "src/lib.rs"
doctest = false
[features]
test-support = []
[dependencies]
chrono.workspace = true
client.workspace = true
db.workspace = true
feature_flags.workspace = true
fs.workspace = true
gpui.workspace = true
ui.workspace = true
workspace.workspace = true
language.workspace = true
menu.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true

View File

@@ -0,0 +1,168 @@
use std::sync::Arc;
use crate::ZedPredictModal;
use chrono::Utc;
use client::{Client, UserStore};
use feature_flags::{FeatureFlagAppExt as _, PredictEditsFeatureFlag};
use fs::Fs;
use gpui::{Entity, Subscription, WeakEntity};
use language::language_settings::{all_language_settings, InlineCompletionProvider};
use settings::SettingsStore;
use ui::{prelude::*, ButtonLike, Tooltip};
use util::ResultExt;
use workspace::Workspace;
/// Prompts user to try AI inline prediction feature
pub struct ZedPredictBanner {
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
client: Arc<Client>,
fs: Arc<dyn Fs>,
dismissed: bool,
_subscription: Subscription,
}
impl ZedPredictBanner {
pub fn new(
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
client: Arc<Client>,
fs: Arc<dyn Fs>,
cx: &mut Context<Self>,
) -> Self {
Self {
workspace,
user_store,
client,
fs,
dismissed: get_dismissed(),
_subscription: cx.observe_global::<SettingsStore>(Self::handle_settings_changed),
}
}
fn should_show(&self, cx: &mut App) -> bool {
if !cx.has_flag::<PredictEditsFeatureFlag>() || self.dismissed {
return false;
}
let provider = all_language_settings(None, cx).inline_completions.provider;
match provider {
InlineCompletionProvider::None
| InlineCompletionProvider::Copilot
| InlineCompletionProvider::Supermaven => true,
InlineCompletionProvider::Zed => false,
}
}
fn handle_settings_changed(&mut self, cx: &mut Context<Self>) {
if self.dismissed {
return;
}
let provider = all_language_settings(None, cx).inline_completions.provider;
match provider {
InlineCompletionProvider::None
| InlineCompletionProvider::Copilot
| InlineCompletionProvider::Supermaven => {}
InlineCompletionProvider::Zed => {
self.dismiss(cx);
}
}
}
fn dismiss(&mut self, cx: &mut Context<Self>) {
persist_dismissed(cx);
self.dismissed = true;
cx.notify();
}
}
const DISMISSED_AT_KEY: &str = "zed_predict_banner_dismissed_at";
pub(crate) fn get_dismissed() -> bool {
db::kvp::KEY_VALUE_STORE
.read_kvp(DISMISSED_AT_KEY)
.log_err()
.map_or(false, |dismissed| dismissed.is_some())
}
pub(crate) fn persist_dismissed(cx: &mut App) {
cx.spawn(|_| {
let time = Utc::now().to_rfc3339();
db::kvp::KEY_VALUE_STORE.write_kvp(DISMISSED_AT_KEY.into(), time)
})
.detach_and_log_err(cx);
}
impl Render for ZedPredictBanner {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if !self.should_show(cx) {
return div();
}
let border_color = cx.theme().colors().editor_foreground.opacity(0.3);
let banner = h_flex()
.rounded_md()
.border_1()
.border_color(border_color)
.child(
ButtonLike::new("try-zed-predict")
.child(
h_flex()
.h_full()
.items_center()
.gap_1p5()
.child(Icon::new(IconName::ZedPredict).size(IconSize::Small))
.child(
h_flex()
.gap_0p5()
.child(
Label::new("Introducing:")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(Label::new("Edit Prediction").size(LabelSize::Small)),
),
)
.on_click({
let workspace = self.workspace.clone();
let user_store = self.user_store.clone();
let client = self.client.clone();
let fs = self.fs.clone();
move |_, window, cx| {
let Some(workspace) = workspace.upgrade() else {
return;
};
ZedPredictModal::toggle(
workspace,
user_store.clone(),
client.clone(),
fs.clone(),
window,
cx,
);
}
}),
)
.child(
div().border_l_1().border_color(border_color).child(
IconButton::new("close", IconName::Close)
.icon_size(IconSize::Indicator)
.on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx)))
.tooltip(|window, cx| {
Tooltip::with_meta(
"Close Announcement Banner",
None,
"It won't show again for this feature",
window,
cx,
)
}),
),
);
div().pr_1().child(banner)
}
}

View File

@@ -0,0 +1,5 @@
mod banner;
mod modal;
pub use banner::ZedPredictBanner;
pub use modal::ZedPredictModal;

View File

@@ -0,0 +1,313 @@
use std::{sync::Arc, time::Duration};
use client::{Client, UserStore};
use feature_flags::FeatureFlagAppExt as _;
use fs::Fs;
use gpui::{
ease_in_out, svg, Animation, AnimationExt as _, ClickEvent, DismissEvent, Entity, EventEmitter,
FocusHandle, Focusable, MouseDownEvent, Render,
};
use language::language_settings::{AllLanguageSettings, InlineCompletionProvider};
use settings::{update_settings_file, Settings};
use ui::{prelude::*, CheckboxWithLabel, TintColor};
use workspace::{notifications::NotifyTaskExt, ModalView, Workspace};
/// Introduces user to AI inline prediction feature and terms of service
pub struct ZedPredictModal {
user_store: Entity<UserStore>,
client: Arc<Client>,
fs: Arc<dyn Fs>,
focus_handle: FocusHandle,
sign_in_status: SignInStatus,
terms_of_service: bool,
}
#[derive(PartialEq, Eq)]
enum SignInStatus {
/// Signed out or signed in but not from this modal
Idle,
/// Authentication triggered from this modal
Waiting,
/// Signed in after authentication from this modal
SignedIn,
}
impl ZedPredictModal {
fn new(
user_store: Entity<UserStore>,
client: Arc<Client>,
fs: Arc<dyn Fs>,
cx: &mut Context<Self>,
) -> Self {
ZedPredictModal {
user_store,
client,
fs,
focus_handle: cx.focus_handle(),
sign_in_status: SignInStatus::Idle,
terms_of_service: false,
}
}
pub fn toggle(
workspace: Entity<Workspace>,
user_store: Entity<UserStore>,
client: Arc<Client>,
fs: Arc<dyn Fs>,
window: &mut Window,
cx: &mut App,
) {
workspace.update(cx, |this, cx| {
this.toggle_modal(window, cx, |_window, cx| {
ZedPredictModal::new(user_store, client, fs, cx)
});
});
}
fn view_terms(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("https://zed.dev/terms-of-service");
cx.notify();
}
fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("https://zed.dev/blog/"); // TODO Add the link when live
cx.notify();
}
fn accept_and_enable(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
let task = self
.user_store
.update(cx, |this, cx| this.accept_terms_of_service(cx));
cx.spawn(|this, mut cx| async move {
task.await?;
this.update(&mut cx, |this, cx| {
update_settings_file::<AllLanguageSettings>(this.fs.clone(), cx, move |file, _| {
file.features
.get_or_insert(Default::default())
.inline_completion_provider = Some(InlineCompletionProvider::Zed);
});
cx.emit(DismissEvent);
})
})
.detach_and_notify_err(window, cx);
}
fn sign_in(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
let client = self.client.clone();
self.sign_in_status = SignInStatus::Waiting;
cx.spawn(move |this, mut cx| async move {
let result = client.authenticate_and_connect(true, &cx).await;
let status = match result {
Ok(_) => SignInStatus::SignedIn,
Err(_) => SignInStatus::Idle,
};
this.update(&mut cx, |this, cx| {
this.sign_in_status = status;
cx.notify()
})?;
result
})
.detach_and_notify_err(window, cx);
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl EventEmitter<DismissEvent> for ZedPredictModal {}
impl Focusable for ZedPredictModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl ModalView for ZedPredictModal {}
impl Render for ZedPredictModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let base = v_flex()
.w(px(420.))
.p_4()
.relative()
.gap_2()
.overflow_hidden()
.elevation_3(cx)
.id("zed predict tos")
.track_focus(&self.focus_handle(cx))
.on_action(cx.listener(Self::cancel))
.key_context("ZedPredictModal")
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
this.focus_handle.focus(window);
}))
.child(
div()
.p_1p5()
.absolute()
.top_0()
.left_0()
.right_0()
.h(px(200.))
.child(
svg()
.path("icons/zed_predict_bg.svg")
.text_color(cx.theme().colors().icon_disabled)
.w(px(416.))
.h(px(128.))
.overflow_hidden(),
),
)
.child(
h_flex()
.w_full()
.mb_2()
.justify_between()
.child(
v_flex()
.gap_1()
.child(
Label::new("Introducing Zed AI's")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(Headline::new("Edit Prediction").size(HeadlineSize::Large)),
)
.child({
let tab = |n: usize| {
let text_color = cx.theme().colors().text;
let border_color = cx.theme().colors().text_accent.opacity(0.4);
h_flex().child(
h_flex()
.px_4()
.py_0p5()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(border_color)
.rounded_md()
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_size(TextSize::XSmall.rems(cx))
.text_color(text_color)
.child("tab")
.with_animation(
ElementId::Integer(n),
Animation::new(Duration::from_secs(2)).repeat(),
move |tab, delta| {
let delta = (delta - 0.15 * n as f32) / 0.7;
let delta = 1.0 - (0.5 - delta).abs() * 2.;
let delta = ease_in_out(delta.clamp(0., 1.));
let delta = 0.1 + 0.9 * delta;
tab.border_color(border_color.opacity(delta))
.text_color(text_color.opacity(delta))
},
),
)
};
v_flex()
.gap_2()
.items_center()
.pr_4()
.child(tab(0).ml_neg_20())
.child(tab(1))
.child(tab(2).ml_20())
}),
)
.child(h_flex().absolute().top_2().right_2().child(
IconButton::new("cancel", IconName::X).on_click(cx.listener(
|_, _: &ClickEvent, _window, cx| {
cx.emit(DismissEvent);
},
)),
));
let blog_post_button = if cx.is_staff() {
Some(
Button::new("view-blog", "Read the Blog Post")
.full_width()
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::Indicator)
.icon_color(Color::Muted)
.on_click(cx.listener(Self::view_blog)),
)
} else {
// TODO: put back when blog post is published
None
};
if self.user_store.read(cx).current_user().is_some() {
let copy = match self.sign_in_status {
SignInStatus::Idle => "Get accurate and helpful edit predictions at every keystroke. To set Zed as your inline completions provider, ensure you:",
SignInStatus::SignedIn => "Almost there! Ensure you:",
SignInStatus::Waiting => unreachable!(),
};
base.child(Label::new(copy).color(Color::Muted))
.child(
h_flex()
.gap_0p5()
.child(CheckboxWithLabel::new(
"tos-checkbox",
Label::new("Have read and accepted the").color(Color::Muted),
self.terms_of_service.into(),
cx.listener(move |this, state, _window, cx| {
this.terms_of_service = *state == ToggleState::Selected;
cx.notify()
}),
))
.child(
Button::new("view-tos", "Terms of Service")
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::Indicator)
.icon_color(Color::Muted)
.on_click(cx.listener(Self::view_terms)),
),
)
.child(
v_flex()
.mt_2()
.gap_2()
.w_full()
.child(
Button::new("accept-tos", "Enable Edit Predictions")
.disabled(!self.terms_of_service)
.style(ButtonStyle::Tinted(TintColor::Accent))
.full_width()
.on_click(cx.listener(Self::accept_and_enable)),
)
.children(blog_post_button),
)
} else {
base.child(
Label::new("To set Zed as your inline completions provider, please sign in.")
.color(Color::Muted),
)
.child(
v_flex()
.mt_2()
.gap_2()
.w_full()
.child(
Button::new("accept-tos", "Sign in with GitHub")
.disabled(self.sign_in_status == SignInStatus::Waiting)
.style(ButtonStyle::Tinted(TintColor::Accent))
.full_width()
.on_click(cx.listener(Self::sign_in)),
)
.children(blog_post_button),
)
}
}
}

View File

@@ -1,155 +0,0 @@
//! AI service Terms of Service acceptance modal.
use client::UserStore;
use gpui::{
App, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent,
Render,
};
use ui::{prelude::*, TintColor};
use workspace::{ModalView, Workspace};
/// Terms of acceptance for AI inline prediction.
pub struct ZedPredictTos {
focus_handle: FocusHandle,
user_store: Entity<UserStore>,
workspace: Entity<Workspace>,
viewed: bool,
}
impl ZedPredictTos {
fn new(
workspace: Entity<Workspace>,
user_store: Entity<UserStore>,
cx: &mut Context<Self>,
) -> Self {
ZedPredictTos {
viewed: false,
focus_handle: cx.focus_handle(),
user_store,
workspace,
}
}
pub fn toggle(
workspace: Entity<Workspace>,
user_store: Entity<UserStore>,
window: &mut Window,
cx: &mut App,
) {
workspace.update(cx, |this, cx| {
let workspace = cx.entity().clone();
this.toggle_modal(window, cx, |_window, cx| {
ZedPredictTos::new(workspace, user_store, cx)
});
});
}
fn view_terms(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context<Self>) {
self.viewed = true;
cx.open_url("https://zed.dev/terms-of-service");
cx.notify();
}
fn accept_terms(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context<Self>) {
let task = self
.user_store
.update(cx, |this, cx| this.accept_terms_of_service(cx));
let workspace = self.workspace.clone();
cx.spawn(|this, mut cx| async move {
match task.await {
Ok(_) => this.update(&mut cx, |_, cx| {
cx.emit(DismissEvent);
}),
Err(err) => workspace.update(&mut cx, |this, cx| {
this.show_error(&err, cx);
}),
}
})
.detach_and_log_err(cx);
}
fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl EventEmitter<DismissEvent> for ZedPredictTos {}
impl Focusable for ZedPredictTos {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl ModalView for ZedPredictTos {}
impl Render for ZedPredictTos {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.id("zed predict tos")
.track_focus(&self.focus_handle(cx))
.on_action(cx.listener(Self::cancel))
.key_context("ZedPredictTos")
.elevation_3(cx)
.w_96()
.items_center()
.p_4()
.gap_2()
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
this.focus_handle.focus(window);
}))
.child(
h_flex()
.w_full()
.justify_between()
.child(
v_flex()
.gap_0p5()
.child(
Label::new("Zed AI")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(Headline::new("Edit Prediction")),
)
.child(Icon::new(IconName::ZedPredict).size(IconSize::XLarge)),
)
.child(
Label::new(
"To use Zed AI's Edit Prediction feature, please read and accept our Terms of Service.",
)
.color(Color::Muted),
)
.child(
v_flex()
.mt_2()
.gap_0p5()
.w_full()
.child(if self.viewed {
Button::new("accept-tos", "I've Read and Accept the Terms of Service")
.style(ButtonStyle::Tinted(TintColor::Accent))
.full_width()
.on_click(cx.listener(Self::accept_terms))
} else {
Button::new("view-tos", "Read Terms of Service")
.style(ButtonStyle::Tinted(TintColor::Accent))
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.full_width()
.on_click(cx.listener(Self::view_terms))
})
.child(
Button::new("cancel", "Cancel")
.full_width()
.on_click(cx.listener(|_, _: &ClickEvent, _window, cx| {
cx.emit(DismissEvent);
})),
),
)
}
}

View File

@@ -22,6 +22,7 @@ arrayvec.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
@@ -34,6 +35,7 @@ language_models.workspace = true
log.workspace = true
menu.workspace = true
rpc.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
similar.workspace = true

View File

@@ -0,0 +1,48 @@
use std::path::{Path, PathBuf};
use workspace::WorkspaceDb;
use db::sqlez_macros::sql;
use db::{define_connection, query};
define_connection!(
pub static ref DB: ZetaDb<WorkspaceDb> = &[
sql! (
CREATE TABLE zeta_preferences(
worktree_path BLOB NOT NULL PRIMARY KEY,
accepted_data_collection INTEGER
) STRICT;
),
];
);
impl ZetaDb {
query! {
pub fn get_all_data_collection_preferences() -> Result<Vec<(PathBuf, bool)>> {
SELECT worktree_path, accepted_data_collection FROM zeta_preferences
}
}
query! {
pub fn get_accepted_data_collection(worktree_path: &Path) -> Result<Option<bool>> {
SELECT accepted_data_collection FROM zeta_preferences
WHERE worktree_path = ?
}
}
query! {
pub async fn save_data_collection_choice(worktree_path: PathBuf, accepted_data_collection: bool) -> Result<()> {
INSERT INTO zeta_preferences
(worktree_path, accepted_data_collection)
VALUES
(?1, ?2)
ON CONFLICT (worktree_path) DO UPDATE SET
accepted_data_collection = ?2
}
}
query! {
pub async fn clear_all_zeta_preferences() -> Result<()> {
DELETE FROM zeta_preferences
}
}
}

View File

@@ -1,16 +1,21 @@
mod completion_diff_element;
mod persistence;
mod rate_completion_modal;
pub(crate) use completion_diff_element::*;
use db::kvp::KEY_VALUE_STORE;
use inline_completion::DataCollectionState;
pub use rate_completion_modal::*;
use anyhow::{anyhow, Context as _, Result};
use arrayvec::ArrayVec;
use client::{Client, UserStore};
use collections::hash_map::Entry;
use collections::{HashMap, HashSet, VecDeque};
use futures::AsyncReadExt;
use gpui::{
actions, App, AppContext as _, AsyncApp, Context, Entity, EntityId, Global, Subscription, Task,
WeakEntity,
};
use http_client::{HttpClient, Method};
use language::{
@@ -21,24 +26,30 @@ use language_models::LlmApiToken;
use rpc::{PredictEditsParams, PredictEditsResponse, EXPIRED_LLM_TOKEN_HEADER_NAME};
use std::{
borrow::Cow,
cmp,
cmp, env,
fmt::Write,
future::Future,
mem,
ops::Range,
path::Path,
path::{Path, PathBuf},
sync::Arc,
time::{Duration, Instant},
};
use telemetry_events::InlineCompletionRating;
use util::ResultExt;
use uuid::Uuid;
use workspace::{
notifications::{simple_message_notification::MessageNotification, NotificationId},
Workspace,
};
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
const START_OF_FILE_MARKER: &'static str = "<|start_of_file|>";
const EDITABLE_REGION_START_MARKER: &'static str = "<|editable_region_start|>";
const EDITABLE_REGION_END_MARKER: &'static str = "<|editable_region_end|>";
const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1);
const ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY: &'static str =
"zed_predict_data_collection_never_ask_again";
actions!(edit_prediction, [ClearHistory]);
@@ -159,6 +170,7 @@ pub struct Zeta {
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
shown_completions: VecDeque<InlineCompletion>,
rated_completions: HashSet<InlineCompletionId>,
data_collection_preferences: DataCollectionPreferences,
llm_token: LlmApiToken,
_llm_token_subscription: Subscription,
tos_accepted: bool, // Terms of service accepted
@@ -188,13 +200,13 @@ impl Zeta {
fn new(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut Context<Self>) -> Self {
let refresh_llm_token_listener = language_models::RefreshLlmTokenListener::global(cx);
Self {
client,
events: VecDeque::new(),
shown_completions: VecDeque::new(),
rated_completions: HashSet::default(),
registered_buffers: HashMap::default(),
data_collection_preferences: Self::load_data_collection_preferences(cx),
llm_token: LlmApiToken::default(),
_llm_token_subscription: cx.subscribe(
&refresh_llm_token_listener,
@@ -212,11 +224,16 @@ impl Zeta {
.read(cx)
.current_user_has_accepted_terms()
.unwrap_or(false),
_user_store_subscription: cx.subscribe(&user_store, |this, _, event, _| match event {
client::user::Event::TermsStatusUpdated { accepted } => {
this.tos_accepted = *accepted;
_user_store_subscription: cx.subscribe(&user_store, |this, user_store, event, cx| {
match event {
client::user::Event::PrivateUserInfoUpdated => {
this.tos_accepted = user_store
.read(cx)
.current_user_has_accepted_terms()
.unwrap_or(false);
}
_ => {}
}
_ => {}
}),
}
}
@@ -282,18 +299,16 @@ impl Zeta {
event: &language::BufferEvent,
cx: &mut Context<Self>,
) {
match event {
language::BufferEvent::Edited => {
self.report_changes_for_buffer(&buffer, cx);
}
_ => {}
if let language::BufferEvent::Edited = event {
self.report_changes_for_buffer(&buffer, cx);
}
}
pub fn request_completion_impl<F, R>(
&mut self,
buffer: &Entity<Buffer>,
position: language::Anchor,
cursor: language::Anchor,
can_collect_data: bool,
cx: &mut Context<Self>,
perform_predict_edits: F,
) -> Task<Result<Option<InlineCompletion>>>
@@ -302,7 +317,7 @@ impl Zeta {
R: Future<Output = Result<PredictEditsResponse>> + Send + 'static,
{
let snapshot = self.report_changes_for_buffer(buffer, cx);
let point = position.to_point(&snapshot);
let point = cursor.to_point(&snapshot);
let offset = point.to_offset(&snapshot);
let excerpt_range = excerpt_range_for_position(point, &snapshot);
let events = self.events.clone();
@@ -346,6 +361,7 @@ impl Zeta {
input_events: input_events.clone(),
input_excerpt: input_excerpt.clone(),
outline: Some(input_outline.clone()),
can_collect_data,
};
let response = perform_predict_edits(client, llm_token, body).await?;
@@ -515,16 +531,23 @@ and then another
) -> Task<Result<Option<InlineCompletion>>> {
use std::future::ready;
self.request_completion_impl(buffer, position, cx, |_, _, _| ready(Ok(response)))
self.request_completion_impl(buffer, position, false, cx, |_, _, _| ready(Ok(response)))
}
pub fn request_completion(
&mut self,
buffer: &Entity<Buffer>,
position: language::Anchor,
can_collect_data: bool,
cx: &mut Context<Self>,
) -> Task<Result<Option<InlineCompletion>>> {
self.request_completion_impl(buffer, position, cx, Self::perform_predict_edits)
self.request_completion_impl(
buffer,
position,
can_collect_data,
cx,
Self::perform_predict_edits,
)
}
fn perform_predict_edits(
@@ -769,6 +792,85 @@ and then another
new_snapshot
}
/// Creates a `Entity<DataCollectionChoice>` for each unique worktree abs path it sees.
pub fn data_collection_choice_at(
&mut self,
worktree_abs_path: PathBuf,
cx: &mut Context<Self>,
) -> Entity<DataCollectionChoice> {
match self
.data_collection_preferences
.per_worktree
.entry(worktree_abs_path)
{
Entry::Vacant(entry) => {
let choice = cx.new(|_| DataCollectionChoice::NotAnswered);
entry.insert(choice.clone());
choice
}
Entry::Occupied(entry) => entry.get().clone(),
}
}
fn set_never_ask_again_for_data_collection(&mut self, cx: &mut Context<Self>) {
self.data_collection_preferences.never_ask_again = true;
// persist choice
db::write_and_log(cx, move || {
KEY_VALUE_STORE.write_kvp(
ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY.into(),
"true".to_string(),
)
});
}
fn load_data_collection_preferences(cx: &mut Context<Self>) -> DataCollectionPreferences {
if env::var("ZED_PREDICT_CLEAR_DATA_COLLECTION_PREFERENCES").is_ok() {
db::write_and_log(cx, move || async move {
KEY_VALUE_STORE
.delete_kvp(ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY.into())
.await
.log_err();
persistence::DB.clear_all_zeta_preferences().await
});
return DataCollectionPreferences::default();
}
let never_ask_again = KEY_VALUE_STORE
.read_kvp(ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY)
.log_err()
.flatten()
.map(|value| value == "true")
.unwrap_or(false);
let preferences_per_worktree = persistence::DB
.get_all_data_collection_preferences()
.log_err()
.into_iter()
.flatten()
.map(|(path, choice)| {
let choice = cx.new(|_| DataCollectionChoice::from(choice));
(path, choice)
})
.collect();
DataCollectionPreferences {
never_ask_again,
per_worktree: preferences_per_worktree,
}
}
}
#[derive(Default, Debug)]
struct DataCollectionPreferences {
/// Set when a user clicks on "Never Ask Again", can never be unset.
never_ask_again: bool,
/// The choices for each worktree.
///
/// This is filled when loading from database, or when querying if no matching path is found.
per_worktree: HashMap<PathBuf, Entity<DataCollectionChoice>>,
}
fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
@@ -968,22 +1070,127 @@ struct PendingCompletion {
_task: Task<()>,
}
#[derive(Debug, Clone, Copy)]
pub enum DataCollectionChoice {
NotAnswered,
Enabled,
Disabled,
}
impl DataCollectionChoice {
pub fn is_enabled(self) -> bool {
match self {
Self::Enabled => true,
Self::NotAnswered | Self::Disabled => false,
}
}
pub fn is_answered(self) -> bool {
match self {
Self::Enabled | Self::Disabled => true,
Self::NotAnswered => false,
}
}
pub fn toggle(self) -> DataCollectionChoice {
match self {
Self::Enabled => Self::Disabled,
Self::Disabled => Self::Enabled,
Self::NotAnswered => Self::Enabled,
}
}
}
impl From<bool> for DataCollectionChoice {
fn from(value: bool) -> Self {
match value {
true => DataCollectionChoice::Enabled,
false => DataCollectionChoice::Disabled,
}
}
}
pub struct ZetaInlineCompletionProvider {
zeta: Entity<Zeta>,
pending_completions: ArrayVec<PendingCompletion, 2>,
next_pending_completion_id: usize,
current_completion: Option<CurrentInlineCompletion>,
data_collection: Option<ProviderDataCollection>,
}
pub struct ProviderDataCollection {
workspace: WeakEntity<Workspace>,
worktree_root_path: PathBuf,
choice: Entity<DataCollectionChoice>,
}
impl ProviderDataCollection {
pub fn new(
zeta: Entity<Zeta>,
workspace: Option<Entity<Workspace>>,
buffer: Option<Entity<Buffer>>,
cx: &mut App,
) -> Option<ProviderDataCollection> {
let workspace = workspace?;
let worktree_root_path = buffer?.update(cx, |buffer, cx| {
let file = buffer.file()?;
if !file.is_local() || file.is_private() {
return None;
}
workspace.update(cx, |workspace, cx| {
Some(
workspace
.absolute_path_of_worktree(file.worktree_id(cx), cx)?
.to_path_buf(),
)
})
})?;
let choice = zeta.update(cx, |zeta, cx| {
zeta.data_collection_choice_at(worktree_root_path.clone(), cx)
});
Some(ProviderDataCollection {
workspace: workspace.downgrade(),
worktree_root_path,
choice,
})
}
fn set_choice(&mut self, choice: DataCollectionChoice, cx: &mut App) {
self.choice.update(cx, |this, _| *this = choice);
let worktree_root_path = self.worktree_root_path.clone();
db::write_and_log(cx, move || {
persistence::DB.save_data_collection_choice(worktree_root_path, choice.is_enabled())
});
}
fn toggle_choice(&mut self, cx: &mut App) {
self.set_choice(self.choice.read(cx).toggle(), cx);
}
}
impl ZetaInlineCompletionProvider {
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(8);
pub fn new(zeta: Entity<Zeta>) -> Self {
pub fn new(zeta: Entity<Zeta>, data_collection: Option<ProviderDataCollection>) -> Self {
Self {
zeta,
pending_completions: ArrayVec::new(),
next_pending_completion_id: 0,
current_completion: None,
data_collection,
}
}
fn set_data_collection_choice(&mut self, choice: DataCollectionChoice, cx: &mut App) {
if let Some(data_collection) = self.data_collection.as_mut() {
data_collection.set_choice(choice, cx);
}
}
}
@@ -994,7 +1201,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
}
fn display_name() -> &'static str {
"Zed Predict"
"Zed's Edit Predictions"
}
fn show_completions_in_menu() -> bool {
@@ -1009,6 +1216,24 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
true
}
fn data_collection_state(&self, cx: &App) -> DataCollectionState {
let Some(data_collection) = self.data_collection.as_ref() else {
return DataCollectionState::Unknown;
};
if data_collection.choice.read(cx).is_enabled() {
DataCollectionState::Enabled
} else {
DataCollectionState::Disabled
}
}
fn toggle_data_collection(&mut self, cx: &mut App) {
if let Some(data_collection) = self.data_collection.as_mut() {
data_collection.toggle_choice(cx);
}
}
fn is_enabled(
&self,
buffer: &Entity<Buffer>,
@@ -1043,6 +1268,12 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
let pending_completion_id = self.next_pending_completion_id;
self.next_pending_completion_id += 1;
let can_collect_data = self
.data_collection
.as_ref()
.map_or(false, |data_collection| {
data_collection.choice.read(cx).is_enabled()
});
let task = cx.spawn(|this, mut cx| async move {
if debounce {
@@ -1051,7 +1282,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
let completion_request = this.update(&mut cx, |this, cx| {
this.zeta.update(cx, |zeta, cx| {
zeta.request_completion(&buffer, position, cx)
zeta.request_completion(&buffer, position, can_collect_data, cx)
})
});
@@ -1128,8 +1359,79 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
// Right now we don't support cycling.
}
fn accept(&mut self, _cx: &mut Context<Self>) {
fn accept(&mut self, cx: &mut Context<Self>) {
self.pending_completions.clear();
let Some(data_collection) = self.data_collection.as_mut() else {
return;
};
if data_collection.choice.read(cx).is_answered()
|| self
.zeta
.read(cx)
.data_collection_preferences
.never_ask_again
{
return;
}
struct ZetaDataCollectionNotification;
let notification_id = NotificationId::unique::<ZetaDataCollectionNotification>();
const DATA_COLLECTION_INFO_URL: &str = "https://zed.dev/terms-of-service"; // TODO: Replace for a link that's dedicated to Edit Predictions data collection
let this = cx.entity();
data_collection
.workspace
.update(cx, |workspace, cx| {
workspace.show_notification(notification_id, cx, |cx| {
let zeta = self.zeta.clone();
cx.new(move |_cx| {
let message =
"To allow Zed to suggest better edits, turn on data collection. You \
can turn off at any time via the status bar menu.";
MessageNotification::new(message)
.with_title("Per-Project Data Collection Program")
.show_close_button(false)
.with_click_message("Turn On")
.on_click({
let this = this.clone();
move |_window, cx| {
this.update(cx, |this, cx| {
this.set_data_collection_choice(
DataCollectionChoice::Enabled,
cx,
)
});
}
})
.with_secondary_click_message("Turn Off")
.on_secondary_click({
move |_window, cx| {
this.update(cx, |this, cx| {
this.set_data_collection_choice(
DataCollectionChoice::Disabled,
cx,
)
});
}
})
.with_tertiary_click_message("Never Ask Again")
.on_tertiary_click({
move |_window, cx| {
zeta.update(cx, |zeta, cx| {
zeta.set_never_ask_again_for_data_collection(cx);
});
}
})
.more_info_message("Learn More")
.more_info_url(DATA_COLLECTION_INFO_URL)
})
});
})
.log_err();
}
fn discard(&mut self, _cx: &mut Context<Self>) {
@@ -1379,8 +1681,9 @@ mod tests {
let buffer = cx.new(|cx| Buffer::local(buffer_content, cx));
let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0)));
let completion_task =
zeta.update(cx, |zeta, cx| zeta.request_completion(&buffer, cursor, cx));
let completion_task = zeta.update(cx, |zeta, cx| {
zeta.request_completion(&buffer, cursor, false, cx)
});
let token_request = server.receive::<proto::GetLlmToken>().await.unwrap();
server.respond(

View File

@@ -382,11 +382,16 @@ There are two options to choose from:
- Default:
```json
"inline_completions": {
"disabled_globs": [
".env"
]
}
"inline_completions": {
"disabled_globs": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/secrets.yml"
]
}
```
**Options**