diff --git a/crates/edit_prediction_ui/src/rate_prediction_modal.rs b/crates/edit_prediction_ui/src/rate_prediction_modal.rs index 65ce7ec8761541d886f272baa3934af58884524d..8299d8da2caf457f3b479bcaade5465656966f7d 100644 --- a/crates/edit_prediction_ui/src/rate_prediction_modal.rs +++ b/crates/edit_prediction_ui/src/rate_prediction_modal.rs @@ -6,12 +6,16 @@ use gpui::{ App, BorderStyle, DismissEvent, EdgesRefinement, Entity, EventEmitter, FocusHandle, Focusable, Length, StyleRefinement, TextStyleRefinement, Window, actions, prelude::*, }; -use language::{LanguageRegistry, Point, language_settings}; +use language::{Buffer, CodeLabel, LanguageRegistry, Point, ToOffset, language_settings}; use markdown::{Markdown, MarkdownStyle}; +use project::{Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource}; use settings::Settings as _; +use std::rc::Rc; use std::{fmt::Write, sync::Arc, time::Duration}; use theme::ThemeSettings; -use ui::{KeyBinding, List, ListItem, ListItemSpacing, Tooltip, prelude::*}; +use ui::{ + ContextMenu, DropdownMenu, KeyBinding, List, ListItem, ListItemSpacing, Tooltip, prelude::*, +}; use workspace::{ModalView, Workspace}; actions!( @@ -419,6 +423,7 @@ impl RatePredictionsModal { editor.set_show_indent_guides(false, cx); editor.set_show_edit_predictions(Some(false), window, cx); editor.set_placeholder_text("Add your feedback…", window, cx); + editor.set_completion_provider(Some(Rc::new(FeedbackCompletionProvider))); if focus { cx.focus_self(window); } @@ -613,6 +618,44 @@ impl RatePredictionsModal { ), ) .when(!rated, |this| { + let modal = cx.entity().downgrade(); + let failure_mode_menu = + ContextMenu::build(window, cx, move |menu, _window, _cx| { + FeedbackCompletionProvider::FAILURE_MODES + .iter() + .fold(menu, |menu, (_key, description)| { + let description: SharedString = (*description).into(); + let modal = modal.clone(); + menu.entry( + description.clone(), + None, + move |window, cx| { + if let Some(modal) = modal.upgrade() { + modal.update(cx, |this, cx| { + if let Some(active) = &this.active_prediction { + active.feedback_editor.update( + cx, + |editor, cx| { + editor.set_text( + description.clone(), + window, + cx, + ); + }, + ); + } + this.thumbs_down_active( + &ThumbsDownActivePrediction, + window, + cx, + ); + }); + } + }, + ) + }) + }); + this.child( h_flex() .p_2() @@ -620,19 +663,32 @@ impl RatePredictionsModal { .border_y_1() .border_color(border_color) .child( - Icon::new(IconName::Info) - .size(IconSize::XSmall) - .color(Color::Muted), + DropdownMenu::new( + "failure-mode-dropdown", + "Issue", + failure_mode_menu, + ) + .style(ui::DropdownStyle::Outlined) + .trigger_size(ButtonSize::Compact), ) .child( - div().w_full().pr_2().flex_wrap().child( - Label::new(concat!( - "Explain why this completion is good or bad. ", - "If it's negative, describe what you expected instead." - )) - .size(LabelSize::Small) - .color(Color::Muted), - ), + h_flex() + .gap_2() + .child( + Icon::new(IconName::Info) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child( + div().flex_wrap().child( + Label::new(concat!( + "Explain why this completion is good or bad. ", + "If it's negative, describe what you expected instead." + )) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ), ), ) }) @@ -910,3 +966,133 @@ fn format_time_ago(elapsed: Duration) -> String { format!("{} days", seconds / 86400) } } + +struct FeedbackCompletionProvider; + +impl FeedbackCompletionProvider { + const FAILURE_MODES: &'static [(&'static str, &'static str)] = &[ + ( + "bad_location", + "Made a prediction somewhere other than expected", + ), + ("incomplete", "Prediction was incomplete or cut off"), + ( + "deleted", + "Prediction deleted code that should have been kept. Prefer `reverted` if it reverted an edit", + ), + ( + "bad_style", + "Prediction used wrong coding style or conventions", + ), + ( + "repetitive", + "Prediction repeated existing code unnecessarily", + ), + ( + "hallucinated", + "Prediction referenced non-existent variables/functions", + ), + ("wrong_indent", "Prediction had incorrect indentation"), + ("syntax_error", "Introduced a syntax error"), + ( + "too_aggressive", + "Prediction made more changes than expected", + ), + ( + "too_conservative", + "Prediction was overly cautious/conservative", + ), + ( + "no_context", + "Misunderstood or did not use contextual information", + ), + ("reverted", "Reverted recent edits"), + ( + "bad_cursor_position", + "The prediction moved the cursor to an unhelpful position", + ), + ]; +} + +impl editor::CompletionProvider for FeedbackCompletionProvider { + fn completions( + &self, + _excerpt_id: editor::ExcerptId, + buffer: &Entity, + buffer_position: language::Anchor, + _trigger: editor::CompletionContext, + _window: &mut Window, + cx: &mut Context, + ) -> gpui::Task>> { + let buffer = buffer.read(cx); + let mut count_back = 0; + + for char in buffer.reversed_chars_at(buffer_position) { + if char.is_ascii_alphanumeric() || char == '_' { + count_back += 1; + } else { + break; + } + } + + let start_anchor = buffer.anchor_before( + buffer_position + .to_offset(&buffer) + .saturating_sub(count_back), + ); + + let replace_range = start_anchor..buffer_position; + let snapshot = buffer.text_snapshot(); + let query: String = snapshot.text_for_range(replace_range.clone()).collect(); + + if query.len() < 3 { + return gpui::Task::ready(Ok(vec![CompletionResponse { + completions: vec![], + display_options: CompletionDisplayOptions { + dynamic_width: true, + }, + is_incomplete: false, + }])); + } + + let query_lower = query.to_lowercase(); + + let completions: Vec = Self::FAILURE_MODES + .iter() + .filter(|(key, _description)| key.starts_with(&query_lower)) + .map(|(key, description)| Completion { + replace_range: replace_range.clone(), + new_text: description.to_string(), + label: CodeLabel::plain(format!("{}: {}", key, description), None), + documentation: None, + source: CompletionSource::Custom, + icon_path: None, + match_start: None, + snippet_deduplication_key: None, + insert_text_mode: None, + confirm: None, + }) + .collect(); + + gpui::Task::ready(Ok(vec![CompletionResponse { + completions, + display_options: CompletionDisplayOptions { + dynamic_width: true, + }, + is_incomplete: false, + }])) + } + + fn is_completion_trigger( + &self, + _buffer: &Entity, + _position: language::Anchor, + text: &str, + _trigger_in_words: bool, + _cx: &mut Context, + ) -> bool { + text.chars() + .last() + .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_') + } +}