diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0ca6796b37..71224c3366 100644 --- a/crates/editor/src/editor.rs +++ b/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; diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 489a798014..07f51be4bf 100644 --- a/crates/gpui/src/elements/text.rs +++ b/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> { + 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 { self.0.lock().as_ref().unwrap().bounds.unwrap() diff --git a/crates/zed/src/zed/inline_completion_registry.rs b/crates/zed/src/zed/inline_completion_registry.rs index a2a59dd45c..888e3b475a 100644 --- a/crates/zed/src/zed/inline_completion_registry.rs +++ b/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::() || cfg!(debug_assertions) { + if cx.has_flag::() + || (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() { diff --git a/crates/zeta/src/completion_diff_element.rs b/crates/zeta/src/completion_diff_element.rs new file mode 100644 index 0000000000..55ed820394 --- /dev/null +++ b/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::(); + + 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 { + 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, + _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, + _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(), + )); + } + } +} diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index c733f684bd..75e1e10b3e 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/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) { 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.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) { + pub fn thumbs_up_active(&mut self, _: &ThumbsUpActiveCompletion, cx: &mut ViewContext) { 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) { + pub fn thumbs_down_active( + &mut self, + _: &ThumbsDownActiveCompletion, + cx: &mut ViewContext, + ) { 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::(); - - 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| { diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 9c794dc7fc..7c74d1f716 100644 --- a/crates/zeta/src/zeta.rs +++ b/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, excerpt_range: Range, + cursor_offset: usize, edits: Arc<[(Range, String)]>, snapshot: BufferSnapshot, input_outline: Arc, @@ -155,9 +158,8 @@ pub struct Zeta { client: Arc, events: VecDeque, registered_buffers: HashMap, - recent_completions: VecDeque, + shown_completions: VecDeque, rated_completions: HashSet, - shown_completions: HashSet, 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, + cursor_offset: usize, path: Arc, 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.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 { - self.recent_completions.iter() + pub fn shown_completions(&self) -> impl DoubleEndedIterator { + 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(),