Compare commits

...

29 Commits

Author SHA1 Message Date
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
60 changed files with 1731 additions and 325 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.8"
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

@@ -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

@@ -69,7 +69,7 @@ pub use element::{
};
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,
@@ -3947,12 +3947,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 +3993,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(())));
}
_ => {}

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

@@ -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

@@ -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 {

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

@@ -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)
}

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

@@ -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

@@ -2756,6 +2756,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())

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.172.0"
version = "0.172.8"
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
preview

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

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

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**