zeta: Various product fixes before Preview release (#23125)

Thorsten Ball , Antonio , Antonio Scandurra , and Bennet created

Various fixes for Zeta and one fix that's visible to non-Zeta-using
users of inline completions.

Release Notes:

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

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Bennet <bennet@zed.dev>

Change summary

crates/editor/src/editor.rs                      |   3 
crates/gpui/src/elements/text.rs                 |  32 +++
crates/zed/src/zed/inline_completion_registry.rs |   4 
crates/zeta/src/completion_diff_element.rs       | 161 ++++++++++++++++++
crates/zeta/src/rate_completion_modal.rs         | 103 ++---------
crates/zeta/src/zeta.rs                          |  65 +++----
6 files changed, 247 insertions(+), 121 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -4511,7 +4511,8 @@ impl Editor {
         if !user_requested
             && (!self.enable_inline_completions
                 || !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx)
-                || !self.is_focused(cx))
+                || !self.is_focused(cx)
+                || buffer.read(cx).is_empty())
         {
             self.discard_inline_completion(false, cx);
             return None;

crates/gpui/src/elements/text.rs 🔗

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

crates/zed/src/zed/inline_completion_registry.rs 🔗

@@ -165,7 +165,9 @@ fn assign_inline_completion_provider(
             }
         }
         language::language_settings::InlineCompletionProvider::Zeta => {
-            if cx.has_flag::<ZetaFeatureFlag>() || cfg!(debug_assertions) {
+            if cx.has_flag::<ZetaFeatureFlag>()
+                || (cfg!(debug_assertions) && client.status().borrow().is_connected())
+            {
                 let zeta = zeta::Zeta::register(client.clone(), cx);
                 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
                     if buffer.read(cx).file().is_some() {

crates/zeta/src/completion_diff_element.rs 🔗

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

crates/zeta/src/rate_completion_modal.rs 🔗

@@ -1,13 +1,11 @@
-use crate::{InlineCompletion, InlineCompletionRating, Zeta};
+use crate::{CompletionDiffElement, InlineCompletion, InlineCompletionRating, Zeta};
 use editor::Editor;
 use gpui::{
-    actions, prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
-    HighlightStyle, Model, StyledText, TextStyle, View, ViewContext,
+    actions, prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
+    View, ViewContext,
 };
-use language::{language_settings, OffsetRangeExt};
-use settings::Settings;
+use language::language_settings;
 use std::time::Duration;
-use theme::ThemeSettings;
 use ui::{prelude::*, KeyBinding, List, ListItem, ListItemSpacing, Tooltip};
 use workspace::{ModalView, Workspace};
 
@@ -73,7 +71,7 @@ impl RateCompletionModal {
         self.selected_index += 1;
         self.selected_index = usize::min(
             self.selected_index,
-            self.zeta.read(cx).recent_completions().count(),
+            self.zeta.read(cx).shown_completions().count(),
         );
         cx.notify();
     }
@@ -87,7 +85,7 @@ impl RateCompletionModal {
         let next_index = self
             .zeta
             .read(cx)
-            .recent_completions()
+            .shown_completions()
             .skip(self.selected_index)
             .enumerate()
             .skip(1) // Skip straight to the next item
@@ -102,12 +100,12 @@ impl RateCompletionModal {
 
     fn select_prev_edit(&mut self, _: &PreviousEdit, cx: &mut ViewContext<Self>) {
         let zeta = self.zeta.read(cx);
-        let completions_len = zeta.recent_completions_len();
+        let completions_len = zeta.shown_completions_len();
 
         let prev_index = self
             .zeta
             .read(cx)
-            .recent_completions()
+            .shown_completions()
             .rev()
             .skip((completions_len - 1) - self.selected_index)
             .enumerate()
@@ -128,11 +126,11 @@ impl RateCompletionModal {
     }
 
     fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
-        self.selected_index = self.zeta.read(cx).recent_completions_len() - 1;
+        self.selected_index = self.zeta.read(cx).shown_completions_len() - 1;
         cx.notify();
     }
 
-    fn thumbs_up_active(&mut self, _: &ThumbsUpActiveCompletion, cx: &mut ViewContext<Self>) {
+    pub fn thumbs_up_active(&mut self, _: &ThumbsUpActiveCompletion, cx: &mut ViewContext<Self>) {
         self.zeta.update(cx, |zeta, cx| {
             if let Some(active) = &self.active_completion {
                 zeta.rate_completion(
@@ -155,7 +153,11 @@ impl RateCompletionModal {
         cx.notify();
     }
 
-    fn thumbs_down_active(&mut self, _: &ThumbsDownActiveCompletion, cx: &mut ViewContext<Self>) {
+    pub fn thumbs_down_active(
+        &mut self,
+        _: &ThumbsDownActiveCompletion,
+        cx: &mut ViewContext<Self>,
+    ) {
         if let Some(active) = &self.active_completion {
             if active.feedback_editor.read(cx).text(cx).is_empty() {
                 return;
@@ -191,7 +193,7 @@ impl RateCompletionModal {
         let completion = self
             .zeta
             .read(cx)
-            .recent_completions()
+            .shown_completions()
             .skip(self.selected_index)
             .take(1)
             .next()
@@ -204,7 +206,7 @@ impl RateCompletionModal {
         let completion = self
             .zeta
             .read(cx)
-            .recent_completions()
+            .shown_completions()
             .skip(self.selected_index)
             .take(1)
             .next()
@@ -224,7 +226,7 @@ impl RateCompletionModal {
             self.selected_index = self
                 .zeta
                 .read(cx)
-                .recent_completions()
+                .shown_completions()
                 .enumerate()
                 .find(|(_, completion_b)| completion.id == completion_b.id)
                 .map(|(ix, _)| ix)
@@ -269,64 +271,10 @@ impl RateCompletionModal {
         let completion_id = active_completion.completion.id;
         let focus_handle = &self.focus_handle(cx);
 
-        let mut diff = active_completion
-            .completion
-            .snapshot
-            .text_for_range(active_completion.completion.excerpt_range.clone())
-            .collect::<String>();
-
-        let mut delta = 0;
-        let mut diff_highlights = Vec::new();
-        for (old_range, new_text) in active_completion.completion.edits.iter() {
-            let old_range = old_range.to_offset(&active_completion.completion.snapshot);
-            let old_start_in_text =
-                old_range.start - active_completion.completion.excerpt_range.start + delta;
-            let old_end_in_text =
-                old_range.end - active_completion.completion.excerpt_range.start + delta;
-            if old_start_in_text < old_end_in_text {
-                diff_highlights.push((
-                    old_start_in_text..old_end_in_text,
-                    HighlightStyle {
-                        background_color: Some(cx.theme().status().deleted_background),
-                        strikethrough: Some(gpui::StrikethroughStyle {
-                            thickness: px(1.),
-                            color: Some(cx.theme().colors().text_muted),
-                        }),
-                        ..Default::default()
-                    },
-                ));
-            }
-
-            if !new_text.is_empty() {
-                diff.insert_str(old_end_in_text, new_text);
-                diff_highlights.push((
-                    old_end_in_text..old_end_in_text + new_text.len(),
-                    HighlightStyle {
-                        background_color: Some(cx.theme().status().created_background),
-                        ..Default::default()
-                    },
-                ));
-                delta += new_text.len();
-            }
-        }
-
-        let settings = ThemeSettings::get_global(cx).clone();
-        let text_style = TextStyle {
-            color: cx.theme().colors().editor_foreground,
-            font_size: settings.buffer_font_size(cx).into(),
-            font_family: settings.buffer_font.family,
-            font_features: settings.buffer_font.features,
-            font_fallbacks: settings.buffer_font.fallbacks,
-            line_height: relative(settings.buffer_line_height.value()),
-            font_weight: settings.buffer_font.weight,
-            font_style: settings.buffer_font.style,
-            ..Default::default()
-        };
         let border_color = cx.theme().colors().border;
         let bg_color = cx.theme().colors().editor_background;
 
         let rated = self.zeta.read(cx).is_completion_rated(completion_id);
-        let was_shown = self.zeta.read(cx).was_completion_shown(completion_id);
         let feedback_empty = active_completion
             .feedback_editor
             .read(cx)
@@ -347,7 +295,8 @@ impl RateCompletionModal {
                         .size_full()
                         .bg(bg_color)
                         .overflow_scroll()
-                        .child(StyledText::new(diff).with_highlights(&text_style, diff_highlights)),
+                        .whitespace_nowrap()
+                        .child(CompletionDiffElement::new(&active_completion.completion, cx)),
                 )
                 .when_some((!rated).then(|| ()), |this, _| {
                     this.child(
@@ -413,16 +362,6 @@ impl RateCompletionModal {
                                     )
                                     .child(Label::new("No edits produced.").color(Color::Muted)),
                             )
-                        } else if !was_shown {
-                            Some(
-                                label_container()
-                                    .child(
-                                        Icon::new(IconName::Warning)
-                                            .size(IconSize::Small)
-                                            .color(Color::Warning),
-                                    )
-                                    .child(Label::new("Completion wasn't shown because another valid one was already on screen.")),
-                            )
                         } else {
                             Some(label_container())
                         })
@@ -541,7 +480,7 @@ impl Render for RateCompletionModal {
                                             )
                                             .into_any_element(),
                                     )
-                                    .children(self.zeta.read(cx).recent_completions().cloned().enumerate().map(
+                                    .children(self.zeta.read(cx).shown_completions().cloned().enumerate().map(
                                         |(index, completion)| {
                                             let selected =
                                                 self.active_completion.as_ref().map_or(false, |selected| {

crates/zeta/src/zeta.rs 🔗

@@ -1,5 +1,7 @@
+mod completion_diff_element;
 mod rate_completion_modal;
 
+pub(crate) use completion_diff_element::*;
 pub use rate_completion_modal::*;
 
 use anyhow::{anyhow, Context as _, Result};
@@ -72,6 +74,7 @@ pub struct InlineCompletion {
     id: InlineCompletionId,
     path: Arc<Path>,
     excerpt_range: Range<usize>,
+    cursor_offset: usize,
     edits: Arc<[(Range<Anchor>, String)]>,
     snapshot: BufferSnapshot,
     input_outline: Arc<str>,
@@ -155,9 +158,8 @@ pub struct Zeta {
     client: Arc<Client>,
     events: VecDeque<Event>,
     registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
-    recent_completions: VecDeque<InlineCompletion>,
+    shown_completions: VecDeque<InlineCompletion>,
     rated_completions: HashSet<InlineCompletionId>,
-    shown_completions: HashSet<InlineCompletionId>,
     llm_token: LlmApiToken,
     _llm_token_subscription: Subscription,
 }
@@ -185,9 +187,8 @@ impl Zeta {
         Self {
             client,
             events: VecDeque::new(),
-            recent_completions: VecDeque::new(),
+            shown_completions: VecDeque::new(),
             rated_completions: HashSet::default(),
-            shown_completions: HashSet::default(),
             registered_buffers: HashMap::default(),
             llm_token: LlmApiToken::default(),
             _llm_token_subscription: cx.subscribe(
@@ -298,7 +299,7 @@ impl Zeta {
         let client = self.client.clone();
         let llm_token = self.llm_token.clone();
 
-        cx.spawn(|this, mut cx| async move {
+        cx.spawn(|_, cx| async move {
             let request_sent_at = Instant::now();
 
             let (input_events, input_excerpt, input_outline) = cx
@@ -337,10 +338,11 @@ impl Zeta {
             let output_excerpt = response.output_excerpt;
             log::debug!("completion response: {}", output_excerpt);
 
-            let inline_completion = Self::process_completion_response(
+            Self::process_completion_response(
                 output_excerpt,
                 &snapshot,
                 excerpt_range,
+                offset,
                 path,
                 input_outline,
                 input_events,
@@ -348,20 +350,7 @@ impl Zeta {
                 request_sent_at,
                 &cx,
             )
-            .await?;
-
-            this.update(&mut cx, |this, cx| {
-                this.recent_completions
-                    .push_front(inline_completion.clone());
-                if this.recent_completions.len() > 50 {
-                    let completion = this.recent_completions.pop_back().unwrap();
-                    this.shown_completions.remove(&completion.id);
-                    this.rated_completions.remove(&completion.id);
-                }
-                cx.notify();
-            })?;
-
-            Ok(inline_completion)
+            .await
         })
     }
 
@@ -494,8 +483,8 @@ and then another
             }
 
             zeta.update(&mut cx, |zeta, _cx| {
-                zeta.recent_completions.get_mut(2).unwrap().edits = Arc::new([]);
-                zeta.recent_completions.get_mut(3).unwrap().edits = Arc::new([]);
+                zeta.shown_completions.get_mut(2).unwrap().edits = Arc::new([]);
+                zeta.shown_completions.get_mut(3).unwrap().edits = Arc::new([]);
             })
             .ok();
         })
@@ -578,6 +567,7 @@ and then another
         output_excerpt: String,
         snapshot: &BufferSnapshot,
         excerpt_range: Range<usize>,
+        cursor_offset: usize,
         path: Arc<Path>,
         input_outline: String,
         input_events: String,
@@ -637,6 +627,7 @@ and then another
                 id: InlineCompletionId::new(),
                 path,
                 excerpt_range,
+                cursor_offset,
                 edits: edits.into(),
                 snapshot: snapshot.clone(),
                 input_outline: input_outline.into(),
@@ -719,12 +710,13 @@ and then another
         self.rated_completions.contains(&completion_id)
     }
 
-    pub fn was_completion_shown(&self, completion_id: InlineCompletionId) -> bool {
-        self.shown_completions.contains(&completion_id)
-    }
-
-    pub fn completion_shown(&mut self, completion_id: InlineCompletionId) {
-        self.shown_completions.insert(completion_id);
+    pub fn completion_shown(&mut self, completion: &InlineCompletion, cx: &mut ModelContext<Self>) {
+        self.shown_completions.push_front(completion.clone());
+        if self.shown_completions.len() > 50 {
+            let completion = self.shown_completions.pop_back().unwrap();
+            self.rated_completions.remove(&completion.id);
+        }
+        cx.notify();
     }
 
     pub fn rate_completion(
@@ -748,12 +740,12 @@ and then another
         cx.notify();
     }
 
-    pub fn recent_completions(&self) -> impl DoubleEndedIterator<Item = &InlineCompletion> {
-        self.recent_completions.iter()
+    pub fn shown_completions(&self) -> impl DoubleEndedIterator<Item = &InlineCompletion> {
+        self.shown_completions.iter()
     }
 
-    pub fn recent_completions_len(&self) -> usize {
-        self.recent_completions.len()
+    pub fn shown_completions_len(&self) -> usize {
+        self.shown_completions.len()
     }
 
     fn report_changes_for_buffer(
@@ -1077,14 +1069,14 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
                     if let Some(old_completion) = this.current_completion.as_ref() {
                         let snapshot = buffer.read(cx).snapshot();
                         if new_completion.should_replace_completion(&old_completion, &snapshot) {
-                            this.zeta.update(cx, |zeta, _cx| {
-                                zeta.completion_shown(new_completion.completion.id)
+                            this.zeta.update(cx, |zeta, cx| {
+                                zeta.completion_shown(&new_completion.completion, cx);
                             });
                             this.current_completion = Some(new_completion);
                         }
                     } else {
-                        this.zeta.update(cx, |zeta, _cx| {
-                            zeta.completion_shown(new_completion.completion.id)
+                        this.zeta.update(cx, |zeta, cx| {
+                            zeta.completion_shown(&new_completion.completion, cx);
                         });
                         this.current_completion = Some(new_completion);
                     }
@@ -1217,6 +1209,7 @@ mod tests {
             snapshot: buffer.read(cx).snapshot(),
             id: InlineCompletionId::new(),
             excerpt_range: 0..0,
+            cursor_offset: 0,
             input_outline: "".into(),
             input_events: "".into(),
             input_excerpt: "".into(),