Detailed changes
@@ -3260,6 +3260,7 @@ dependencies = [
"futures 0.3.31",
"gpui",
"http_client",
+ "icons",
"language",
"language_models",
"log",
@@ -3693,6 +3694,7 @@ dependencies = [
"futures 0.3.31",
"gpui",
"http_client",
+ "icons",
"indoc",
"language",
"log",
@@ -5402,6 +5404,7 @@ version = "0.1.0"
dependencies = [
"client",
"gpui",
+ "icons",
"language",
"text",
]
@@ -1,32 +1 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_3348_16)">
@@ -0,0 +1 @@
@@ -0,0 +1 @@
@@ -0,0 +1 @@
@@ -0,0 +1 @@
@@ -15,6 +15,7 @@ edit_prediction.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
+icons.workspace = true
language.workspace = true
language_models.workspace = true
log.workspace = true
@@ -1,9 +1,10 @@
use anyhow::Result;
use edit_prediction::cursor_excerpt;
-use edit_prediction_types::{EditPrediction, EditPredictionDelegate};
+use edit_prediction_types::{EditPrediction, EditPredictionDelegate, EditPredictionIconSet};
use futures::AsyncReadExt;
use gpui::{App, Context, Entity, Task};
use http_client::HttpClient;
+use icons::IconName;
use language::{
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, EditPreview, ToPoint,
};
@@ -172,6 +173,10 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate {
true
}
+ fn icons(&self, _cx: &App) -> EditPredictionIconSet {
+ EditPredictionIconSet::new(IconName::AiMistral)
+ }
+
fn is_enabled(&self, _buffer: &Entity<Buffer>, _cursor_position: Anchor, cx: &App) -> bool {
Self::api_key(cx).is_some()
}
@@ -32,6 +32,7 @@ fs.workspace = true
futures.workspace = true
gpui.workspace = true
edit_prediction_types.workspace = true
+icons.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
@@ -6,8 +6,11 @@ use crate::{
},
};
use anyhow::Result;
-use edit_prediction_types::{EditPrediction, EditPredictionDelegate, interpolate_edits};
+use edit_prediction_types::{
+ EditPrediction, EditPredictionDelegate, EditPredictionIconSet, interpolate_edits,
+};
use gpui::{App, Context, Entity, Task};
+use icons::IconName;
use language::{Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToPointUtf16};
use std::{ops::Range, sync::Arc, time::Duration};
@@ -50,6 +53,12 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
true
}
+ fn icons(&self, _cx: &App) -> EditPredictionIconSet {
+ EditPredictionIconSet::new(IconName::Copilot)
+ .with_disabled(IconName::CopilotDisabled)
+ .with_error(IconName::CopilotError)
+ }
+
fn is_refreshing(&self, _cx: &App) -> bool {
self.pending_refresh.is_some() && self.completion.is_none()
}
@@ -659,6 +659,32 @@ impl EditPredictionStore {
self.edit_prediction_model = model;
}
+ pub fn icons(&self) -> edit_prediction_types::EditPredictionIconSet {
+ use ui::IconName;
+ match self.edit_prediction_model {
+ EditPredictionModel::Ollama => {
+ edit_prediction_types::EditPredictionIconSet::new(IconName::AiOllama)
+ }
+ EditPredictionModel::Sweep => {
+ edit_prediction_types::EditPredictionIconSet::new(IconName::SweepAi)
+ .with_disabled(IconName::SweepAiDisabled)
+ .with_up(IconName::SweepAiUp)
+ .with_down(IconName::SweepAiDown)
+ .with_error(IconName::SweepAiError)
+ }
+ EditPredictionModel::Mercury => {
+ edit_prediction_types::EditPredictionIconSet::new(IconName::Inception)
+ }
+ EditPredictionModel::Zeta1 | EditPredictionModel::Zeta2 { .. } => {
+ edit_prediction_types::EditPredictionIconSet::new(IconName::ZedPredict)
+ .with_disabled(IconName::ZedPredictDisabled)
+ .with_up(IconName::ZedPredictUp)
+ .with_down(IconName::ZedPredictDown)
+ .with_error(IconName::ZedPredictError)
+ }
+ }
+ }
+
pub fn has_sweep_api_token(&self, cx: &App) -> bool {
self.sweep_ai.api_token.read(cx).has_key()
}
@@ -2,10 +2,13 @@ use std::{cmp, sync::Arc};
use client::{Client, UserStore};
use cloud_llm_client::EditPredictionRejectReason;
-use edit_prediction_types::{DataCollectionState, EditPredictionDelegate, SuggestionDisplayType};
+use edit_prediction_types::{
+ DataCollectionState, EditPredictionDelegate, EditPredictionIconSet, SuggestionDisplayType,
+};
use gpui::{App, Entity, prelude::*};
use language::{Buffer, ToPoint as _};
use project::Project;
+use ui::prelude::*;
use crate::{BufferEditPrediction, EditPredictionModel, EditPredictionStore};
@@ -58,6 +61,25 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
true
}
+ fn icons(&self, cx: &App) -> EditPredictionIconSet {
+ match self.store.read(cx).edit_prediction_model {
+ EditPredictionModel::Ollama => EditPredictionIconSet::new(IconName::AiOllama),
+ EditPredictionModel::Sweep => EditPredictionIconSet::new(IconName::SweepAi)
+ .with_disabled(IconName::SweepAiDisabled)
+ .with_up(IconName::SweepAiUp)
+ .with_down(IconName::SweepAiDown)
+ .with_error(IconName::SweepAiError),
+ EditPredictionModel::Mercury => EditPredictionIconSet::new(IconName::Inception),
+ EditPredictionModel::Zeta1 | EditPredictionModel::Zeta2 { .. } => {
+ EditPredictionIconSet::new(IconName::ZedPredict)
+ .with_disabled(IconName::ZedPredictDisabled)
+ .with_up(IconName::ZedPredictUp)
+ .with_down(IconName::ZedPredictDown)
+ .with_error(IconName::ZedPredictError)
+ }
+ }
+ }
+
fn data_collection_state(&self, cx: &App) -> DataCollectionState {
if let Some(buffer) = &self.singleton_buffer
&& let Some(file) = buffer.read(cx).file()
@@ -14,5 +14,6 @@ path = "src/edit_prediction_types.rs"
[dependencies]
client.workspace = true
gpui.workspace = true
+icons.workspace = true
language.workspace = true
text.workspace = true
@@ -2,8 +2,78 @@ use std::{ops::Range, sync::Arc};
use client::EditPredictionUsage;
use gpui::{App, Context, Entity, SharedString};
+use icons::IconName;
use language::{Anchor, Buffer, OffsetRangeExt};
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct EditPredictionIconSet {
+ pub base: IconName,
+ pub disabled: IconName,
+ pub up: IconName,
+ pub down: IconName,
+ pub error: IconName,
+}
+
+impl EditPredictionIconSet {
+ pub fn new(base: IconName) -> Self {
+ Self {
+ base,
+ disabled: IconName::ZedPredictDisabled,
+ up: IconName::ZedPredictUp,
+ down: IconName::ZedPredictDown,
+ error: IconName::ZedPredictError,
+ }
+ }
+
+ pub fn with_disabled(mut self, disabled: IconName) -> Self {
+ self.disabled = disabled;
+ self
+ }
+
+ pub fn with_up(mut self, up: IconName) -> Self {
+ self.up = up;
+ self
+ }
+
+ pub fn with_down(mut self, down: IconName) -> Self {
+ self.down = down;
+ self
+ }
+
+ pub fn with_error(mut self, error: IconName) -> Self {
+ self.error = error;
+ self
+ }
+}
+
+/// Represents a predicted cursor position after an edit is applied.
+///
+/// Since the cursor may be positioned inside newly inserted text that doesn't
+/// exist in the original buffer, we store an anchor (which points to a position
+/// in the original buffer, typically the start of an edit) plus an offset into
+/// the inserted text.
+#[derive(Clone, Debug)]
+pub struct PredictedCursorPosition {
+ /// An anchor in the original buffer. If the cursor is inside an edit,
+ /// this points to the start of that edit's range.
+ pub anchor: language::Anchor,
+ /// Offset from the anchor into the new text. If the cursor is inside
+ /// inserted text, this is the offset within that insertion. If the cursor
+ /// is outside any edit, this is 0.
+ pub offset: usize,
+}
+
+impl PredictedCursorPosition {
+ pub fn new(anchor: language::Anchor, offset: usize) -> Self {
+ Self { anchor, offset }
+ }
+
+ /// Creates a predicted cursor position at an exact anchor location (offset = 0).
+ pub fn at_anchor(anchor: language::Anchor) -> Self {
+ Self { anchor, offset: 0 }
+ }
+}
+
/// The display mode used when showing an edit prediction to the user.
/// Used for metrics tracking.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -81,6 +151,8 @@ pub trait EditPredictionDelegate: 'static + Sized {
true
}
+ fn icons(&self, cx: &App) -> EditPredictionIconSet;
+
fn data_collection_state(&self, _cx: &App) -> DataCollectionState {
DataCollectionState::Unsupported
}
@@ -127,6 +199,7 @@ pub trait EditPredictionDelegateHandle {
fn show_predictions_in_menu(&self) -> bool;
fn show_tab_accept_marker(&self) -> bool;
fn supports_jump_to_edit(&self) -> bool;
+ fn icons(&self, cx: &App) -> EditPredictionIconSet;
fn data_collection_state(&self, cx: &App) -> DataCollectionState;
fn usage(&self, cx: &App) -> Option<EditPredictionUsage>;
fn toggle_data_collection(&self, cx: &mut App);
@@ -173,6 +246,10 @@ where
T::supports_jump_to_edit()
}
+ fn icons(&self, cx: &App) -> EditPredictionIconSet {
+ self.read(cx).icons(cx)
+ }
+
fn data_collection_state(&self, cx: &App) -> DataCollectionState {
self.read(cx).data_collection_state(cx)
}
@@ -360,6 +360,13 @@ impl Render for EditPredictionButton {
| EditPredictionProvider::Sweep
| EditPredictionProvider::Mercury) => {
let enabled = self.editor_enabled.unwrap_or(true);
+ let icons = self
+ .edit_prediction_provider
+ .as_ref()
+ .map(|p| p.icons(cx))
+ .unwrap_or_else(|| {
+ edit_prediction_types::EditPredictionIconSet::new(IconName::ZedPredict)
+ });
let ep_icon;
let tooltip_meta;
@@ -369,17 +376,15 @@ impl Render for EditPredictionButton {
EditPredictionProvider::Sweep => {
missing_token = edit_prediction::EditPredictionStore::try_global(cx)
.is_some_and(|ep_store| !ep_store.read(cx).has_sweep_api_token(cx));
- ep_icon = IconName::SweepAi;
+ ep_icon = if enabled { icons.base } else { icons.disabled };
tooltip_meta = if missing_token {
"Missing API key for Sweep"
} else {
"Powered by Sweep"
};
- missing_token = edit_prediction::EditPredictionStore::try_global(cx)
- .is_some_and(|ep_store| !ep_store.read(cx).has_sweep_api_token(cx));
}
EditPredictionProvider::Mercury => {
- ep_icon = IconName::Inception;
+ ep_icon = if enabled { icons.base } else { icons.disabled };
missing_token = edit_prediction::EditPredictionStore::try_global(cx)
.is_some_and(|ep_store| !ep_store.read(cx).has_mercury_api_token(cx));
tooltip_meta = if missing_token {
@@ -389,11 +394,7 @@ impl Render for EditPredictionButton {
};
}
_ => {
- ep_icon = if enabled {
- IconName::ZedPredict
- } else {
- IconName::ZedPredictDisabled
- };
+ ep_icon = if enabled { icons.base } else { icons.disabled };
tooltip_meta = "Powered by Zeta"
}
};
@@ -889,10 +890,17 @@ impl EditPredictionButton {
);
if !self.editor_enabled.unwrap_or(true) {
+ let icons = self
+ .edit_prediction_provider
+ .as_ref()
+ .map(|p| p.icons(cx))
+ .unwrap_or_else(|| {
+ edit_prediction_types::EditPredictionIconSet::new(IconName::ZedPredict)
+ });
menu = menu.item(
ContextMenuEntry::new("This file is excluded.")
.disabled(true)
- .icon(IconName::ZedPredictDisabled)
+ .icon(icons.disabled)
.icon_size(IconSize::Small),
);
}
@@ -839,20 +839,21 @@ impl Render for RatePredictionsModal {
.border_color(border_color)
.flex_shrink_0()
.overflow_hidden()
- .child(
+ .child({
+ let icons = self.ep_store.read(cx).icons();
h_flex()
.h_8()
.px_2()
.justify_between()
.border_b_1()
.border_color(border_color)
- .child(Icon::new(IconName::ZedPredict).size(IconSize::Small))
+ .child(Icon::new(icons.base).size(IconSize::Small))
.child(
Label::new("From most recent to oldest")
.color(Color::Muted)
.size(LabelSize::Small),
- ),
- )
+ )
+ })
.child(
div()
.id("completion_list")
@@ -1,9 +1,10 @@
-use edit_prediction_types::EditPredictionDelegate;
+use edit_prediction_types::{EditPredictionDelegate, EditPredictionIconSet};
use gpui::{Entity, KeyBinding, Modifiers, prelude::*};
use indoc::indoc;
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
use std::{ops::Range, sync::Arc};
use text::{Point, ToOffset};
+use ui::prelude::*;
use crate::{
AcceptEditPrediction, EditPrediction, MenuEditPredictionsPolicy, editor_tests::init_test,
@@ -463,6 +464,10 @@ impl EditPredictionDelegate for FakeEditPredictionDelegate {
true
}
+ fn icons(&self, _cx: &gpui::App) -> EditPredictionIconSet {
+ EditPredictionIconSet::new(IconName::ZedPredict)
+ }
+
fn is_enabled(
&self,
_buffer: &gpui::Entity<language::Buffer>,
@@ -530,6 +535,10 @@ impl EditPredictionDelegate for FakeNonZedEditPredictionDelegate {
false
}
+ fn icons(&self, _cx: &gpui::App) -> EditPredictionIconSet {
+ EditPredictionIconSet::new(IconName::ZedPredict)
+ }
+
fn is_enabled(
&self,
_buffer: &gpui::Entity<language::Buffer>,
@@ -9722,6 +9722,7 @@ impl Editor {
let keybind = self.render_edit_prediction_accept_keybind(window, cx);
let has_keybind = keybind.is_some();
+ let icons = Self::get_prediction_provider_icons(&self.edit_prediction_provider, cx);
h_flex()
.id("ep-line-popover")
@@ -9740,7 +9741,7 @@ impl Editor {
el.bg(status_colors.error_background)
.border_color(status_colors.error.opacity(0.6))
.pl_2()
- .child(Icon::new(IconName::ZedPredictError).color(Color::Error))
+ .child(Icon::new(icons.error).color(Color::Error))
.cursor_default()
.hoverable_tooltip(move |_window, cx| {
cx.new(|_| MissingEditPredictionKeybindingTooltip).into()
@@ -9780,6 +9781,7 @@ impl Editor {
) -> Stateful<Div> {
let keybind = self.render_edit_prediction_accept_keybind(window, cx);
let has_keybind = keybind.is_some();
+ let icons = Self::get_prediction_provider_icons(&self.edit_prediction_provider, cx);
let file_name = snapshot
.file()
@@ -9802,7 +9804,7 @@ impl Editor {
el.bg(status_colors.error_background)
.border_color(status_colors.error.opacity(0.6))
.pl_2()
- .child(Icon::new(IconName::ZedPredictError).color(Color::Error))
+ .child(Icon::new(icons.error).color(Color::Error))
.cursor_default()
.hoverable_tooltip(move |_window, cx| {
cx.new(|_| MissingEditPredictionKeybindingTooltip).into()
@@ -9844,16 +9846,13 @@ impl Editor {
let editor_bg_color = cx.theme().colors().editor_background;
editor_bg_color.blend(accent_color.opacity(0.6))
}
- fn get_prediction_provider_icon_name(
+ fn get_prediction_provider_icons(
provider: &Option<RegisteredEditPredictionDelegate>,
- ) -> IconName {
+ cx: &App,
+ ) -> edit_prediction_types::EditPredictionIconSet {
match provider {
- Some(provider) => match provider.provider.name() {
- "copilot" => IconName::Copilot,
- "supermaven" => IconName::Supermaven,
- _ => IconName::ZedPredict,
- },
- None => IconName::ZedPredict,
+ Some(provider) => provider.provider.icons(cx),
+ None => edit_prediction_types::EditPredictionIconSet::new(IconName::ZedPredict),
}
}
@@ -9868,7 +9867,7 @@ impl Editor {
cx: &mut Context<Editor>,
) -> Option<AnyElement> {
let provider = self.edit_prediction_provider.as_ref()?;
- let provider_icon = Self::get_prediction_provider_icon_name(&self.edit_prediction_provider);
+ let icons = Self::get_prediction_provider_icons(&self.edit_prediction_provider, cx);
let is_refreshing = provider.provider.is_refreshing(cx);
@@ -9898,16 +9897,16 @@ impl Editor {
use text::ToPoint as _;
if target.text_anchor.to_point(snapshot).row > cursor_point.row
{
- Icon::new(IconName::ZedPredictDown)
+ Icon::new(icons.down)
} else {
- Icon::new(IconName::ZedPredictUp)
+ Icon::new(icons.up)
}
}
EditPrediction::MoveOutside { .. } => {
// TODO [zeta2] custom icon for external jump?
- Icon::new(provider_icon)
+ Icon::new(icons.base)
}
- EditPrediction::Edit { .. } => Icon::new(provider_icon),
+ EditPrediction::Edit { .. } => Icon::new(icons.base),
}))
.child(
h_flex()
@@ -9974,11 +9973,11 @@ impl Editor {
cx,
)?,
- None => pending_completion_container(provider_icon)
+ None => pending_completion_container(icons.base)
.child(Label::new("...").size(LabelSize::Small)),
},
- None => pending_completion_container(provider_icon)
+ None => pending_completion_container(icons.base)
.child(Label::new("...").size(LabelSize::Small)),
};
@@ -10088,6 +10087,8 @@ impl Editor {
.map(|provider| provider.provider.supports_jump_to_edit())
.unwrap_or(true);
+ let icons = Self::get_prediction_provider_icons(&self.edit_prediction_provider, cx);
+
match &completion.completion {
EditPrediction::MoveWithin {
target, snapshot, ..
@@ -10103,9 +10104,9 @@ impl Editor {
.flex_1()
.child(
if target.text_anchor.to_point(snapshot).row > cursor_point.row {
- Icon::new(IconName::ZedPredictDown)
+ Icon::new(icons.down)
} else {
- Icon::new(IconName::ZedPredictUp)
+ Icon::new(icons.up)
},
)
.child(Label::new("Jump to Edit")),
@@ -10121,7 +10122,7 @@ impl Editor {
.px_2()
.gap_2()
.flex_1()
- .child(Icon::new(IconName::ZedPredict))
+ .child(Icon::new(icons.base))
.child(Label::new(format!("Jump to {file_name}"))),
)
}
@@ -10154,9 +10155,7 @@ impl Editor {
render_relative_row_jump("", cursor_point.row, first_edit_row)
.into_any_element()
} else {
- let icon_name =
- Editor::get_prediction_provider_icon_name(&self.edit_prediction_provider);
- Icon::new(icon_name).into_any_element()
+ Icon::new(icons.base).into_any_element()
};
Some(
@@ -222,6 +222,10 @@ pub enum IconName {
SupermavenInit,
SwatchBook,
SweepAi,
+ SweepAiDisabled,
+ SweepAiDown,
+ SweepAiError,
+ SweepAiUp,
Tab,
Terminal,
TerminalAlt,
@@ -1,6 +1,6 @@
use crate::{Supermaven, SupermavenCompletionStateId};
use anyhow::Result;
-use edit_prediction_types::{EditPrediction, EditPredictionDelegate};
+use edit_prediction_types::{EditPrediction, EditPredictionDelegate, EditPredictionIconSet};
use futures::StreamExt as _;
use gpui::{App, Context, Entity, EntityId, Task};
use language::{Anchor, Buffer, BufferSnapshot};
@@ -11,6 +11,7 @@ use std::{
time::Duration,
};
use text::{ToOffset, ToPoint};
+use ui::prelude::*;
use unicode_segmentation::UnicodeSegmentation;
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
@@ -125,6 +126,12 @@ impl EditPredictionDelegate for SupermavenEditPredictionDelegate {
false
}
+ fn icons(&self, _cx: &App) -> EditPredictionIconSet {
+ EditPredictionIconSet::new(IconName::Supermaven)
+ .with_disabled(IconName::SupermavenDisabled)
+ .with_error(IconName::SupermavenError)
+ }
+
fn is_enabled(&self, _buffer: &Entity<Buffer>, _cursor_position: Anchor, cx: &App) -> bool {
self.supermaven.read(cx).is_enabled()
}