Detailed changes
@@ -792,11 +792,11 @@
],
// When to show edit predictions previews in buffer.
// This setting takes two possible values:
- // 1. Display inline when holding modifier key (alt by default).
- // "mode": "auto"
- // 2. Display inline when there are no language server completions available.
+ // 1. Display inline when there are no language server completions available.
// "mode": "eager_preview"
- "mode": "auto"
+ // 2. Display inline when holding modifier key (alt by default).
+ // "mode": "auto"
+ "mode": "eager_preview"
},
// Settings specific to journaling
"journal": {
@@ -5648,6 +5648,77 @@ impl Editor {
}
}
+ fn render_edit_prediction_accept_keybind(&self, window: &mut Window, cx: &App) -> Option<Div> {
+ let accept_binding = self.accept_edit_prediction_keybind(window, cx);
+ let accept_keystroke = accept_binding.keystroke()?;
+ let colors = cx.theme().colors();
+ let accent_color = colors.text_accent;
+ let editor_bg_color = colors.editor_background;
+ let bg_color = editor_bg_color.blend(accent_color.opacity(0.1));
+
+ h_flex()
+ .px_0p5()
+ .gap_1()
+ .bg(bg_color)
+ .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
+ .text_size(TextSize::XSmall.rems(cx))
+ .when(!self.edit_prediction_preview_is_active(), |parent| {
+ parent.children(ui::render_modifiers(
+ &accept_keystroke.modifiers,
+ PlatformStyle::platform(),
+ Some(if accept_keystroke.modifiers == window.modifiers() {
+ Color::Accent
+ } else {
+ Color::Muted
+ }),
+ Some(IconSize::XSmall.rems().into()),
+ false,
+ ))
+ })
+ .child(accept_keystroke.key.clone())
+ .into()
+ }
+
+ fn render_edit_prediction_line_popover(
+ &self,
+ label: impl Into<SharedString>,
+ icon: Option<IconName>,
+ window: &mut Window,
+ cx: &App,
+ ) -> Option<Div> {
+ let bg_color = Self::edit_prediction_line_popover_bg_color(cx);
+
+ let padding_right = if icon.is_some() { px(4.) } else { px(8.) };
+
+ let result = h_flex()
+ .gap_1()
+ .border_1()
+ .rounded_lg()
+ .shadow_sm()
+ .bg(bg_color)
+ .border_color(cx.theme().colors().text_accent.opacity(0.4))
+ .py_0p5()
+ .pl_1()
+ .pr(padding_right)
+ .children(self.render_edit_prediction_accept_keybind(window, cx))
+ .child(Label::new(label).size(LabelSize::Small))
+ .when_some(icon, |element, icon| {
+ element.child(
+ div()
+ .mt(px(1.5))
+ .child(Icon::new(icon).size(IconSize::Small)),
+ )
+ });
+
+ Some(result)
+ }
+
+ fn edit_prediction_line_popover_bg_color(cx: &App) -> Hsla {
+ let accent_color = cx.theme().colors().text_accent;
+ let editor_bg_color = cx.theme().colors().editor_background;
+ editor_bg_color.blend(accent_color.opacity(0.1))
+ }
+
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_cursor_popover(
&self,
@@ -5788,18 +5859,26 @@ impl Editor {
.min_w(min_width)
.max_w(max_width)
.flex_1()
- .px_2()
.elevation_2(cx)
.border_color(cx.theme().colors().border)
- .child(div().py_1().overflow_hidden().child(completion))
+ .child(
+ div()
+ .flex_1()
+ .py_1()
+ .px_2()
+ .overflow_hidden()
+ .child(completion),
+ )
.child(
h_flex()
.h_full()
.border_l_1()
+ .rounded_r_lg()
.border_color(cx.theme().colors().border)
+ .bg(Self::edit_prediction_line_popover_bg_color(cx))
.gap_1()
.py_1()
- .pl_2()
+ .px_2()
.child(
h_flex()
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
@@ -14548,6 +14627,7 @@ impl Editor {
}
self.hide_context_menu(window, cx);
+ self.discard_inline_completion(false, cx);
cx.emit(EditorEvent::Blurred);
cx.notify();
}
@@ -3583,14 +3583,14 @@ impl EditorElement {
}
if target_display_point.row() < visible_row_range.start {
- let mut element = inline_completion_accept_indicator(
- "Scroll",
- Some(IconName::ArrowUp),
- editor,
- window,
- cx,
- )?
- .into_any();
+ let mut element = editor
+ .render_edit_prediction_line_popover(
+ "Scroll",
+ Some(IconName::ArrowUp),
+ window,
+ cx,
+ )?
+ .into_any();
element.layout_as_root(AvailableSpace::min_size(), window, cx);
@@ -3608,14 +3608,14 @@ impl EditorElement {
element.prepaint_at(origin, window, cx);
return Some(element);
} else if target_display_point.row() >= visible_row_range.end {
- let mut element = inline_completion_accept_indicator(
- "Scroll",
- Some(IconName::ArrowDown),
- editor,
- window,
- cx,
- )?
- .into_any();
+ let mut element = editor
+ .render_edit_prediction_line_popover(
+ "Scroll",
+ Some(IconName::ArrowDown),
+ window,
+ cx,
+ )?
+ .into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
@@ -3640,12 +3640,11 @@ impl EditorElement {
let mut element = v_flex()
.child(
- inline_completion_accept_indicator(
- "Jump", None, editor, window, cx,
- )?
- .rounded_br(px(0.))
- .rounded_tr(px(0.))
- .border_r_2(),
+ editor
+ .render_edit_prediction_line_popover("Jump", None, window, cx)?
+ .rounded_br(px(0.))
+ .rounded_tr(px(0.))
+ .border_r_2(),
)
.child(
div()
@@ -3680,28 +3679,30 @@ impl EditorElement {
}
if target_display_point.row().as_f32() < scroll_top {
- let mut element = inline_completion_accept_indicator(
- "Jump to Edit",
- Some(IconName::ArrowUp),
- editor,
- window,
- cx,
- )?
- .into_any();
+ let mut element = editor
+ .render_edit_prediction_line_popover(
+ "Jump to Edit",
+ Some(IconName::ArrowUp),
+ window,
+ cx,
+ )?
+ .into_any();
+
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let offset = point((text_bounds.size.width - size.width) / 2., PADDING_Y);
element.prepaint_at(text_bounds.origin + offset, window, cx);
Some(element)
} else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
- let mut element = inline_completion_accept_indicator(
- "Jump to Edit",
- Some(IconName::ArrowDown),
- editor,
- window,
- cx,
- )?
- .into_any();
+ let mut element = editor
+ .render_edit_prediction_line_popover(
+ "Jump to Edit",
+ Some(IconName::ArrowDown),
+ window,
+ cx,
+ )?
+ .into_any();
+
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let offset = point(
(text_bounds.size.width - size.width) / 2.,
@@ -3711,14 +3712,9 @@ impl EditorElement {
element.prepaint_at(text_bounds.origin + offset, window, cx);
Some(element)
} else {
- let mut element = inline_completion_accept_indicator(
- "Jump to Edit",
- None,
- editor,
- window,
- cx,
- )?
- .into_any();
+ let mut element = editor
+ .render_edit_prediction_line_popover("Jump to Edit", None, window, cx)?
+ .into_any();
let target_line_end = DisplayPoint::new(
target_display_point.row(),
editor_snapshot.line_len(target_display_point.row()),
@@ -3776,10 +3772,11 @@ impl EditorElement {
);
let (mut element, origin) = self.editor.update(cx, |editor, cx| {
Some((
- inline_completion_accept_indicator(
- "Accept", None, editor, window, cx,
- )?
- .into_any(),
+ editor
+ .render_edit_prediction_line_popover(
+ "Accept", None, window, cx,
+ )?
+ .into_any(),
editor.display_to_pixel_point(
target_line_end,
editor_snapshot,
@@ -3808,6 +3805,37 @@ impl EditorElement {
cx,
);
+ let styled_text = highlighted_edits.to_styled_text(&style.text);
+
+ const ACCEPT_INDICATOR_HEIGHT: Pixels = px(24.);
+
+ let mut element = v_flex()
+ .items_end()
+ .shadow_sm()
+ .child(
+ h_flex()
+ .h(ACCEPT_INDICATOR_HEIGHT)
+ .mb(px(-1.))
+ .px_1p5()
+ .gap_1()
+ .bg(Editor::edit_prediction_line_popover_bg_color(cx))
+ .border_1()
+ .border_b_0()
+ .border_color(cx.theme().colors().border)
+ .rounded_t_lg()
+ .children(editor.render_edit_prediction_accept_keybind(window, cx)),
+ )
+ .child(
+ div()
+ .bg(cx.theme().colors().editor_background)
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .rounded_lg()
+ .rounded_tr(Pixels::ZERO)
+ .child(styled_text),
+ )
+ .into_any();
+
let line_count = highlighted_edits.text.lines().count();
let longest_row =
@@ -3827,16 +3855,6 @@ impl EditorElement {
.width
};
- let styled_text = highlighted_edits.to_styled_text(&style.text);
-
- let mut element = div()
- .bg(cx.theme().colors().editor_background)
- .border_1()
- .border_color(cx.theme().colors().border)
- .rounded_md()
- .child(styled_text)
- .into_any();
-
let viewport_bounds = Bounds::new(Default::default(), window.viewport_size())
.extend(Edges {
right: -Self::SCROLLBAR_WIDTH,
@@ -3853,7 +3871,7 @@ impl EditorElement {
let is_fully_visible = x_after_longest < text_bounds.right()
&& x_after_longest + element_bounds.width < viewport_bounds.right();
- let origin = if is_fully_visible {
+ let mut origin = if is_fully_visible {
point(
x_after_longest,
text_bounds.origin.y + edit_start.row().as_f32() * line_height
@@ -3898,6 +3916,8 @@ impl EditorElement {
)
};
+ origin.y -= ACCEPT_INDICATOR_HEIGHT;
+
window.defer_draw(element, origin, 1);
// Do not return an element, since it will already be drawn due to defer_draw.
@@ -5796,63 +5816,6 @@ fn header_jump_data(
}
}
-fn inline_completion_accept_indicator(
- label: impl Into<SharedString>,
- icon: Option<IconName>,
- editor: &Editor,
- window: &mut Window,
- cx: &App,
-) -> Option<Div> {
- let accept_binding = editor.accept_edit_prediction_keybind(window, cx);
- let accept_keystroke = accept_binding.keystroke()?;
-
- let accept_key = h_flex()
- .px_0p5()
- .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
- .text_size(TextSize::XSmall.rems(cx))
- .text_color(cx.theme().colors().text)
- .gap_1()
- .when(!editor.edit_prediction_preview_is_active(), |parent| {
- parent.children(ui::render_modifiers(
- &accept_keystroke.modifiers,
- PlatformStyle::platform(),
- Some(Color::Default),
- None,
- false,
- ))
- })
- .child(accept_keystroke.key.clone());
-
- let colors = cx.theme().colors();
-
- let accent_color = colors.text_accent;
- let editor_bg_color = colors.editor_background;
- let bg_color = editor_bg_color.blend(accent_color.opacity(0.2));
- let padding_right = if icon.is_some() { px(4.) } else { px(8.) };
-
- let result = h_flex()
- .gap_1()
- .border_1()
- .rounded_md()
- .shadow_sm()
- .bg(bg_color)
- .border_color(colors.text_accent.opacity(0.8))
- .py_0p5()
- .pl_1()
- .pr(padding_right)
- .child(accept_key)
- .child(Label::new(label).size(LabelSize::Small))
- .when_some(icon, |element, icon| {
- element.child(
- div()
- .mt(px(1.5))
- .child(Icon::new(icon).size(IconSize::Small)),
- )
- });
-
- Some(result)
-}
-
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
impl AcceptEditPredictionBinding {
@@ -573,37 +573,41 @@ impl InlineCompletionButton {
language::EditPredictionsMode::Auto => false,
language::EditPredictionsMode::EagerPreview => true,
};
- menu = menu.separator().toggleable_entry(
- "Eager Preview Mode",
- is_eager_preview_enabled,
- IconPosition::Start,
- None,
- {
- let fs = fs.clone();
- move |_window, cx| {
- update_settings_file::<AllLanguageSettings>(
- fs.clone(),
- cx,
- move |settings, _cx| {
- let new_mode = match is_eager_preview_enabled {
- true => language::EditPredictionsMode::Auto,
- false => language::EditPredictionsMode::EagerPreview,
- };
-
- if let Some(edit_predictions) = settings.edit_predictions.as_mut() {
- edit_predictions.mode = new_mode;
- } else {
- settings.edit_predictions =
- Some(language_settings::EditPredictionSettingsContent {
- mode: new_mode,
- ..Default::default()
- });
- }
- },
- );
- }
- },
- );
+ menu = if cx.is_staff() {
+ menu.separator().toggleable_entry(
+ "Eager Preview Mode",
+ is_eager_preview_enabled,
+ IconPosition::Start,
+ None,
+ {
+ let fs = fs.clone();
+ move |_window, cx| {
+ update_settings_file::<AllLanguageSettings>(
+ fs.clone(),
+ cx,
+ move |settings, _cx| {
+ let new_mode = match is_eager_preview_enabled {
+ true => language::EditPredictionsMode::Auto,
+ false => language::EditPredictionsMode::EagerPreview,
+ };
+
+ if let Some(edit_predictions) = settings.edit_predictions.as_mut() {
+ edit_predictions.mode = new_mode;
+ } else {
+ settings.edit_predictions =
+ Some(language_settings::EditPredictionSettingsContent {
+ mode: new_mode,
+ ..Default::default()
+ });
+ }
+ },
+ );
+ }
+ },
+ )
+ } else {
+ menu
+ };
if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
menu = menu
@@ -237,9 +237,9 @@ pub struct EditPredictionSettings {
pub enum EditPredictionsMode {
/// If provider supports it, display inline when holding modifier key (e.g., alt).
/// Otherwise, eager preview is used.
- #[default]
Auto,
/// Display inline when there are no language server completions available.
+ #[default]
EagerPreview,
}
@@ -27,7 +27,10 @@ use gpui::{
};
use http_client::{HttpClient, Method};
use input_excerpt::excerpt_for_cursor_position;
-use language::{Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToOffset, ToPoint};
+use language::{
+ Anchor, Buffer, BufferSnapshot, CharClassifier, CharKind, EditPreview, OffsetRangeExt,
+ ToOffset, ToPoint,
+};
use language_models::LlmApiToken;
use postage::watch;
use project::Project;
@@ -57,9 +60,9 @@ const EDITABLE_REGION_END_MARKER: &'static str = "<|editable_region_end|>";
const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1);
const ZED_PREDICT_DATA_COLLECTION_CHOICE: &str = "zed_predict_data_collection_choice";
-const MAX_CONTEXT_TOKENS: usize = 100;
-const MAX_REWRITE_TOKENS: usize = 300;
-const MAX_EVENT_TOKENS: usize = 400;
+const MAX_CONTEXT_TOKENS: usize = 150;
+const MAX_REWRITE_TOKENS: usize = 350;
+const MAX_EVENT_TOKENS: usize = 500;
/// Maximum number of events to track.
const MAX_EVENT_COUNT: usize = 16;
@@ -834,8 +837,34 @@ and then another
offset: usize,
snapshot: &BufferSnapshot,
) -> Vec<(Range<Anchor>, String)> {
- let diff = similar::TextDiff::from_words(old_text.as_str(), new_text);
+ fn tokenize(text: &str) -> Vec<&str> {
+ let classifier = CharClassifier::new(None).for_completion(true);
+ let mut chars = text.chars().peekable();
+ let mut prev_ch = chars.peek().copied();
+ let mut tokens = Vec::new();
+ let mut start = 0;
+ let mut end = 0;
+ while let Some(ch) = chars.next() {
+ let prev_kind = prev_ch.map(|ch| classifier.kind(ch));
+ let kind = classifier.kind(ch);
+ if Some(kind) != prev_kind || (kind == CharKind::Punctuation && Some(ch) != prev_ch)
+ {
+ tokens.push(&text[start..end]);
+ start = end;
+ }
+ end += ch.len_utf8();
+ prev_ch = Some(ch);
+ }
+ tokens.push(&text[start..end]);
+ tokens
+ }
+
+ let old_tokens = tokenize(&old_text);
+ let new_tokens = tokenize(new_text);
+ let diff = similar::TextDiffConfig::default()
+ .algorithm(similar::Algorithm::Patience)
+ .diff_slices(&old_tokens, &new_tokens);
let mut edits: Vec<(Range<usize>, String)> = Vec::new();
let mut old_start = offset;
for change in diff.iter_all_changes() {
@@ -1705,6 +1734,70 @@ mod tests {
})
}
+ #[gpui::test]
+ async fn test_clean_up_diff(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ client::init_settings(cx);
+ });
+
+ let edits = edits_for_prediction(
+ indoc! {"
+ fn main() {
+ let word_1 = \"lorem\";
+ let range = word.len()..word.len();
+ }
+ "},
+ indoc! {"
+ <|editable_region_start|>
+ fn main() {
+ let word_1 = \"lorem\";
+ let range = word_1.len()..word_1.len();
+ }
+
+ <|editable_region_end|>
+ "},
+ cx,
+ )
+ .await;
+ assert_eq!(
+ edits,
+ [
+ (Point::new(2, 20)..Point::new(2, 20), "_1".to_string()),
+ (Point::new(2, 32)..Point::new(2, 32), "_1".to_string()),
+ ]
+ );
+
+ let edits = edits_for_prediction(
+ indoc! {"
+ fn main() {
+ let story = \"the quick\"
+ }
+ "},
+ indoc! {"
+ <|editable_region_start|>
+ fn main() {
+ let story = \"the quick brown fox jumps over the lazy dog\";
+ }
+
+ <|editable_region_end|>
+ "},
+ cx,
+ )
+ .await;
+ assert_eq!(
+ edits,
+ [
+ (
+ Point::new(1, 26)..Point::new(1, 26),
+ " brown fox jumps over the lazy dog".to_string()
+ ),
+ (Point::new(1, 27)..Point::new(1, 27), ";".to_string()),
+ ]
+ );
+ }
+
#[gpui::test]
async fn test_inline_completion_end_of_buffer(cx: &mut TestAppContext) {
cx.update(|cx| {
@@ -1768,6 +1861,58 @@ mod tests {
);
}
+ async fn edits_for_prediction(
+ buffer_content: &str,
+ completion_response: &str,
+ cx: &mut TestAppContext,
+ ) -> Vec<(Range<Point>, String)> {
+ let completion_response = completion_response.to_string();
+ let http_client = FakeHttpClient::create(move |_| {
+ let completion = completion_response.clone();
+ async move {
+ Ok(http_client::Response::builder()
+ .status(200)
+ .body(
+ serde_json::to_string(&PredictEditsResponse {
+ request_id: Uuid::new_v4(),
+ output_excerpt: completion,
+ })
+ .unwrap()
+ .into(),
+ )
+ .unwrap())
+ }
+ });
+
+ let client = cx.update(|cx| Client::new(Arc::new(FakeSystemClock::new()), http_client, cx));
+ cx.update(|cx| {
+ RefreshLlmTokenListener::register(client.clone(), cx);
+ });
+ let server = FakeServer::for_client(42, &client, cx).await;
+ let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
+ let zeta = cx.new(|cx| Zeta::new(client, user_store, cx));
+
+ let buffer = cx.new(|cx| Buffer::local(buffer_content, cx));
+ let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+ let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0)));
+ let completion_task = zeta.update(cx, |zeta, cx| {
+ zeta.request_completion(None, &buffer, cursor, false, cx)
+ });
+
+ let token_request = server.receive::<proto::GetLlmToken>().await.unwrap();
+ server.respond(
+ token_request.receipt(),
+ proto::GetLlmTokenResponse { token: "".into() },
+ );
+
+ let completion = completion_task.await.unwrap().unwrap();
+ completion
+ .edits
+ .into_iter()
+ .map(|(old_range, new_text)| (old_range.to_point(&snapshot), new_text.clone()))
+ .collect::<Vec<_>>()
+ }
+
fn to_completion_edits(
iterator: impl IntoIterator<Item = (Range<usize>, String)>,
buffer: &Entity<Buffer>,