completions: Add subtle/eager behavior to Supermaven and Copilot (#35548)
This pull request introduces changes to improve the behavior and consistency of multiple completion providers (`CopilotCompletionProvider`, `SupermavenCompletionProvider`) and their integration with UI elements like menus and inline completion buttons. It now allows to see the prediction with the completion menu open whilst pressing `opt` and also enables the subtle/eager setting that was introduced with zeta. Edit: I managed to get the preview working with correct icons! <img width="909" height="232" alt="image" src="https://github.com/user-attachments/assets/65800e67-4bc4-40f8-be78-806fcfe74ad9" /> <img width="1460" height="318" alt="CleanShot 2025-08-04 at 01 36 31@2x" src="https://github.com/user-attachments/assets/15651405-720f-465f-a13c-c7470817810a" /> Correct icons are also displayed: <img width="244" height="96" alt="image" src="https://github.com/user-attachments/assets/0b8a687f-73e3-452d-aefb-784c52831b73" /> Edit2: I added some comments, would be very happy to receive feedback (still learning rust) Release Notes: - Added Subtle and Eager edit prediction modes to Copilot and Supermaven
This commit is contained in:
parent
dd840e4b27
commit
2234220618
7 changed files with 372 additions and 63 deletions
|
@ -58,11 +58,19 @@ impl EditPredictionProvider for CopilotCompletionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_completions_in_menu() -> bool {
|
fn show_completions_in_menu() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_tab_accept_marker() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_jump_to_edit() -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_refreshing(&self) -> bool {
|
fn is_refreshing(&self) -> bool {
|
||||||
self.pending_refresh.is_some()
|
self.pending_refresh.is_some() && self.completions.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_enabled(
|
fn is_enabled(
|
||||||
|
@ -343,8 +351,8 @@ mod tests {
|
||||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
assert!(editor.context_menu_visible());
|
assert!(editor.context_menu_visible());
|
||||||
assert!(!editor.has_active_edit_prediction());
|
assert!(editor.has_active_edit_prediction());
|
||||||
// Since we have both, the copilot suggestion is not shown inline
|
// Since we have both, the copilot suggestion is existing but does not show up as ghost text
|
||||||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||||
assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n");
|
assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n");
|
||||||
|
|
||||||
|
@ -934,8 +942,9 @@ mod tests {
|
||||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||||
cx.update_editor(|editor, _, cx| {
|
cx.update_editor(|editor, _, cx| {
|
||||||
assert!(editor.context_menu_visible());
|
assert!(editor.context_menu_visible());
|
||||||
assert!(!editor.has_active_edit_prediction(),);
|
assert!(editor.has_active_edit_prediction());
|
||||||
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
|
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
|
||||||
|
assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1077,8 +1086,6 @@ mod tests {
|
||||||
vec![complete_from_marker.clone(), replace_range_marker.clone()],
|
vec![complete_from_marker.clone(), replace_range_marker.clone()],
|
||||||
);
|
);
|
||||||
|
|
||||||
let complete_from_position =
|
|
||||||
cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
|
|
||||||
let replace_range =
|
let replace_range =
|
||||||
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
|
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
|
||||||
|
|
||||||
|
@ -1087,10 +1094,6 @@ mod tests {
|
||||||
let completions = completions.clone();
|
let completions = completions.clone();
|
||||||
async move {
|
async move {
|
||||||
assert_eq!(params.text_document_position.text_document.uri, url.clone());
|
assert_eq!(params.text_document_position.text_document.uri, url.clone());
|
||||||
assert_eq!(
|
|
||||||
params.text_document_position.position,
|
|
||||||
complete_from_position
|
|
||||||
);
|
|
||||||
Ok(Some(lsp::CompletionResponse::Array(
|
Ok(Some(lsp::CompletionResponse::Array(
|
||||||
completions
|
completions
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
@ -61,6 +61,10 @@ pub trait EditPredictionProvider: 'static + Sized {
|
||||||
fn show_tab_accept_marker() -> bool {
|
fn show_tab_accept_marker() -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
fn supports_jump_to_edit() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn data_collection_state(&self, _cx: &App) -> DataCollectionState {
|
fn data_collection_state(&self, _cx: &App) -> DataCollectionState {
|
||||||
DataCollectionState::Unsupported
|
DataCollectionState::Unsupported
|
||||||
}
|
}
|
||||||
|
@ -116,6 +120,7 @@ pub trait EditPredictionProviderHandle {
|
||||||
) -> bool;
|
) -> bool;
|
||||||
fn show_completions_in_menu(&self) -> bool;
|
fn show_completions_in_menu(&self) -> bool;
|
||||||
fn show_tab_accept_marker(&self) -> bool;
|
fn show_tab_accept_marker(&self) -> bool;
|
||||||
|
fn supports_jump_to_edit(&self) -> bool;
|
||||||
fn data_collection_state(&self, cx: &App) -> DataCollectionState;
|
fn data_collection_state(&self, cx: &App) -> DataCollectionState;
|
||||||
fn usage(&self, cx: &App) -> Option<EditPredictionUsage>;
|
fn usage(&self, cx: &App) -> Option<EditPredictionUsage>;
|
||||||
fn toggle_data_collection(&self, cx: &mut App);
|
fn toggle_data_collection(&self, cx: &mut App);
|
||||||
|
@ -166,6 +171,10 @@ where
|
||||||
T::show_tab_accept_marker()
|
T::show_tab_accept_marker()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn supports_jump_to_edit(&self) -> bool {
|
||||||
|
T::supports_jump_to_edit()
|
||||||
|
}
|
||||||
|
|
||||||
fn data_collection_state(&self, cx: &App) -> DataCollectionState {
|
fn data_collection_state(&self, cx: &App) -> DataCollectionState {
|
||||||
self.read(cx).data_collection_state(cx)
|
self.read(cx).data_collection_state(cx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -491,7 +491,12 @@ impl EditPredictionButton {
|
||||||
let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle);
|
let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle);
|
||||||
let eager_mode = matches!(current_mode, EditPredictionsMode::Eager);
|
let eager_mode = matches!(current_mode, EditPredictionsMode::Eager);
|
||||||
|
|
||||||
if matches!(provider, EditPredictionProvider::Zed) {
|
if matches!(
|
||||||
|
provider,
|
||||||
|
EditPredictionProvider::Zed
|
||||||
|
| EditPredictionProvider::Copilot
|
||||||
|
| EditPredictionProvider::Supermaven
|
||||||
|
) {
|
||||||
menu = menu
|
menu = menu
|
||||||
.separator()
|
.separator()
|
||||||
.header("Display Modes")
|
.header("Display Modes")
|
||||||
|
|
|
@ -228,6 +228,49 @@ async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test(cx, |_| {});
|
||||||
|
|
||||||
|
let mut cx = EditorTestContext::new(cx).await;
|
||||||
|
let provider = cx.new(|_| FakeNonZedEditPredictionProvider::default());
|
||||||
|
assign_editor_completion_provider_non_zed(provider.clone(), &mut cx);
|
||||||
|
|
||||||
|
// Cursor is 2+ lines above the proposed edit
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
line 0
|
||||||
|
line ˇ1
|
||||||
|
line 2
|
||||||
|
line 3
|
||||||
|
line
|
||||||
|
"});
|
||||||
|
|
||||||
|
propose_edits_non_zed(
|
||||||
|
&provider,
|
||||||
|
vec![(Point::new(4, 3)..Point::new(4, 3), " 4")],
|
||||||
|
&mut cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
|
||||||
|
|
||||||
|
// For non-Zed providers, there should be no move completion (jump functionality disabled)
|
||||||
|
cx.editor(|editor, _, _| {
|
||||||
|
if let Some(completion_state) = &editor.active_edit_prediction {
|
||||||
|
// Should be an Edit prediction, not a Move prediction
|
||||||
|
match &completion_state.completion {
|
||||||
|
EditPrediction::Edit { .. } => {
|
||||||
|
// This is expected for non-Zed providers
|
||||||
|
}
|
||||||
|
EditPrediction::Move { .. } => {
|
||||||
|
panic!(
|
||||||
|
"Non-Zed providers should not show Move predictions (jump functionality)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn assert_editor_active_edit_completion(
|
fn assert_editor_active_edit_completion(
|
||||||
cx: &mut EditorTestContext,
|
cx: &mut EditorTestContext,
|
||||||
assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),
|
assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),
|
||||||
|
@ -301,6 +344,37 @@ fn assign_editor_completion_provider(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn propose_edits_non_zed<T: ToOffset>(
|
||||||
|
provider: &Entity<FakeNonZedEditPredictionProvider>,
|
||||||
|
edits: Vec<(Range<T>, &str)>,
|
||||||
|
cx: &mut EditorTestContext,
|
||||||
|
) {
|
||||||
|
let snapshot = cx.buffer_snapshot();
|
||||||
|
let edits = edits.into_iter().map(|(range, text)| {
|
||||||
|
let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end);
|
||||||
|
(range, text.into())
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.update(|_, cx| {
|
||||||
|
provider.update(cx, |provider, _| {
|
||||||
|
provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
|
||||||
|
id: None,
|
||||||
|
edits: edits.collect(),
|
||||||
|
edit_preview: None,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assign_editor_completion_provider_non_zed(
|
||||||
|
provider: Entity<FakeNonZedEditPredictionProvider>,
|
||||||
|
cx: &mut EditorTestContext,
|
||||||
|
) {
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.set_edit_prediction_provider(Some(provider), window, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub struct FakeEditPredictionProvider {
|
pub struct FakeEditPredictionProvider {
|
||||||
pub completion: Option<edit_prediction::EditPrediction>,
|
pub completion: Option<edit_prediction::EditPrediction>,
|
||||||
|
@ -325,6 +399,84 @@ impl EditPredictionProvider for FakeEditPredictionProvider {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn supports_jump_to_edit() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_enabled(
|
||||||
|
&self,
|
||||||
|
_buffer: &gpui::Entity<language::Buffer>,
|
||||||
|
_cursor_position: language::Anchor,
|
||||||
|
_cx: &gpui::App,
|
||||||
|
) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_refreshing(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh(
|
||||||
|
&mut self,
|
||||||
|
_project: Option<Entity<Project>>,
|
||||||
|
_buffer: gpui::Entity<language::Buffer>,
|
||||||
|
_cursor_position: language::Anchor,
|
||||||
|
_debounce: bool,
|
||||||
|
_cx: &mut gpui::Context<Self>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cycle(
|
||||||
|
&mut self,
|
||||||
|
_buffer: gpui::Entity<language::Buffer>,
|
||||||
|
_cursor_position: language::Anchor,
|
||||||
|
_direction: edit_prediction::Direction,
|
||||||
|
_cx: &mut gpui::Context<Self>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||||
|
|
||||||
|
fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||||
|
|
||||||
|
fn suggest<'a>(
|
||||||
|
&mut self,
|
||||||
|
_buffer: &gpui::Entity<language::Buffer>,
|
||||||
|
_cursor_position: language::Anchor,
|
||||||
|
_cx: &mut gpui::Context<Self>,
|
||||||
|
) -> Option<edit_prediction::EditPrediction> {
|
||||||
|
self.completion.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct FakeNonZedEditPredictionProvider {
|
||||||
|
pub completion: Option<edit_prediction::EditPrediction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FakeNonZedEditPredictionProvider {
|
||||||
|
pub fn set_edit_prediction(&mut self, completion: Option<edit_prediction::EditPrediction>) {
|
||||||
|
self.completion = completion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditPredictionProvider for FakeNonZedEditPredictionProvider {
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"fake-non-zed-provider"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_name() -> &'static str {
|
||||||
|
"Fake Non-Zed Provider"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_completions_in_menu() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_jump_to_edit() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn is_enabled(
|
fn is_enabled(
|
||||||
&self,
|
&self,
|
||||||
_buffer: &gpui::Entity<language::Buffer>,
|
_buffer: &gpui::Entity<language::Buffer>,
|
||||||
|
|
|
@ -7760,8 +7760,14 @@ impl Editor {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let is_move =
|
let supports_jump = self
|
||||||
move_invalidation_row_range.is_some() || self.edit_predictions_hidden_for_vim_mode;
|
.edit_prediction_provider
|
||||||
|
.as_ref()
|
||||||
|
.map(|provider| provider.provider.supports_jump_to_edit())
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
let is_move = supports_jump
|
||||||
|
&& (move_invalidation_row_range.is_some() || self.edit_predictions_hidden_for_vim_mode);
|
||||||
let completion = if is_move {
|
let completion = if is_move {
|
||||||
invalidation_row_range =
|
invalidation_row_range =
|
||||||
move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row);
|
move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row);
|
||||||
|
@ -8799,8 +8805,12 @@ impl Editor {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let highlighted_edits =
|
let highlighted_edits = if let Some(edit_preview) = edit_preview.as_ref() {
|
||||||
crate::edit_prediction_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx);
|
crate::edit_prediction_edit_text(&snapshot, edits, edit_preview, false, cx)
|
||||||
|
} else {
|
||||||
|
// Fallback for providers without edit_preview
|
||||||
|
crate::edit_prediction_fallback_text(edits, cx)
|
||||||
|
};
|
||||||
|
|
||||||
let styled_text = highlighted_edits.to_styled_text(&style.text);
|
let styled_text = highlighted_edits.to_styled_text(&style.text);
|
||||||
let line_count = highlighted_edits.text.lines().count();
|
let line_count = highlighted_edits.text.lines().count();
|
||||||
|
@ -9068,6 +9078,18 @@ impl Editor {
|
||||||
let editor_bg_color = cx.theme().colors().editor_background;
|
let editor_bg_color = cx.theme().colors().editor_background;
|
||||||
editor_bg_color.blend(accent_color.opacity(0.6))
|
editor_bg_color.blend(accent_color.opacity(0.6))
|
||||||
}
|
}
|
||||||
|
fn get_prediction_provider_icon_name(
|
||||||
|
provider: &Option<RegisteredEditPredictionProvider>,
|
||||||
|
) -> IconName {
|
||||||
|
match provider {
|
||||||
|
Some(provider) => match provider.provider.name() {
|
||||||
|
"copilot" => IconName::Copilot,
|
||||||
|
"supermaven" => IconName::Supermaven,
|
||||||
|
_ => IconName::ZedPredict,
|
||||||
|
},
|
||||||
|
None => IconName::ZedPredict,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn render_edit_prediction_cursor_popover(
|
fn render_edit_prediction_cursor_popover(
|
||||||
&self,
|
&self,
|
||||||
|
@ -9080,6 +9102,7 @@ impl Editor {
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
let provider = self.edit_prediction_provider.as_ref()?;
|
let provider = self.edit_prediction_provider.as_ref()?;
|
||||||
|
let provider_icon = Self::get_prediction_provider_icon_name(&self.edit_prediction_provider);
|
||||||
|
|
||||||
if provider.provider.needs_terms_acceptance(cx) {
|
if provider.provider.needs_terms_acceptance(cx) {
|
||||||
return Some(
|
return Some(
|
||||||
|
@ -9106,7 +9129,7 @@ impl Editor {
|
||||||
h_flex()
|
h_flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(Icon::new(IconName::ZedPredict))
|
.child(Icon::new(provider_icon))
|
||||||
.child(Label::new("Accept Terms of Service"))
|
.child(Label::new("Accept Terms of Service"))
|
||||||
.child(div().w_full())
|
.child(div().w_full())
|
||||||
.child(
|
.child(
|
||||||
|
@ -9122,12 +9145,8 @@ impl Editor {
|
||||||
|
|
||||||
let is_refreshing = provider.provider.is_refreshing(cx);
|
let is_refreshing = provider.provider.is_refreshing(cx);
|
||||||
|
|
||||||
fn pending_completion_container() -> Div {
|
fn pending_completion_container(icon: IconName) -> Div {
|
||||||
h_flex()
|
h_flex().h_full().flex_1().gap_2().child(Icon::new(icon))
|
||||||
.h_full()
|
|
||||||
.flex_1()
|
|
||||||
.gap_2()
|
|
||||||
.child(Icon::new(IconName::ZedPredict))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let completion = match &self.active_edit_prediction {
|
let completion = match &self.active_edit_prediction {
|
||||||
|
@ -9157,7 +9176,7 @@ impl Editor {
|
||||||
Icon::new(IconName::ZedPredictUp)
|
Icon::new(IconName::ZedPredictUp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EditPrediction::Edit { .. } => Icon::new(IconName::ZedPredict),
|
EditPrediction::Edit { .. } => Icon::new(provider_icon),
|
||||||
}))
|
}))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -9224,15 +9243,15 @@ impl Editor {
|
||||||
cx,
|
cx,
|
||||||
)?,
|
)?,
|
||||||
|
|
||||||
None => {
|
None => pending_completion_container(provider_icon)
|
||||||
pending_completion_container().child(Label::new("...").size(LabelSize::Small))
|
.child(Label::new("...").size(LabelSize::Small)),
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
None => pending_completion_container().child(Label::new("No Prediction")),
|
None => pending_completion_container(provider_icon)
|
||||||
|
.child(Label::new("...").size(LabelSize::Small)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let completion = if is_refreshing {
|
let completion = if is_refreshing || self.active_edit_prediction.is_none() {
|
||||||
completion
|
completion
|
||||||
.with_animation(
|
.with_animation(
|
||||||
"loading-completion",
|
"loading-completion",
|
||||||
|
@ -9332,23 +9351,35 @@ impl Editor {
|
||||||
.child(Icon::new(arrow).color(Color::Muted).size(IconSize::Small))
|
.child(Icon::new(arrow).color(Color::Muted).size(IconSize::Small))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let supports_jump = self
|
||||||
|
.edit_prediction_provider
|
||||||
|
.as_ref()
|
||||||
|
.map(|provider| provider.provider.supports_jump_to_edit())
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
match &completion.completion {
|
match &completion.completion {
|
||||||
EditPrediction::Move {
|
EditPrediction::Move {
|
||||||
target, snapshot, ..
|
target, snapshot, ..
|
||||||
} => Some(
|
} => {
|
||||||
h_flex()
|
if !supports_jump {
|
||||||
.px_2()
|
return None;
|
||||||
.gap_2()
|
}
|
||||||
.flex_1()
|
|
||||||
.child(
|
Some(
|
||||||
if target.text_anchor.to_point(&snapshot).row > cursor_point.row {
|
h_flex()
|
||||||
Icon::new(IconName::ZedPredictDown)
|
.px_2()
|
||||||
} else {
|
.gap_2()
|
||||||
Icon::new(IconName::ZedPredictUp)
|
.flex_1()
|
||||||
},
|
.child(
|
||||||
)
|
if target.text_anchor.to_point(&snapshot).row > cursor_point.row {
|
||||||
.child(Label::new("Jump to Edit")),
|
Icon::new(IconName::ZedPredictDown)
|
||||||
),
|
} else {
|
||||||
|
Icon::new(IconName::ZedPredictUp)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.child(Label::new("Jump to Edit")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
EditPrediction::Edit {
|
EditPrediction::Edit {
|
||||||
edits,
|
edits,
|
||||||
|
@ -9358,14 +9389,13 @@ impl Editor {
|
||||||
} => {
|
} => {
|
||||||
let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row;
|
let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row;
|
||||||
|
|
||||||
let (highlighted_edits, has_more_lines) = crate::edit_prediction_edit_text(
|
let (highlighted_edits, has_more_lines) =
|
||||||
&snapshot,
|
if let Some(edit_preview) = edit_preview.as_ref() {
|
||||||
&edits,
|
crate::edit_prediction_edit_text(&snapshot, &edits, edit_preview, true, cx)
|
||||||
edit_preview.as_ref()?,
|
.first_line_preview()
|
||||||
true,
|
} else {
|
||||||
cx,
|
crate::edit_prediction_fallback_text(&edits, cx).first_line_preview()
|
||||||
)
|
};
|
||||||
.first_line_preview();
|
|
||||||
|
|
||||||
let styled_text = gpui::StyledText::new(highlighted_edits.text)
|
let styled_text = gpui::StyledText::new(highlighted_edits.text)
|
||||||
.with_default_highlights(&style.text, highlighted_edits.highlights);
|
.with_default_highlights(&style.text, highlighted_edits.highlights);
|
||||||
|
@ -9376,11 +9406,13 @@ impl Editor {
|
||||||
.child(styled_text)
|
.child(styled_text)
|
||||||
.when(has_more_lines, |parent| parent.child("…"));
|
.when(has_more_lines, |parent| parent.child("…"));
|
||||||
|
|
||||||
let left = if first_edit_row != cursor_point.row {
|
let left = if supports_jump && first_edit_row != cursor_point.row {
|
||||||
render_relative_row_jump("", cursor_point.row, first_edit_row)
|
render_relative_row_jump("", cursor_point.row, first_edit_row)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
Icon::new(IconName::ZedPredict).into_any_element()
|
let icon_name =
|
||||||
|
Editor::get_prediction_provider_icon_name(&self.edit_prediction_provider);
|
||||||
|
Icon::new(icon_name).into_any_element()
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
|
@ -23270,6 +23302,33 @@ fn edit_prediction_edit_text(
|
||||||
edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx)
|
edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn edit_prediction_fallback_text(edits: &[(Range<Anchor>, String)], cx: &App) -> HighlightedText {
|
||||||
|
// Fallback for providers that don't provide edit_preview (like Copilot/Supermaven)
|
||||||
|
// Just show the raw edit text with basic styling
|
||||||
|
let mut text = String::new();
|
||||||
|
let mut highlights = Vec::new();
|
||||||
|
|
||||||
|
let insertion_highlight_style = HighlightStyle {
|
||||||
|
color: Some(cx.theme().colors().text),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for (_, edit_text) in edits {
|
||||||
|
let start_offset = text.len();
|
||||||
|
text.push_str(edit_text);
|
||||||
|
let end_offset = text.len();
|
||||||
|
|
||||||
|
if start_offset < end_offset {
|
||||||
|
highlights.push((start_offset..end_offset, insertion_highlight_style));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HighlightedText {
|
||||||
|
text: text.into(),
|
||||||
|
highlights,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn diagnostic_style(severity: lsp::DiagnosticSeverity, colors: &StatusColors) -> Hsla {
|
pub fn diagnostic_style(severity: lsp::DiagnosticSeverity, colors: &StatusColors) -> Hsla {
|
||||||
match severity {
|
match severity {
|
||||||
lsp::DiagnosticSeverity::ERROR => colors.error,
|
lsp::DiagnosticSeverity::ERROR => colors.error,
|
||||||
|
|
|
@ -234,16 +234,14 @@ fn find_relevant_completion<'a>(
|
||||||
}
|
}
|
||||||
|
|
||||||
let original_cursor_offset = buffer.clip_offset(state.prefix_offset, text::Bias::Left);
|
let original_cursor_offset = buffer.clip_offset(state.prefix_offset, text::Bias::Left);
|
||||||
let text_inserted_since_completion_request =
|
let text_inserted_since_completion_request: String = buffer
|
||||||
buffer.text_for_range(original_cursor_offset..current_cursor_offset);
|
.text_for_range(original_cursor_offset..current_cursor_offset)
|
||||||
let mut trimmed_completion = state_completion;
|
.collect();
|
||||||
for chunk in text_inserted_since_completion_request {
|
let trimmed_completion =
|
||||||
if let Some(suffix) = trimmed_completion.strip_prefix(chunk) {
|
match state_completion.strip_prefix(&text_inserted_since_completion_request) {
|
||||||
trimmed_completion = suffix;
|
Some(suffix) => suffix,
|
||||||
} else {
|
None => continue 'completions,
|
||||||
continue 'completions;
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if best_completion.map_or(false, |best| best.len() > trimmed_completion.len()) {
|
if best_completion.map_or(false, |best| best.len() > trimmed_completion.len()) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -439,3 +437,77 @@ pub struct SupermavenCompletion {
|
||||||
pub id: SupermavenCompletionStateId,
|
pub id: SupermavenCompletionStateId,
|
||||||
pub updates: watch::Receiver<()>,
|
pub updates: watch::Receiver<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use collections::BTreeMap;
|
||||||
|
use gpui::TestAppContext;
|
||||||
|
use language::Buffer;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_find_relevant_completion_no_first_letter_skip(cx: &mut TestAppContext) {
|
||||||
|
let buffer = cx.new(|cx| Buffer::local("hello world", cx));
|
||||||
|
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||||
|
|
||||||
|
let mut states = BTreeMap::new();
|
||||||
|
let state_id = SupermavenCompletionStateId(1);
|
||||||
|
let (updates_tx, _) = watch::channel();
|
||||||
|
|
||||||
|
states.insert(
|
||||||
|
state_id,
|
||||||
|
SupermavenCompletionState {
|
||||||
|
buffer_id: buffer.entity_id(),
|
||||||
|
prefix_anchor: buffer_snapshot.anchor_before(0), // Start of buffer
|
||||||
|
prefix_offset: 0,
|
||||||
|
text: "hello".to_string(),
|
||||||
|
dedent: String::new(),
|
||||||
|
updates_tx,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let cursor_position = buffer_snapshot.anchor_after(1);
|
||||||
|
|
||||||
|
let result = find_relevant_completion(
|
||||||
|
&states,
|
||||||
|
buffer.entity_id(),
|
||||||
|
&buffer_snapshot,
|
||||||
|
cursor_position,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(result, Some("ello"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_find_relevant_completion_with_multiple_chars(cx: &mut TestAppContext) {
|
||||||
|
let buffer = cx.new(|cx| Buffer::local("hello world", cx));
|
||||||
|
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||||
|
|
||||||
|
let mut states = BTreeMap::new();
|
||||||
|
let state_id = SupermavenCompletionStateId(1);
|
||||||
|
let (updates_tx, _) = watch::channel();
|
||||||
|
|
||||||
|
states.insert(
|
||||||
|
state_id,
|
||||||
|
SupermavenCompletionState {
|
||||||
|
buffer_id: buffer.entity_id(),
|
||||||
|
prefix_anchor: buffer_snapshot.anchor_before(0), // Start of buffer
|
||||||
|
prefix_offset: 0,
|
||||||
|
text: "hello".to_string(),
|
||||||
|
dedent: String::new(),
|
||||||
|
updates_tx,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let cursor_position = buffer_snapshot.anchor_after(3);
|
||||||
|
|
||||||
|
let result = find_relevant_completion(
|
||||||
|
&states,
|
||||||
|
buffer.entity_id(),
|
||||||
|
&buffer_snapshot,
|
||||||
|
cursor_position,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(result, Some("lo"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -108,6 +108,14 @@ impl EditPredictionProvider for SupermavenCompletionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_completions_in_menu() -> bool {
|
fn show_completions_in_menu() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_tab_accept_marker() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_jump_to_edit() -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +124,7 @@ impl EditPredictionProvider for SupermavenCompletionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_refreshing(&self) -> bool {
|
fn is_refreshing(&self) -> bool {
|
||||||
self.pending_refresh.is_some()
|
self.pending_refresh.is_some() && self.completion_id.is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh(
|
fn refresh(
|
||||||
|
@ -197,6 +205,7 @@ impl EditPredictionProvider for SupermavenCompletionProvider {
|
||||||
let mut point = cursor_position.to_point(&snapshot);
|
let mut point = cursor_position.to_point(&snapshot);
|
||||||
point.column = snapshot.line_len(point.row);
|
point.column = snapshot.line_len(point.row);
|
||||||
let range = cursor_position..snapshot.anchor_after(point);
|
let range = cursor_position..snapshot.anchor_after(point);
|
||||||
|
|
||||||
Some(completion_from_diff(
|
Some(completion_from_diff(
|
||||||
snapshot,
|
snapshot,
|
||||||
completion_text,
|
completion_text,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue