Detailed changes
@@ -509,6 +509,13 @@
"tab": "editor::AcceptInlineCompletion"
}
},
+ {
+ "context": "Editor && inline_completion && showing_completions",
+ "bindings": {
+ // Currently, changing this binding breaks the preview behavior
+ "alt-enter": "editor::AcceptInlineCompletion"
+ }
+ },
{
"context": "Editor && showing_code_actions",
"bindings": {
@@ -586,6 +586,13 @@
"tab": "editor::AcceptInlineCompletion"
}
},
+ {
+ "context": "Editor && inline_completion && showing_completions",
+ "bindings": {
+ // Currently, changing this binding breaks the preview behavior
+ "alt-tab": "editor::AcceptInlineCompletion"
+ }
+ },
{
"context": "Editor && showing_code_actions",
"use_key_equivalents": true,
@@ -341,7 +341,6 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, window, cx| {
assert!(editor.context_menu_visible());
- assert!(!editor.context_menu_contains_inline_completion());
assert!(!editor.has_active_inline_completion());
// Since we have both, the copilot suggestion is not shown inline
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
@@ -399,7 +398,6 @@ mod tests {
executor.run_until_parked();
cx.update_editor(|editor, _, cx| {
assert!(!editor.context_menu_visible());
- assert!(!editor.context_menu_contains_inline_completion());
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
@@ -419,7 +417,6 @@ mod tests {
cx.update_editor(|editor, window, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion());
- assert!(!editor.context_menu_contains_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
@@ -934,7 +931,6 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, _, cx| {
assert!(editor.context_menu_visible());
- assert!(!editor.context_menu_contains_inline_completion());
assert!(!editor.has_active_inline_completion(),);
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
});
@@ -1,8 +1,8 @@
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
- div, pulsating_between, px, uniform_list, Animation, AnimationExt, AnyElement,
- BackgroundExecutor, Div, Entity, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString,
- Size, StrikethroughStyle, StyledText, UniformListScrollHandle, WeakEntity,
+ div, px, uniform_list, AnyElement, BackgroundExecutor, Div, Entity, FontWeight,
+ ListSizingBehavior, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
+ UniformListScrollHandle, WeakEntity,
};
use language::Buffer;
use language::{CodeLabel, CompletionDocumentation};
@@ -10,8 +10,7 @@ use lsp::LanguageServerId;
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
use project::{CodeAction, Completion, TaskSourceKind};
-use settings::Settings;
-use std::time::Duration;
+
use std::{
cell::RefCell,
cmp::{min, Reverse},
@@ -26,11 +25,9 @@ use workspace::Workspace;
use crate::{
actions::{ConfirmCodeAction, ConfirmCompletion},
- display_map::DisplayPoint,
render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,
CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,
};
-use crate::{AcceptInlineCompletion, InlineCompletionMenuHint, InlineCompletionText};
pub const MENU_GAP: Pixels = px(4.);
pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
@@ -114,10 +111,10 @@ impl CodeContextMenu {
}
}
- pub fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
+ pub fn origin(&self) -> ContextMenuOrigin {
match self {
- CodeContextMenu::Completions(menu) => menu.origin(cursor_position),
- CodeContextMenu::CodeActions(menu) => menu.origin(cursor_position),
+ CodeContextMenu::Completions(menu) => menu.origin(),
+ CodeContextMenu::CodeActions(menu) => menu.origin(),
}
}
@@ -154,7 +151,7 @@ impl CodeContextMenu {
}
pub enum ContextMenuOrigin {
- EditorPoint(DisplayPoint),
+ Cursor,
GutterIndicator(DisplayRow),
}
@@ -166,18 +163,13 @@ pub struct CompletionsMenu {
pub buffer: Entity<Buffer>,
pub completions: Rc<RefCell<Box<[Completion]>>>,
match_candidates: Rc<[StringMatchCandidate]>,
- pub entries: Rc<RefCell<Vec<CompletionEntry>>>,
+ pub entries: Rc<RefCell<Vec<StringMatch>>>,
pub selected_item: usize,
scroll_handle: UniformListScrollHandle,
resolve_completions: bool,
show_completion_documentation: bool,
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
-}
-
-#[derive(Clone, Debug)]
-pub(crate) enum CompletionEntry {
- Match(StringMatch),
- InlineCompletionHint(InlineCompletionMenuHint),
+ pub previewing_inline_completion: bool,
}
impl CompletionsMenu {
@@ -208,6 +200,7 @@ impl CompletionsMenu {
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: true,
last_rendered_range: RefCell::new(None).into(),
+ previewing_inline_completion: false,
}
}
@@ -244,13 +237,11 @@ impl CompletionsMenu {
let entries = choices
.iter()
.enumerate()
- .map(|(id, completion)| {
- CompletionEntry::Match(StringMatch {
- candidate_id: id,
- score: 1.,
- positions: vec![],
- string: completion.clone(),
- })
+ .map(|(id, completion)| StringMatch {
+ candidate_id: id,
+ score: 1.,
+ positions: vec![],
+ string: completion.clone(),
})
.collect::<Vec<_>>();
Self {
@@ -266,6 +257,7 @@ impl CompletionsMenu {
resolve_completions: false,
show_completion_documentation: false,
last_rendered_range: RefCell::new(None).into(),
+ previewing_inline_completion: false,
}
}
@@ -340,24 +332,6 @@ impl CompletionsMenu {
}
}
- pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {
- let hint = CompletionEntry::InlineCompletionHint(hint);
- let mut entries = self.entries.borrow_mut();
- match entries.first() {
- Some(CompletionEntry::InlineCompletionHint { .. }) => {
- entries[0] = hint;
- }
- _ => {
- entries.insert(0, hint);
- // When `y_flipped`, need to scroll to bring it into view.
- if self.selected_item == 0 {
- self.scroll_handle
- .scroll_to_item(self.selected_item, ScrollStrategy::Top);
- }
- }
- }
- }
-
pub fn resolve_visible_completions(
&mut self,
provider: Option<&dyn CompletionProvider>,
@@ -406,17 +380,15 @@ impl CompletionsMenu {
// This filtering doesn't happen if the completions are currently being updated.
let completions = self.completions.borrow();
let candidate_ids = entry_indices
- .flat_map(|i| Self::entry_candidate_id(&entries[i]))
+ .map(|i| entries[i].candidate_id)
.filter(|i| completions[*i].documentation.is_none());
// Current selection is always resolved even if it already has documentation, to handle
// out-of-spec language servers that return more results later.
- let candidate_ids = match Self::entry_candidate_id(&entries[self.selected_item]) {
- None => candidate_ids.collect::<Vec<usize>>(),
- Some(selected_candidate_id) => iter::once(selected_candidate_id)
- .chain(candidate_ids.filter(|id| *id != selected_candidate_id))
- .collect::<Vec<usize>>(),
- };
+ let selected_candidate_id = entries[self.selected_item].candidate_id;
+ let candidate_ids = iter::once(selected_candidate_id)
+ .chain(candidate_ids.filter(|id| *id != selected_candidate_id))
+ .collect::<Vec<usize>>();
drop(entries);
if candidate_ids.is_empty() {
@@ -438,19 +410,16 @@ impl CompletionsMenu {
.detach();
}
- fn entry_candidate_id(entry: &CompletionEntry) -> Option<usize> {
- match entry {
- CompletionEntry::Match(entry) => Some(entry.candidate_id),
- CompletionEntry::InlineCompletionHint { .. } => None,
- }
+ pub fn is_empty(&self) -> bool {
+ self.entries.borrow().is_empty()
}
pub fn visible(&self) -> bool {
- !self.entries.borrow().is_empty()
+ !self.is_empty() && !self.previewing_inline_completion
}
- fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
- ContextMenuOrigin::EditorPoint(cursor_position)
+ fn origin(&self) -> ContextMenuOrigin {
+ ContextMenuOrigin::Cursor
}
fn render(
@@ -468,23 +437,18 @@ impl CompletionsMenu {
.borrow()
.iter()
.enumerate()
- .max_by_key(|(_, mat)| match mat {
- CompletionEntry::Match(mat) => {
- let completion = &completions[mat.candidate_id];
- let documentation = &completion.documentation;
-
- let mut len = completion.label.text.chars().count();
- if let Some(CompletionDocumentation::SingleLine(text)) = documentation {
- if show_completion_documentation {
- len += text.chars().count();
- }
- }
+ .max_by_key(|(_, mat)| {
+ let completion = &completions[mat.candidate_id];
+ let documentation = &completion.documentation;
- len
- }
- CompletionEntry::InlineCompletionHint(hint) => {
- "Zed AI / ".chars().count() + hint.label().chars().count()
+ let mut len = completion.label.text.chars().count();
+ if let Some(CompletionDocumentation::SingleLine(text)) = documentation {
+ if show_completion_documentation {
+ len += text.chars().count();
+ }
}
+
+ len
})
.map(|(ix, _)| ix);
drop(completions);
@@ -508,179 +472,83 @@ impl CompletionsMenu {
.enumerate()
.map(|(ix, mat)| {
let item_ix = start_ix + ix;
- let buffer_font = theme::ThemeSettings::get_global(cx).buffer_font.clone();
- let base_label = h_flex()
- .gap_1()
- .child(div().font(buffer_font.clone()).child("Zed AI"))
- .child(div().px_0p5().child("/").opacity(0.2));
-
- match mat {
- CompletionEntry::Match(mat) => {
- let candidate_id = mat.candidate_id;
- let completion = &completions_guard[candidate_id];
-
- let documentation = if show_completion_documentation {
- &completion.documentation
- } else {
- &None
- };
-
- let filter_start = completion.label.filter_range.start;
- let highlights = gpui::combine_highlights(
- mat.ranges().map(|range| {
- (
- filter_start + range.start..filter_start + range.end,
- FontWeight::BOLD.into(),
- )
- }),
- styled_runs_for_code_label(&completion.label, &style.syntax)
- .map(|(range, mut highlight)| {
- // Ignore font weight for syntax highlighting, as we'll use it
- // for fuzzy matches.
- highlight.font_weight = None;
-
- if completion.lsp_completion.deprecated.unwrap_or(false)
- {
- highlight.strikethrough =
- Some(StrikethroughStyle {
- thickness: 1.0.into(),
- ..Default::default()
- });
- highlight.color =
- Some(cx.theme().colors().text_muted);
- }
-
- (range, highlight)
- }),
- );
-
- let completion_label =
- StyledText::new(completion.label.text.clone())
- .with_highlights(&style.text, highlights);
- let documentation_label =
- if let Some(CompletionDocumentation::SingleLine(text)) =
- documentation
- {
- if text.trim().is_empty() {
- None
- } else {
- Some(
- Label::new(text.clone())
- .ml_4()
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- }
- } else {
- None
- };
-
- let color_swatch = completion
- .color()
- .map(|color| div().size_4().bg(color).rounded_sm());
-
- div().min_w(px(220.)).max_w(px(540.)).child(
- ListItem::new(mat.candidate_id)
- .inset(true)
- .toggle_state(item_ix == selected_item)
- .on_click(cx.listener(move |editor, _event, window, cx| {
- cx.stop_propagation();
- if let Some(task) = editor.confirm_completion(
- &ConfirmCompletion {
- item_ix: Some(item_ix),
- },
- window,
- cx,
- ) {
- task.detach_and_log_err(cx)
- }
- }))
- .start_slot::<Div>(color_swatch)
- .child(h_flex().overflow_hidden().child(completion_label))
- .end_slot::<Label>(documentation_label),
+ let completion = &completions_guard[mat.candidate_id];
+ let documentation = if show_completion_documentation {
+ &completion.documentation
+ } else {
+ &None
+ };
+
+ let filter_start = completion.label.filter_range.start;
+ let highlights = gpui::combine_highlights(
+ mat.ranges().map(|range| {
+ (
+ filter_start + range.start..filter_start + range.end,
+ FontWeight::BOLD.into(),
)
- }
- CompletionEntry::InlineCompletionHint(
- hint @ InlineCompletionMenuHint::None,
- ) => div().min_w(px(250.)).max_w(px(500.)).child(
- ListItem::new("inline-completion")
- .inset(true)
- .toggle_state(item_ix == selected_item)
- .start_slot(Icon::new(IconName::ZedPredict))
- .child(
- base_label.child(
- StyledText::new(hint.label())
- .with_highlights(&style.text, None),
- ),
- ),
- ),
- CompletionEntry::InlineCompletionHint(
- hint @ InlineCompletionMenuHint::Loading,
- ) => div().min_w(px(250.)).max_w(px(500.)).child(
- ListItem::new("inline-completion")
- .inset(true)
- .toggle_state(item_ix == selected_item)
- .start_slot(Icon::new(IconName::ZedPredict))
- .child(base_label.child({
- let text_style = style.text.clone();
- StyledText::new(hint.label())
- .with_highlights(&text_style, None)
- .with_animation(
- "pulsating-label",
- Animation::new(Duration::from_secs(1))
- .repeat()
- .with_easing(pulsating_between(0.4, 0.8)),
- move |text, delta| {
- let mut text_style = text_style.clone();
- text_style.color =
- text_style.color.opacity(delta);
- text.with_highlights(&text_style, None)
- },
- )
- })),
- ),
- CompletionEntry::InlineCompletionHint(
- hint @ InlineCompletionMenuHint::PendingTermsAcceptance,
- ) => div().min_w(px(250.)).max_w(px(500.)).child(
- ListItem::new("inline-completion")
- .inset(true)
- .toggle_state(item_ix == selected_item)
- .start_slot(Icon::new(IconName::ZedPredict))
- .child(
- base_label.child(
- StyledText::new(hint.label())
- .with_highlights(&style.text, None),
- ),
- )
- .on_click(cx.listener(move |editor, _event, window, cx| {
- cx.stop_propagation();
- editor.toggle_zed_predict_onboarding(window, cx);
- })),
+ }),
+ styled_runs_for_code_label(&completion.label, &style.syntax).map(
+ |(range, mut highlight)| {
+ // Ignore font weight for syntax highlighting, as we'll use it
+ // for fuzzy matches.
+ highlight.font_weight = None;
+ if completion.lsp_completion.deprecated.unwrap_or(false) {
+ highlight.strikethrough = Some(StrikethroughStyle {
+ thickness: 1.0.into(),
+ ..Default::default()
+ });
+ highlight.color = Some(cx.theme().colors().text_muted);
+ }
+
+ (range, highlight)
+ },
),
+ );
+
+ let completion_label = StyledText::new(completion.label.text.clone())
+ .with_highlights(&style.text, highlights);
+ let documentation_label = if let Some(
+ CompletionDocumentation::SingleLine(text),
+ ) = documentation
+ {
+ if text.trim().is_empty() {
+ None
+ } else {
+ Some(
+ Label::new(text.clone())
+ .ml_4()
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ }
+ } else {
+ None
+ };
- CompletionEntry::InlineCompletionHint(
- hint @ InlineCompletionMenuHint::Loaded { .. },
- ) => div().min_w(px(250.)).max_w(px(500.)).child(
- ListItem::new("inline-completion")
- .inset(true)
- .toggle_state(item_ix == selected_item)
- .start_slot(Icon::new(IconName::ZedPredict))
- .child(
- base_label.child(
- StyledText::new(hint.label())
- .with_highlights(&style.text, None),
- ),
- )
- .on_click(cx.listener(move |editor, _event, window, cx| {
- cx.stop_propagation();
- editor.accept_inline_completion(
- &AcceptInlineCompletion {},
- window,
- cx,
- );
- })),
- ),
- }
+ let color_swatch = completion
+ .color()
+ .map(|color| div().size_4().bg(color).rounded_sm());
+
+ div().min_w(px(280.)).max_w(px(540.)).child(
+ ListItem::new(mat.candidate_id)
+ .inset(true)
+ .toggle_state(item_ix == selected_item)
+ .on_click(cx.listener(move |editor, _event, window, cx| {
+ cx.stop_propagation();
+ if let Some(task) = editor.confirm_completion(
+ &ConfirmCompletion {
+ item_ix: Some(item_ix),
+ },
+ window,
+ cx,
+ ) {
+ task.detach_and_log_err(cx)
+ }
+ }))
+ .start_slot::<Div>(color_swatch)
+ .child(h_flex().overflow_hidden().child(completion_label))
+ .end_slot::<Label>(documentation_label),
+ )
})
.collect()
},
@@ -706,45 +574,25 @@ impl CompletionsMenu {
return None;
}
- let multiline_docs = match &self.entries.borrow()[self.selected_item] {
- CompletionEntry::Match(mat) => {
- match self.completions.borrow_mut()[mat.candidate_id]
- .documentation
- .as_ref()?
- {
- CompletionDocumentation::MultiLinePlainText(text) => {
- div().child(SharedString::from(text.clone()))
- }
- CompletionDocumentation::MultiLineMarkdown(parsed)
- if !parsed.text.is_empty() =>
- {
- div().child(render_parsed_markdown(
- "completions_markdown",
- parsed,
- &style,
- workspace,
- cx,
- ))
- }
- CompletionDocumentation::MultiLineMarkdown(_) => return None,
- CompletionDocumentation::SingleLine(_) => return None,
- CompletionDocumentation::Undocumented => return None,
- }
- }
- CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::Loaded { text }) => {
- match text {
- InlineCompletionText::Edit(highlighted_edits) => div()
- .mx_1()
- .rounded_md()
- .bg(cx.theme().colors().editor_background)
- .child(
- gpui::StyledText::new(highlighted_edits.text.clone())
- .with_highlights(&style.text, highlighted_edits.highlights.clone()),
- ),
- InlineCompletionText::Move(text) => div().child(text.clone()),
- }
+ let mat = &self.entries.borrow()[self.selected_item];
+ let multiline_docs = match self.completions.borrow_mut()[mat.candidate_id]
+ .documentation
+ .as_ref()?
+ {
+ CompletionDocumentation::MultiLinePlainText(text) => {
+ div().child(SharedString::from(text.clone()))
}
- CompletionEntry::InlineCompletionHint(_) => return None,
+ CompletionDocumentation::MultiLineMarkdown(parsed) if !parsed.text.is_empty() => div()
+ .child(render_parsed_markdown(
+ "completions_markdown",
+ parsed,
+ &style,
+ workspace,
+ cx,
+ )),
+ CompletionDocumentation::MultiLineMarkdown(_) => return None,
+ CompletionDocumentation::SingleLine(_) => return None,
+ CompletionDocumentation::Undocumented => return None,
};
Some(
@@ -763,11 +611,6 @@ impl CompletionsMenu {
}
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
- let inline_completion_was_selected = self.selected_item == 0
- && self.entries.borrow().first().map_or(false, |entry| {
- matches!(entry, CompletionEntry::InlineCompletionHint(_))
- });
-
let mut matches = if let Some(query) = query {
fuzzy::match_strings(
&self.match_candidates,
@@ -861,25 +704,15 @@ impl CompletionsMenu {
}
drop(completions);
- let mut entries = self.entries.borrow_mut();
- let new_selection = if let Some(CompletionEntry::InlineCompletionHint(_)) = entries.first()
- {
- entries.truncate(1);
- if inline_completion_was_selected || matches.is_empty() {
- 0
- } else {
- 1
- }
- } else {
- entries.truncate(0);
- 0
- };
- entries.extend(matches.into_iter().map(CompletionEntry::Match));
- self.selected_item = new_selection;
- // Scroll to 0 even if the LSP completion is the only one selected. This keeps the display
- // consistent when y_flipped.
+ *self.entries.borrow_mut() = matches;
+ self.selected_item = 0;
+ // This keeps the display consistent when y_flipped.
self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
}
+
+ pub fn set_previewing_inline_completion(&mut self, value: bool) {
+ self.previewing_inline_completion = value;
+ }
}
#[derive(Clone)]
@@ -1077,11 +910,11 @@ impl CodeActionsMenu {
!self.actions.is_empty()
}
- fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
+ fn origin(&self) -> ContextMenuOrigin {
if let Some(row) = self.deployed_from_indicator {
ContextMenuOrigin::GutterIndicator(row)
} else {
- ContextMenuOrigin::EditorPoint(cursor_position)
+ ContextMenuOrigin::Cursor
}
}
@@ -508,7 +508,7 @@ impl DisplayMap {
pub(crate) fn splice_inlays(
&mut self,
- to_remove: Vec<InlayId>,
+ to_remove: &[InlayId],
to_insert: Vec<Inlay>,
cx: &mut Context<Self>,
) {
@@ -545,7 +545,7 @@ impl InlayMap {
pub fn splice(
&mut self,
- to_remove: Vec<InlayId>,
+ to_remove: &[InlayId],
to_insert: Vec<Inlay>,
) -> (InlaySnapshot, Vec<InlayEdit>) {
let snapshot = &mut self.snapshot;
@@ -653,7 +653,7 @@ impl InlayMap {
}
log::info!("removing inlays: {:?}", to_remove);
- let (snapshot, edits) = self.splice(to_remove, to_insert);
+ let (snapshot, edits) = self.splice(&to_remove, to_insert);
(snapshot, edits)
}
}
@@ -1171,7 +1171,7 @@ mod tests {
let mut next_inlay_id = 0;
let (inlay_snapshot, _) = inlay_map.splice(
- Vec::new(),
+ &[],
vec![Inlay {
id: InlayId::Hint(post_inc(&mut next_inlay_id)),
position: buffer.read(cx).snapshot(cx).anchor_after(3),
@@ -1247,7 +1247,7 @@ mod tests {
assert_eq!(inlay_snapshot.text(), "abxyDzefghi");
let (inlay_snapshot, _) = inlay_map.splice(
- Vec::new(),
+ &[],
vec![
Inlay {
id: InlayId::Hint(post_inc(&mut next_inlay_id)),
@@ -1444,7 +1444,11 @@ mod tests {
// The inlays can be manually removed.
let (inlay_snapshot, _) = inlay_map.splice(
- inlay_map.inlays.iter().map(|inlay| inlay.id).collect(),
+ &inlay_map
+ .inlays
+ .iter()
+ .map(|inlay| inlay.id)
+ .collect::<Vec<InlayId>>(),
Vec::new(),
);
assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
@@ -1458,7 +1462,7 @@ mod tests {
let mut next_inlay_id = 0;
let (inlay_snapshot, _) = inlay_map.splice(
- Vec::new(),
+ &[],
vec![
Inlay {
id: InlayId::Hint(post_inc(&mut next_inlay_id)),
@@ -73,17 +73,18 @@ use zed_predict_onboarding::ZedPredictModal;
use code_context_menus::{
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
- CompletionEntry, CompletionsMenu, ContextMenuOrigin,
+ CompletionsMenu, ContextMenuOrigin,
};
use git::blame::GitBlame;
use gpui::{
- div, impl_actions, point, prelude::*, px, relative, size, Action, AnyElement, App,
- AsyncWindowContext, AvailableSpace, Bounds, ClipboardEntry, ClipboardItem, Context,
- DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent,
- Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext,
- MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size,
- Styled, StyledText, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection,
- UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window,
+ div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation,
+ AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Bounds, ClipboardEntry,
+ ClipboardItem, Context, DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter,
+ FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla,
+ InteractiveText, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement,
+ Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, Task, TextStyle,
+ TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity,
+ WeakFocusHandle, Window,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@@ -107,7 +108,7 @@ pub use proposed_changes_editor::{
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
};
use similar::{ChangeTag, TextDiff};
-use std::iter::Peekable;
+use std::iter::{self, Peekable};
use task::{ResolvedTask, TaskTemplate, TaskVariables};
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
@@ -163,7 +164,7 @@ use ui::{
h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize,
Tooltip,
};
-use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
+use util::{defer, maybe, post_inc, RangeExt, ResultExt, TakeUntilExt, TryFutureExt};
use workspace::item::{ItemHandle, PreviewTabsSettings};
use workspace::notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt};
use workspace::{
@@ -465,32 +466,6 @@ pub fn make_suggestion_styles(cx: &mut App) -> InlineCompletionStyles {
type CompletionId = usize;
-#[derive(Debug, Clone)]
-enum InlineCompletionMenuHint {
- Loading,
- Loaded { text: InlineCompletionText },
- PendingTermsAcceptance,
- None,
-}
-
-impl InlineCompletionMenuHint {
- pub fn label(&self) -> &'static str {
- match self {
- InlineCompletionMenuHint::Loading | InlineCompletionMenuHint::Loaded { .. } => {
- "Edit Prediction"
- }
- InlineCompletionMenuHint::PendingTermsAcceptance => "Accept Terms of Service",
- InlineCompletionMenuHint::None => "No Prediction",
- }
- }
-}
-
-#[derive(Clone, Debug)]
-enum InlineCompletionText {
- Move(SharedString),
- Edit(HighlightedText),
-}
-
pub(crate) enum EditDisplayMode {
TabAccept,
DiffPopover,
@@ -504,7 +479,11 @@ enum InlineCompletion {
display_mode: EditDisplayMode,
snapshot: BufferSnapshot,
},
- Move(Anchor),
+ Move {
+ target: Anchor,
+ range_around_target: Range<text::Anchor>,
+ snapshot: BufferSnapshot,
+ },
}
struct InlineCompletionState {
@@ -513,6 +492,15 @@ struct InlineCompletionState {
invalidation_range: Range<Anchor>,
}
+impl InlineCompletionState {
+ pub fn is_move(&self) -> bool {
+ match &self.completion {
+ InlineCompletion::Move { .. } => true,
+ _ => false,
+ }
+ }
+}
+
enum InlineCompletionHighlight {}
pub enum MenuInlineCompletionsPolicy {
@@ -687,6 +675,8 @@ pub struct Editor {
inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
code_action_providers: Vec<Rc<dyn CodeActionProvider>>,
active_inline_completion: Option<InlineCompletionState>,
+ /// Used to prevent flickering as the user types while the menu is open
+ stale_inline_completion_in_menu: Option<InlineCompletionState>,
// enable_inline_completions is a switch that Vim can use to disable
// inline completions based on its mode.
enable_inline_completions: bool,
@@ -1381,6 +1371,7 @@ impl Editor {
hovered_link_state: Default::default(),
inline_completion_provider: None,
active_inline_completion: None,
+ stale_inline_completion_in_menu: None,
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
@@ -1496,7 +1487,7 @@ impl Editor {
match self.context_menu.borrow().as_ref() {
Some(CodeContextMenu::Completions(_)) => {
key_context.add("menu");
- key_context.add("showing_completions")
+ key_context.add("showing_completions");
}
Some(CodeContextMenu::CodeActions(_)) => {
key_context.add("menu");
@@ -2611,9 +2602,6 @@ impl Editor {
}
if self.hide_context_menu(window, cx).is_some() {
- if self.show_inline_completions_in_menu(cx) && self.has_active_inline_completion() {
- self.update_visible_inline_completion(window, cx);
- }
return true;
}
@@ -3601,10 +3589,11 @@ impl Editor {
} else {
self.inlay_hint_cache.clear();
self.splice_inlays(
- self.visible_inlay_hints(cx)
+ &self
+ .visible_inlay_hints(cx)
.iter()
.map(|inlay| inlay.id)
- .collect(),
+ .collect::<Vec<InlayId>>(),
Vec::new(),
cx,
);
@@ -3622,7 +3611,7 @@ impl Editor {
to_remove,
to_insert,
})) => {
- self.splice_inlays(to_remove, to_insert, cx);
+ self.splice_inlays(&to_remove, to_insert, cx);
return;
}
ControlFlow::Break(None) => return,
@@ -3635,7 +3624,7 @@ impl Editor {
to_insert,
}) = self.inlay_hint_cache.remove_excerpts(excerpts_removed)
{
- self.splice_inlays(to_remove, to_insert, cx);
+ self.splice_inlays(&to_remove, to_insert, cx);
}
return;
}
@@ -3658,7 +3647,7 @@ impl Editor {
ignore_debounce,
cx,
) {
- self.splice_inlays(to_remove, to_insert, cx);
+ self.splice_inlays(&to_remove, to_insert, cx);
}
}
@@ -3738,7 +3727,7 @@ impl Editor {
pub fn splice_inlays(
&self,
- to_remove: Vec<InlayId>,
+ to_remove: &[InlayId],
to_insert: Vec<Inlay>,
cx: &mut Context<Self>,
) {
@@ -3905,17 +3894,15 @@ impl Editor {
let mut menu = menu.unwrap();
menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx);
+ *editor.context_menu.borrow_mut() =
+ Some(CodeContextMenu::Completions(menu));
+
if editor.show_inline_completions_in_menu(cx) {
- if let Some(hint) = editor.inline_completion_menu_hint(window, cx) {
- menu.show_inline_completion_hint(hint);
- }
+ editor.update_visible_inline_completion(window, cx);
} else {
editor.discard_inline_completion(false, cx);
}
- *editor.context_menu.borrow_mut() =
- Some(CodeContextMenu::Completions(menu));
-
cx.notify();
} else if editor.completion_tasks.len() <= 1 {
// If there are no more completion tasks and the last menu was
@@ -3982,34 +3969,6 @@ impl Editor {
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
use language::ToOffset as _;
- {
- let context_menu = self.context_menu.borrow();
- if let CodeContextMenu::Completions(menu) = context_menu.as_ref()? {
- let entries = menu.entries.borrow();
- let entry = entries.get(item_ix.unwrap_or(menu.selected_item));
- match entry {
- Some(CompletionEntry::InlineCompletionHint(
- InlineCompletionMenuHint::Loading,
- )) => return Some(Task::ready(Ok(()))),
- Some(CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::None)) => {
- drop(entries);
- drop(context_menu);
- self.context_menu_next(&Default::default(), window, cx);
- return Some(Task::ready(Ok(())));
- }
- Some(CompletionEntry::InlineCompletionHint(
- InlineCompletionMenuHint::PendingTermsAcceptance,
- )) => {
- drop(entries);
- drop(context_menu);
- self.toggle_zed_predict_onboarding(window, cx);
- return Some(Task::ready(Ok(())));
- }
- _ => {}
- }
- }
- }
-
let completions_menu =
if let CodeContextMenu::Completions(menu) = self.hide_context_menu(window, cx)? {
menu
@@ -4019,19 +3978,9 @@ impl Editor {
let entries = completions_menu.entries.borrow();
let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?;
- let mat = match mat {
- CompletionEntry::InlineCompletionHint(_) => {
- self.accept_inline_completion(&AcceptInlineCompletion, window, cx);
- cx.stop_propagation();
- return Some(Task::ready(Ok(())));
- }
- CompletionEntry::Match(mat) => {
- if self.show_inline_completions_in_menu(cx) {
- self.discard_inline_completion(true, cx);
- }
- mat
- }
- };
+ if self.show_inline_completions_in_menu(cx) {
+ self.discard_inline_completion(true, cx);
+ }
let candidate_id = mat.candidate_id;
drop(entries);
@@ -4863,10 +4812,10 @@ impl Editor {
self.report_inline_completion_event(true, cx);
match &active_inline_completion.completion {
- InlineCompletion::Move(position) => {
- let position = *position;
+ InlineCompletion::Move { target, .. } => {
+ let target = *target;
self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
- selections.select_anchor_ranges([position..position]);
+ selections.select_anchor_ranges([target..target]);
});
}
InlineCompletion::Edit { edits, .. } => {
@@ -4911,10 +4860,10 @@ impl Editor {
self.report_inline_completion_event(true, cx);
match &active_inline_completion.completion {
- InlineCompletion::Move(position) => {
- let position = *position;
+ InlineCompletion::Move { target, .. } => {
+ let target = *target;
self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
- selections.select_anchor_ranges([position..position]);
+ selections.select_anchor_ranges([target..target]);
});
}
InlineCompletion::Edit { edits, .. } => {
@@ -4973,7 +4922,7 @@ impl Editor {
provider.discard(cx);
}
- self.take_active_inline_completion(cx).is_some()
+ self.take_active_inline_completion(cx)
}
fn report_inline_completion_event(&self, accepted: bool, cx: &App) {
@@ -5010,19 +4959,58 @@ impl Editor {
self.active_inline_completion.is_some()
}
- fn take_active_inline_completion(
+ fn take_active_inline_completion(&mut self, cx: &mut Context<Self>) -> bool {
+ let Some(active_inline_completion) = self.active_inline_completion.take() else {
+ return false;
+ };
+
+ self.splice_inlays(&active_inline_completion.inlay_ids, Default::default(), cx);
+ self.clear_highlights::<InlineCompletionHighlight>(cx);
+ self.stale_inline_completion_in_menu = Some(active_inline_completion);
+ true
+ }
+
+ fn update_inline_completion_preview(
&mut self,
+ modifiers: &Modifiers,
+ window: &mut Window,
cx: &mut Context<Self>,
- ) -> Option<InlineCompletion> {
- let active_inline_completion = self.active_inline_completion.take()?;
- self.splice_inlays(active_inline_completion.inlay_ids, Default::default(), cx);
- self.clear_highlights::<InlineCompletionHighlight>(cx);
- Some(active_inline_completion.completion)
+ ) {
+ // Moves jump directly with a preview step
+
+ if self
+ .active_inline_completion
+ .as_ref()
+ .map_or(true, |c| c.is_move())
+ {
+ cx.notify();
+ return;
+ }
+
+ if !self.show_inline_completions_in_menu(cx) {
+ return;
+ }
+
+ let mut menu_borrow = self.context_menu.borrow_mut();
+
+ let Some(CodeContextMenu::Completions(completions_menu)) = menu_borrow.as_mut() else {
+ return;
+ };
+
+ if completions_menu.is_empty()
+ || completions_menu.previewing_inline_completion == modifiers.alt
+ {
+ return;
+ }
+
+ completions_menu.set_previewing_inline_completion(modifiers.alt);
+ drop(menu_borrow);
+ self.update_visible_inline_completion(window, cx);
}
fn update_visible_inline_completion(
&mut self,
- window: &mut Window,
+ _window: &mut Window,
cx: &mut Context<Self>,
) -> Option<()> {
let selection = self.selections.newest_anchor();
@@ -5031,7 +5019,8 @@ impl Editor {
let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
let excerpt_id = cursor.excerpt_id;
- let completions_menu_has_precedence = !self.show_inline_completions_in_menu(cx)
+ let show_in_menu = self.show_inline_completions_in_menu(cx);
+ let completions_menu_has_precedence = !show_in_menu
&& (self.context_menu.borrow().is_some()
|| (!self.completion_tasks.is_empty() && !self.has_active_inline_completion()));
if completions_menu_has_precedence
@@ -5080,50 +5069,73 @@ impl Editor {
let cursor_row = cursor.to_point(&multibuffer).row;
+ let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?;
+
let mut inlay_ids = Vec::new();
let invalidation_row_range;
- let completion = if cursor_row < edit_start_row {
- invalidation_row_range = cursor_row..edit_end_row;
- InlineCompletion::Move(first_edit_start)
+ let move_invalidation_row_range = if cursor_row < edit_start_row {
+ Some(cursor_row..edit_end_row)
} else if cursor_row > edit_end_row {
- invalidation_row_range = edit_start_row..cursor_row;
- InlineCompletion::Move(first_edit_start)
+ Some(edit_start_row..cursor_row)
} else {
- if edits
- .iter()
- .all(|(range, _)| range.to_offset(&multibuffer).is_empty())
- {
- let mut inlays = Vec::new();
- for (range, new_text) in &edits {
- let inlay = Inlay::inline_completion(
- post_inc(&mut self.next_inlay_id),
- range.start,
- new_text.as_str(),
+ None
+ };
+ let completion = if let Some(move_invalidation_row_range) = move_invalidation_row_range {
+ invalidation_row_range = move_invalidation_row_range;
+ let target = first_edit_start;
+ let target_point = text::ToPoint::to_point(&target.text_anchor, &snapshot);
+ // TODO: Base this off of TreeSitter or word boundaries?
+ let target_excerpt_begin = snapshot.anchor_before(snapshot.clip_point(
+ Point::new(target_point.row, target_point.column.saturating_sub(10)),
+ Bias::Left,
+ ));
+ let target_excerpt_end = snapshot.anchor_after(snapshot.clip_point(
+ Point::new(target_point.row, target_point.column + 10),
+ Bias::Right,
+ ));
+ // TODO: Extend this to be before the jump target, and draw a cursor at the jump target
+ // (using Editor::current_user_player_color).
+ let range_around_target = target_excerpt_begin..target_excerpt_end;
+ InlineCompletion::Move {
+ target,
+ range_around_target,
+ snapshot,
+ }
+ } else {
+ if !show_in_menu || !self.has_active_completions_menu() {
+ if edits
+ .iter()
+ .all(|(range, _)| range.to_offset(&multibuffer).is_empty())
+ {
+ let mut inlays = Vec::new();
+ for (range, new_text) in &edits {
+ let inlay = Inlay::inline_completion(
+ post_inc(&mut self.next_inlay_id),
+ range.start,
+ new_text.as_str(),
+ );
+ inlay_ids.push(inlay.id);
+ inlays.push(inlay);
+ }
+
+ self.splice_inlays(&[], inlays, cx);
+ } else {
+ let background_color = cx.theme().status().deleted_background;
+ self.highlight_text::<InlineCompletionHighlight>(
+ edits.iter().map(|(range, _)| range.clone()).collect(),
+ HighlightStyle {
+ background_color: Some(background_color),
+ ..Default::default()
+ },
+ cx,
);
- inlay_ids.push(inlay.id);
- inlays.push(inlay);
}
-
- self.splice_inlays(vec![], inlays, cx);
- } else {
- let background_color = cx.theme().status().deleted_background;
- self.highlight_text::<InlineCompletionHighlight>(
- edits.iter().map(|(range, _)| range.clone()).collect(),
- HighlightStyle {
- background_color: Some(background_color),
- ..Default::default()
- },
- cx,
- );
}
invalidation_row_range = edit_start_row..edit_end_row;
let display_mode = if all_edits_insertions_or_deletions(&edits, &multibuffer) {
- if provider.show_tab_accept_marker()
- && first_edit_start_point.row == last_edit_end_point.row
- && !edits.iter().any(|(_, edit)| edit.contains('\n'))
- {
+ if provider.show_tab_accept_marker() {
EditDisplayMode::TabAccept
} else {
EditDisplayMode::Inline
@@ -5132,8 +5144,6 @@ impl Editor {
EditDisplayMode::DiffPopover
};
- let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?;
-
InlineCompletion::Edit {
edits,
edit_preview: inline_completion.edit_preview,
@@ -5149,69 +5159,18 @@ impl Editor {
multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)),
));
+ self.stale_inline_completion_in_menu = None;
self.active_inline_completion = Some(InlineCompletionState {
inlay_ids,
completion,
invalidation_range,
});
- if self.show_inline_completions_in_menu(cx) && self.has_active_completions_menu() {
- if let Some(hint) = self.inline_completion_menu_hint(window, cx) {
- match self.context_menu.borrow_mut().as_mut() {
- Some(CodeContextMenu::Completions(menu)) => {
- menu.show_inline_completion_hint(hint);
- }
- _ => {}
- }
- }
- }
-
cx.notify();
Some(())
}
- fn inline_completion_menu_hint(
- &self,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Option<InlineCompletionMenuHint> {
- let provider = self.inline_completion_provider()?;
- if self.has_active_inline_completion() {
- let editor_snapshot = self.snapshot(window, cx);
-
- let text = match &self.active_inline_completion.as_ref()?.completion {
- InlineCompletion::Edit {
- edits,
- edit_preview,
- display_mode: _,
- snapshot,
- } => edit_preview
- .as_ref()
- .and_then(|edit_preview| {
- inline_completion_edit_text(&snapshot, &edits, edit_preview, true, cx)
- })
- .map(InlineCompletionText::Edit),
- InlineCompletion::Move(target) => {
- let target_point =
- target.to_point(&editor_snapshot.display_snapshot.buffer_snapshot);
- let target_line = target_point.row + 1;
- Some(InlineCompletionText::Move(
- format!("Jump to edit in line {}", target_line).into(),
- ))
- }
- };
-
- Some(InlineCompletionMenuHint::Loaded { text: text? })
- } else if provider.is_refreshing(cx) {
- Some(InlineCompletionMenuHint::Loading)
- } else if provider.needs_terms_acceptance(cx) {
- Some(InlineCompletionMenuHint::PendingTermsAcceptance)
- } else {
- Some(InlineCompletionMenuHint::None)
- }
- }
-
pub fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
Some(self.inline_completion_provider.as_ref()?.provider.clone())
}
@@ -5439,7 +5398,6 @@ impl Editor {
}))
}
- #[cfg(any(test, feature = "test-support"))]
pub fn context_menu_visible(&self) -> bool {
self.context_menu
.borrow()
@@ -5447,26 +5405,300 @@ impl Editor {
.map_or(false, |menu| menu.visible())
}
- #[cfg(feature = "test-support")]
- pub fn context_menu_contains_inline_completion(&self) -> bool {
+ fn context_menu_origin(&self) -> Option<ContextMenuOrigin> {
self.context_menu
.borrow()
.as_ref()
- .map_or(false, |menu| match menu {
- CodeContextMenu::Completions(menu) => {
- menu.entries.borrow().first().map_or(false, |entry| {
- matches!(entry, CompletionEntry::InlineCompletionHint(_))
- })
+ .map(|menu| menu.origin())
+ }
+
+ fn edit_prediction_cursor_popover_height(&self) -> Pixels {
+ px(32.)
+ }
+
+ fn current_user_player_color(&self, cx: &mut App) -> PlayerColor {
+ if self.read_only(cx) {
+ cx.theme().players().read_only()
+ } else {
+ self.style.as_ref().unwrap().local_player
+ }
+ }
+
+ fn render_edit_prediction_cursor_popover(
+ &self,
+ max_width: Pixels,
+ cursor_point: Point,
+ style: &EditorStyle,
+ accept_keystroke: &gpui::Keystroke,
+ window: &Window,
+ cx: &mut Context<Editor>,
+ ) -> Option<AnyElement> {
+ let provider = self.inline_completion_provider.as_ref()?;
+
+ if provider.provider.needs_terms_acceptance(cx) {
+ return Some(
+ h_flex()
+ .h(self.edit_prediction_cursor_popover_height())
+ .flex_1()
+ .px_2()
+ .gap_3()
+ .elevation_2(cx)
+ .hover(|style| style.bg(cx.theme().colors().element_hover))
+ .id("accept-terms")
+ .cursor_pointer()
+ .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
+ .on_click(cx.listener(|this, _event, window, cx| {
+ cx.stop_propagation();
+ this.toggle_zed_predict_onboarding(window, cx)
+ }))
+ .child(
+ h_flex()
+ .w_full()
+ .gap_2()
+ .child(Icon::new(IconName::ZedPredict))
+ .child(Label::new("Accept Terms of Service"))
+ .child(div().w_full())
+ .child(Icon::new(IconName::ArrowUpRight))
+ .into_any_element(),
+ )
+ .into_any(),
+ );
+ }
+
+ let is_refreshing = provider.provider.is_refreshing(cx);
+
+ fn pending_completion_container() -> Div {
+ h_flex().gap_3().child(Icon::new(IconName::ZedPredict))
+ }
+
+ let completion = match &self.active_inline_completion {
+ Some(completion) => self.render_edit_prediction_cursor_popover_preview(
+ completion,
+ cursor_point,
+ style,
+ cx,
+ )?,
+
+ None if is_refreshing => match &self.stale_inline_completion_in_menu {
+ Some(stale_completion) => self.render_edit_prediction_cursor_popover_preview(
+ stale_completion,
+ cursor_point,
+ style,
+ cx,
+ )?,
+
+ None => {
+ pending_completion_container().child(Label::new("...").size(LabelSize::Small))
}
- CodeContextMenu::CodeActions(_) => false,
- })
+ },
+
+ None => pending_completion_container().child(Label::new("No Prediction")),
+ };
+
+ let buffer_font = theme::ThemeSettings::get_global(cx).buffer_font.clone();
+ let completion = completion.font(buffer_font.clone());
+
+ let completion = if is_refreshing {
+ completion
+ .with_animation(
+ "loading-completion",
+ Animation::new(Duration::from_secs(2))
+ .repeat()
+ .with_easing(pulsating_between(0.4, 0.8)),
+ |label, delta| label.opacity(delta),
+ )
+ .into_any_element()
+ } else {
+ completion.into_any_element()
+ };
+
+ let has_completion = self.active_inline_completion.is_some();
+
+ Some(
+ h_flex()
+ .h(self.edit_prediction_cursor_popover_height())
+ .max_w(max_width)
+ .flex_1()
+ .px_2()
+ .gap_3()
+ .elevation_2(cx)
+ .child(completion)
+ .child(div().w_full())
+ .child(
+ h_flex()
+ .border_l_1()
+ .border_color(cx.theme().colors().border_variant)
+ .pl_2()
+ .child(
+ h_flex()
+ .font(buffer_font.clone())
+ .p_1()
+ .rounded_sm()
+ .children(ui::render_modifiers(
+ &accept_keystroke.modifiers,
+ PlatformStyle::platform(),
+ if window.modifiers() == accept_keystroke.modifiers {
+ Some(Color::Accent)
+ } else {
+ None
+ },
+ )),
+ )
+ .opacity(if has_completion { 1.0 } else { 0.1 })
+ .child(
+ if self
+ .active_inline_completion
+ .as_ref()
+ .map_or(false, |c| c.is_move())
+ {
+ div()
+ .child(ui::Key::new(&accept_keystroke.key, None))
+ .font(buffer_font.clone())
+ .into_any()
+ } else {
+ Label::new("Preview").color(Color::Muted).into_any_element()
+ },
+ ),
+ )
+ .into_any(),
+ )
}
- fn context_menu_origin(&self, cursor_position: DisplayPoint) -> Option<ContextMenuOrigin> {
- self.context_menu
- .borrow()
- .as_ref()
- .map(|menu| menu.origin(cursor_position))
+ fn render_edit_prediction_cursor_popover_preview(
+ &self,
+ completion: &InlineCompletionState,
+ cursor_point: Point,
+ style: &EditorStyle,
+ cx: &mut Context<Editor>,
+ ) -> Option<Div> {
+ use text::ToPoint as _;
+
+ fn render_relative_row_jump(
+ prefix: impl Into<String>,
+ current_row: u32,
+ target_row: u32,
+ ) -> Div {
+ let (row_diff, arrow) = if target_row < current_row {
+ (current_row - target_row, IconName::ArrowUp)
+ } else {
+ (target_row - current_row, IconName::ArrowDown)
+ };
+
+ h_flex()
+ .child(
+ Label::new(format!("{}{}", prefix.into(), row_diff))
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ )
+ .child(Icon::new(arrow).color(Color::Muted).size(IconSize::Small))
+ }
+
+ match &completion.completion {
+ InlineCompletion::Edit {
+ edits,
+ edit_preview,
+ snapshot,
+ display_mode: _,
+ } => {
+ let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row;
+
+ let highlighted_edits = crate::inline_completion_edit_text(
+ &snapshot,
+ &edits,
+ edit_preview.as_ref()?,
+ true,
+ cx,
+ );
+
+ let len_total = highlighted_edits.text.len();
+ let first_line = &highlighted_edits.text
+ [..highlighted_edits.text.find('\n').unwrap_or(len_total)];
+ let first_line_len = first_line.len();
+
+ let first_highlight_start = highlighted_edits
+ .highlights
+ .first()
+ .map_or(0, |(range, _)| range.start);
+ let drop_prefix_len = first_line
+ .char_indices()
+ .find(|(_, c)| !c.is_whitespace())
+ .map_or(first_highlight_start, |(ix, _)| {
+ ix.min(first_highlight_start)
+ });
+
+ let preview_text = &first_line[drop_prefix_len..];
+ let preview_len = preview_text.len();
+ let highlights = highlighted_edits
+ .highlights
+ .into_iter()
+ .take_until(|(range, _)| range.start > first_line_len)
+ .map(|(range, style)| {
+ (
+ range.start - drop_prefix_len
+ ..(range.end - drop_prefix_len).min(preview_len),
+ style,
+ )
+ });
+
+ let styled_text = gpui::StyledText::new(SharedString::new(preview_text))
+ .with_highlights(&style.text, highlights);
+
+ let preview = h_flex()
+ .gap_1()
+ .child(styled_text)
+ .when(len_total > first_line_len, |parent| parent.child("…"));
+
+ let left = if first_edit_row != cursor_point.row {
+ render_relative_row_jump("", cursor_point.row, first_edit_row)
+ .into_any_element()
+ } else {
+ Icon::new(IconName::ZedPredict).into_any_element()
+ };
+
+ Some(h_flex().gap_3().child(left).child(preview))
+ }
+
+ InlineCompletion::Move {
+ target,
+ range_around_target,
+ snapshot,
+ } => {
+ let mut highlighted_text = snapshot.highlighted_text_for_range(
+ range_around_target.clone(),
+ None,
+ &style.syntax,
+ );
+ let cursor_color = self.current_user_player_color(cx).cursor;
+ let target_offset =
+ text::ToOffset::to_offset(&target.text_anchor, &snapshot).saturating_sub(
+ text::ToOffset::to_offset(&range_around_target.start, &snapshot),
+ );
+ highlighted_text.highlights = gpui::combine_highlights(
+ highlighted_text.highlights,
+ iter::once((
+ target_offset..target_offset + 1,
+ HighlightStyle {
+ background_color: Some(cursor_color),
+ ..Default::default()
+ },
+ )),
+ )
+ .collect::<Vec<_>>();
+
+ Some(
+ h_flex()
+ .gap_3()
+ .child(render_relative_row_jump(
+ "Jump ",
+ cursor_point.row,
+ target.text_anchor.to_point(&snapshot).row,
+ ))
+ .when(!highlighted_text.text.is_empty(), |parent| {
+ parent.child(highlighted_text.to_styled_text(&style.text))
+ }),
+ )
+ }
+ }
}
fn render_context_menu(
@@ -5477,13 +5709,12 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Editor>,
) -> Option<AnyElement> {
- self.context_menu.borrow().as_ref().and_then(|menu| {
- if menu.visible() {
- Some(menu.render(style, max_height_in_lines, y_flipped, window, cx))
- } else {
- None
- }
- })
+ let menu = self.context_menu.borrow();
+ let menu = menu.as_ref()?;
+ if !menu.visible() {
+ return None;
+ };
+ Some(menu.render(style, max_height_in_lines, y_flipped, window, cx))
}
fn render_context_menu_aside(
@@ -5514,7 +5745,8 @@ impl Editor {
cx.notify();
self.completion_tasks.clear();
let context_menu = self.context_menu.borrow_mut().take();
- if context_menu.is_some() && !self.show_inline_completions_in_menu(cx) {
+ self.stale_inline_completion_in_menu.take();
+ if context_menu.is_some() {
self.update_visible_inline_completion(window, cx);
}
context_menu
@@ -15859,7 +16091,7 @@ fn inline_completion_edit_text(
edit_preview: &EditPreview,
include_deletions: bool,
cx: &App,
-) -> Option<HighlightedText> {
+) -> HighlightedText {
let edits = edits
.iter()
.map(|(anchor, text)| {
@@ -15870,7 +16102,7 @@ fn inline_completion_edit_text(
})
.collect::<Vec<_>>();
- Some(edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx))
+ edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx)
}
pub fn highlight_diagnostic_message(
@@ -11707,10 +11707,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
.entries
.borrow()
.iter()
- .flat_map(|c| match c {
- CompletionEntry::Match(mat) => Some(mat.string.clone()),
- _ => None,
- })
+ .map(|mat| mat.string.clone())
.collect::<Vec<String>>(),
items_out
.iter()
@@ -11852,13 +11849,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
let entries = menu.entries.borrow();
- entries
- .iter()
- .flat_map(|e| match e {
- CompletionEntry::Match(mat) => Some(mat.string.clone()),
- _ => None,
- })
- .collect()
+ entries.iter().map(|mat| mat.string.clone()).collect()
}
#[gpui::test]
@@ -15469,8 +15460,7 @@ async fn assert_highlighted_edits(
&edit_preview,
include_deletions,
cx,
- )
- .expect("Missing highlighted edits");
+ );
assertion_fn(highlighted_edits, cx)
});
}
@@ -32,11 +32,12 @@ use gpui::{
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds,
ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase,
- Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
- Hsla, InteractiveElement, IntoElement, Length, ModifiersChangedEvent, MouseButton,
- MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
- ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
- Subscription, TextRun, TextStyleRefinement, WeakEntity, Window,
+ Edges, Element, ElementInputHandler, Entity, FocusHandle, Focusable as _, FontId,
+ GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Keystroke, Length,
+ ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
+ ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
+ StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement,
+ WeakEntity, Window,
};
use itertools::Itertools;
use language::{
@@ -525,6 +526,8 @@ impl EditorElement {
window: &mut Window,
cx: &mut Context<Editor>,
) {
+ editor.update_inline_completion_preview(&event.modifiers, window, cx);
+
let mouse_position = window.mouse_position();
if !text_hitbox.is_hovered(window) {
return;
@@ -1010,12 +1013,7 @@ impl EditorElement {
layouts.push(layout);
}
- let player = if editor.read_only(cx) {
- cx.theme().players().read_only()
- } else {
- self.style.local_player
- };
-
+ let player = editor.current_user_player_color(cx);
selections.push((player, layouts));
}
@@ -1077,11 +1075,6 @@ impl EditorElement {
selections.extend(remote_selections.into_values());
} else if !editor.is_focused(window) && editor.show_cursor_when_unfocused {
- let player = if editor.read_only(cx) {
- cx.theme().players().read_only()
- } else {
- self.style.local_player
- };
let layouts = snapshot
.buffer_snapshot
.selections_in_range(&(start_anchor..end_anchor), true)
@@ -1097,6 +1090,7 @@ impl EditorElement {
)
})
.collect::<Vec<_>>();
+ let player = editor.current_user_player_color(cx);
selections.push((player, layouts));
}
});
@@ -3157,7 +3151,7 @@ impl EditorElement {
}
#[allow(clippy::too_many_arguments)]
- fn layout_context_menu(
+ fn layout_cursor_popovers(
&self,
line_height: Pixels,
text_hitbox: &Hitbox,
@@ -3165,63 +3159,296 @@ impl EditorElement {
start_row: DisplayRow,
scroll_pixel_position: gpui::Point<Pixels>,
line_layouts: &[LineWithInvisibles],
- newest_selection_head: DisplayPoint,
+ cursor: DisplayPoint,
+ cursor_point: Point,
+ style: &EditorStyle,
+ window: &mut Window,
+ cx: &mut App,
+ ) {
+ let mut min_menu_height = Pixels::ZERO;
+ let mut max_menu_height = Pixels::ZERO;
+ let mut height_above_menu = Pixels::ZERO;
+ let height_below_menu = Pixels::ZERO;
+ let mut edit_prediction_popover_visible = false;
+ let mut context_menu_visible = false;
+
+ {
+ let editor = self.editor.read(cx);
+ if editor.has_active_completions_menu() && editor.show_inline_completions_in_menu(cx) {
+ height_above_menu +=
+ editor.edit_prediction_cursor_popover_height() + POPOVER_Y_PADDING;
+ edit_prediction_popover_visible = true;
+ }
+
+ if editor.context_menu_visible() {
+ if let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin() {
+ min_menu_height += line_height * 3. + POPOVER_Y_PADDING;
+ max_menu_height += line_height * 12. + POPOVER_Y_PADDING;
+ context_menu_visible = true;
+ }
+ }
+ }
+
+ let visible = edit_prediction_popover_visible || context_menu_visible;
+ if !visible {
+ return;
+ }
+
+ let cursor_row_layout = &line_layouts[cursor.row().minus(start_row) as usize];
+ let target_position = content_origin
+ + gpui::Point {
+ x: cmp::max(
+ px(0.),
+ cursor_row_layout.x_for_index(cursor.column() as usize)
+ - scroll_pixel_position.x,
+ ),
+ y: cmp::max(
+ px(0.),
+ cursor.row().next_row().as_f32() * line_height - scroll_pixel_position.y,
+ ),
+ };
+
+ let viewport_bounds =
+ Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
+ right: -Self::SCROLLBAR_WIDTH - MENU_GAP,
+ ..Default::default()
+ });
+
+ let min_height = height_above_menu + min_menu_height + height_below_menu;
+ let max_height = height_above_menu + max_menu_height + height_below_menu;
+ let Some((laid_out_popovers, y_flipped)) = self.layout_popovers_above_or_below_line(
+ target_position,
+ line_height,
+ min_height,
+ max_height,
+ text_hitbox,
+ viewport_bounds,
+ window,
+ cx,
+ |height, max_width_for_stable_x, y_flipped, window, cx| {
+ // First layout the menu to get its size - others can be at least this wide.
+ let context_menu = if context_menu_visible {
+ let menu_height = if y_flipped {
+ height - height_below_menu
+ } else {
+ height - height_above_menu
+ };
+ let mut element = self
+ .render_context_menu(line_height, menu_height, y_flipped, window, cx)
+ .unwrap();
+ let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
+ Some((CursorPopoverType::CodeContextMenu, element, size))
+ } else {
+ None
+ };
+ let max_width = max_width_for_stable_x.max(
+ context_menu
+ .as_ref()
+ .map_or(px(0.), |(_, _, size)| size.width),
+ );
+ let edit_prediction = if edit_prediction_popover_visible {
+ let accept_keystroke: Option<Keystroke>;
+
+ // TODO: load modifier from keymap.
+ // `bindings_for_action_in` returns `None` in Linux, and is intermittent on macOS
+ #[cfg(target_os = "macos")]
+ {
+ // let bindings = window.bindings_for_action_in(
+ // &crate::AcceptInlineCompletion,
+ // &self.editor.focus_handle(cx),
+ // );
+
+ // let last_binding = bindings.last();
+
+ // accept_keystroke = if let Some(binding) = last_binding {
+ // match &binding.keystrokes() {
+ // // TODO: no need to clone once this logic works on linux.
+ // [keystroke] => Some(keystroke.clone()),
+ // _ => None,
+ // }
+ // } else {
+ // None
+ // };
+ accept_keystroke = Some(Keystroke {
+ modifiers: gpui::Modifiers {
+ alt: true,
+ control: false,
+ shift: false,
+ platform: false,
+ function: false,
+ },
+ key: "tab".to_string(),
+ key_char: None,
+ });
+ }
+
+ #[cfg(not(target_os = "macos"))]
+ {
+ accept_keystroke = Some(Keystroke {
+ modifiers: gpui::Modifiers {
+ alt: true,
+ control: false,
+ shift: false,
+ platform: false,
+ function: false,
+ },
+ key: "enter".to_string(),
+ key_char: None,
+ });
+ }
+
+ self.editor.update(cx, move |editor, cx| {
+ let mut element = editor.render_edit_prediction_cursor_popover(
+ max_width,
+ cursor_point,
+ style,
+ accept_keystroke.as_ref()?,
+ window,
+ cx,
+ )?;
+ let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
+ Some((CursorPopoverType::EditPrediction, element, size))
+ })
+ } else {
+ None
+ };
+ vec![edit_prediction, context_menu]
+ .into_iter()
+ .flatten()
+ .collect::<Vec<_>>()
+ },
+ ) else {
+ return;
+ };
+
+ let Some((_, menu_bounds)) = laid_out_popovers
+ .iter()
+ .find(|(x, _)| matches!(x, CursorPopoverType::CodeContextMenu))
+ else {
+ return;
+ };
+ let first_popover_bounds = laid_out_popovers[0].1;
+ let last_popover_bounds = laid_out_popovers[laid_out_popovers.len() - 1].1;
+
+ let mut target_bounds = if y_flipped {
+ Bounds::from_corners(
+ last_popover_bounds.origin,
+ first_popover_bounds.bottom_right(),
+ )
+ } else {
+ Bounds::from_corners(
+ first_popover_bounds.origin,
+ last_popover_bounds.bottom_right(),
+ )
+ };
+ target_bounds.size.width = menu_bounds.size.width;
+
+ let mut max_target_bounds = target_bounds;
+ max_target_bounds.size.height = max_height;
+ if y_flipped {
+ max_target_bounds.origin.y -= max_height - target_bounds.size.height;
+ }
+
+ let mut extend_amount = Edges::all(MENU_GAP);
+ if y_flipped {
+ extend_amount.bottom = line_height;
+ } else {
+ extend_amount.top = line_height;
+ }
+ let target_bounds = target_bounds.extend(extend_amount);
+ let max_target_bounds = max_target_bounds.extend(extend_amount);
+
+ self.layout_context_menu_aside(
+ y_flipped,
+ *menu_bounds,
+ target_bounds,
+ max_target_bounds,
+ max_menu_height,
+ text_hitbox,
+ viewport_bounds,
+ window,
+ cx,
+ );
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn layout_gutter_menu(
+ &self,
+ line_height: Pixels,
+ text_hitbox: &Hitbox,
+ content_origin: gpui::Point<Pixels>,
+ scroll_pixel_position: gpui::Point<Pixels>,
gutter_overshoot: Pixels,
window: &mut Window,
cx: &mut App,
) {
- let Some(context_menu_origin) = self
- .editor
- .read(cx)
- .context_menu_origin(newest_selection_head)
+ let Some(crate::ContextMenuOrigin::GutterIndicator(gutter_row)) =
+ self.editor.read(cx).context_menu_origin()
else {
return;
};
+ // Context menu was spawned via a click on a gutter. Ensure it's a bit closer to the
+ // indicator than just a plain first column of the text field.
let target_position = content_origin
- + match context_menu_origin {
- crate::ContextMenuOrigin::EditorPoint(display_point) => {
- let cursor_row_layout =
- &line_layouts[display_point.row().minus(start_row) as usize];
- gpui::Point {
- x: cmp::max(
- px(0.),
- cursor_row_layout.x_for_index(display_point.column() as usize)
- - scroll_pixel_position.x,
- ),
- y: cmp::max(
- px(0.),
- display_point.row().next_row().as_f32() * line_height
- - scroll_pixel_position.y,
- ),
- }
- }
- crate::ContextMenuOrigin::GutterIndicator(row) => {
- // Context menu was spawned via a click on a gutter. Ensure it's a bit closer to the indicator than just a plain first column of the
- // text field.
- gpui::Point {
- x: -gutter_overshoot,
- y: row.next_row().as_f32() * line_height - scroll_pixel_position.y,
- }
- }
+ + gpui::Point {
+ x: -gutter_overshoot,
+ y: gutter_row.next_row().as_f32() * line_height - scroll_pixel_position.y,
};
-
+ let min_height = line_height * 3. + POPOVER_Y_PADDING;
+ let max_height = line_height * 12. + POPOVER_Y_PADDING;
let viewport_bounds =
Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
right: -Self::SCROLLBAR_WIDTH - MENU_GAP,
..Default::default()
});
+ self.layout_popovers_above_or_below_line(
+ target_position,
+ line_height,
+ min_height,
+ max_height,
+ text_hitbox,
+ viewport_bounds,
+ window,
+ cx,
+ move |height, _max_width_for_stable_x, y_flipped, window, cx| {
+ let Some(mut element) =
+ self.render_context_menu(line_height, height, y_flipped, window, cx)
+ else {
+ return vec![];
+ };
+ let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
+ vec![(CursorPopoverType::CodeContextMenu, element, size)]
+ },
+ );
+ }
- // If the context menu's max height won't fit below, then flip it above the line and display
- // it in reverse order. If the available space above is less than below.
- let unconstrained_max_height = line_height * 12. + POPOVER_Y_PADDING;
- let min_height = line_height * 3. + POPOVER_Y_PADDING;
+ #[allow(clippy::too_many_arguments)]
+ fn layout_popovers_above_or_below_line(
+ &self,
+ target_position: gpui::Point<Pixels>,
+ line_height: Pixels,
+ min_height: Pixels,
+ max_height: Pixels,
+ text_hitbox: &Hitbox,
+ viewport_bounds: Bounds<Pixels>,
+ window: &mut Window,
+ cx: &mut App,
+ make_sized_popovers: impl FnOnce(
+ Pixels,
+ Pixels,
+ bool,
+ &mut Window,
+ &mut App,
+ ) -> Vec<(CursorPopoverType, AnyElement, Size<Pixels>)>,
+ ) -> Option<(Vec<(CursorPopoverType, Bounds<Pixels>)>, bool)> {
+ // If the max height won't fit below and there is more space above, put it above the line.
let bottom_y_when_flipped = target_position.y - line_height;
let available_above = bottom_y_when_flipped - text_hitbox.top();
let available_below = text_hitbox.bottom() - target_position.y;
- let y_overflows_below = unconstrained_max_height > available_below;
+ let y_overflows_below = max_height > available_below;
let mut y_flipped = y_overflows_below && available_above > available_below;
let mut height = cmp::min(
- unconstrained_max_height,
+ max_height,
if y_flipped {
available_above
} else {
@@ -3229,14 +3456,14 @@ impl EditorElement {
},
);
- // If less than 3 lines fit within the text bounds, instead fit within the window.
+ // If the min height doesn't fit within text bounds, instead fit within the window.
if height < min_height {
let available_above = bottom_y_when_flipped;
let available_below = viewport_bounds.bottom() - target_position.y;
- if available_below > 3. * line_height {
+ if available_below > min_height {
y_flipped = false;
height = min_height;
- } else if available_above > 3. * line_height {
+ } else if available_above > min_height {
y_flipped = true;
height = min_height;
} else if available_above > available_below {
@@ -3248,82 +3475,67 @@ impl EditorElement {
}
}
- let max_height_in_lines = ((height - POPOVER_Y_PADDING) / line_height).floor() as u32;
+ let max_width_for_stable_x = viewport_bounds.right() - target_position.x;
- // TODO(mgsloan): use viewport_bounds.width as a max width when rendering menu.
- let Some(mut menu_element) = self.editor.update(cx, |editor, cx| {
- editor.render_context_menu(&self.style, max_height_in_lines, y_flipped, window, cx)
- }) else {
- return;
- };
+ // TODO: Use viewport_bounds.width as a max width so that it doesn't get clipped on the left
+ // for very narrow windows.
+ let popovers = make_sized_popovers(height, max_width_for_stable_x, y_flipped, window, cx);
+ if popovers.is_empty() {
+ return None;
+ }
+
+ let max_width = popovers
+ .iter()
+ .map(|(_, _, size)| size.width)
+ .max()
+ .unwrap_or_default();
- let menu_size = menu_element.layout_as_root(AvailableSpace::min_size(), window, cx);
- let menu_position = gpui::Point {
+ let mut current_position = gpui::Point {
// Snap the right edge of the list to the right edge of the window if its horizontal bounds
// overflow. Include space for the scrollbar.
x: target_position
.x
- .min((viewport_bounds.right() - menu_size.width).max(Pixels::ZERO)),
+ .min((viewport_bounds.right() - max_width).max(Pixels::ZERO)),
y: if y_flipped {
- bottom_y_when_flipped - menu_size.height
+ bottom_y_when_flipped
} else {
target_position.y
},
};
- window.defer_draw(menu_element, menu_position, 1);
-
- // Layout documentation aside
- let menu_bounds = Bounds::new(menu_position, menu_size);
- let max_menu_size = size(menu_size.width, unconstrained_max_height);
- let max_menu_bounds = if y_flipped {
- Bounds::new(
- point(
- menu_position.x,
- bottom_y_when_flipped - max_menu_size.height,
- ),
- max_menu_size,
- )
- } else {
- Bounds::new(target_position, max_menu_size)
- };
- self.layout_context_menu_aside(
- text_hitbox,
- y_flipped,
- menu_position,
- menu_bounds,
- max_menu_bounds,
- unconstrained_max_height,
- line_height,
- viewport_bounds,
- window,
- cx,
- );
+
+ let laid_out_popovers = popovers
+ .into_iter()
+ .map(|(popover_type, element, size)| {
+ if y_flipped {
+ current_position.y -= size.height;
+ }
+ let position = current_position;
+ window.defer_draw(element, current_position, 1);
+ if !y_flipped {
+ current_position.y += size.height + MENU_GAP;
+ } else {
+ current_position.y -= MENU_GAP;
+ }
+ (popover_type, Bounds::new(position, size))
+ })
+ .collect::<Vec<_>>();
+
+ Some((laid_out_popovers, y_flipped))
}
#[allow(clippy::too_many_arguments)]
fn layout_context_menu_aside(
&self,
- text_hitbox: &Hitbox,
y_flipped: bool,
- menu_position: gpui::Point<Pixels>,
menu_bounds: Bounds<Pixels>,
- max_menu_bounds: Bounds<Pixels>,
+ target_bounds: Bounds<Pixels>,
+ max_target_bounds: Bounds<Pixels>,
max_height: Pixels,
- line_height: Pixels,
+ text_hitbox: &Hitbox,
viewport_bounds: Bounds<Pixels>,
window: &mut Window,
cx: &mut App,
) {
- let mut extend_amount = Edges::all(MENU_GAP);
- // Extend to include the cursored line to avoid overlapping it.
- if y_flipped {
- extend_amount.bottom = line_height;
- } else {
- extend_amount.top = line_height;
- }
- let target_bounds = menu_bounds.extend(extend_amount);
- let max_target_bounds = max_menu_bounds.extend(extend_amount);
-
let available_within_viewport = target_bounds.space_within(&viewport_bounds);
let positioned_aside = if available_within_viewport.right >= MENU_ASIDE_MIN_WIDTH {
let max_width = cmp::min(
@@ -3336,7 +3548,7 @@ impl EditorElement {
return;
};
aside.layout_as_root(AvailableSpace::min_size(), window, cx);
- let right_position = point(target_bounds.right(), menu_position.y);
+ let right_position = point(target_bounds.right(), menu_bounds.origin.y);
Some((aside, right_position))
} else {
let max_size = size(
@@ -3359,8 +3571,11 @@ impl EditorElement {
};
let actual_size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
- let top_position = point(menu_position.x, target_bounds.top() - actual_size.height);
- let bottom_position = point(menu_position.x, target_bounds.bottom());
+ let top_position = point(
+ menu_bounds.origin.x,
+ target_bounds.top() - actual_size.height,
+ );
+ let bottom_position = point(menu_bounds.origin.x, target_bounds.bottom());
let fit_within = |available: Edges<Pixels>, wanted: Size<Pixels>| {
// Prefer to fit on the same side of the line as the menu, then on the other side of
@@ -3396,6 +3611,20 @@ impl EditorElement {
}
}
+ fn render_context_menu(
+ &self,
+ line_height: Pixels,
+ height: Pixels,
+ y_flipped: bool,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<AnyElement> {
+ let max_height_in_lines = ((height - POPOVER_Y_PADDING) / line_height).floor() as u32;
+ self.editor.update(cx, |editor, cx| {
+ editor.render_context_menu(&self.style, max_height_in_lines, y_flipped, window, cx)
+ })
+ }
+
fn render_context_menu_aside(
&self,
max_size: Size<Pixels>,
@@ -3434,12 +3663,14 @@ impl EditorElement {
let active_inline_completion = self.editor.read(cx).active_inline_completion.as_ref()?;
match &active_inline_completion.completion {
- InlineCompletion::Move(target_position) => {
- let target_display_point = target_position.to_display_point(editor_snapshot);
+ InlineCompletion::Move { target, .. } => {
+ let target_display_point = target.to_display_point(editor_snapshot);
if target_display_point.row().as_f32() < scroll_top {
- let mut element = inline_completion_tab_indicator(
+ let mut element = inline_completion_accept_indicator(
"Jump to Edit",
Some(IconName::ArrowUp),
+ self.editor.focus_handle(cx),
+ window,
cx,
);
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
@@ -3447,9 +3678,11 @@ impl EditorElement {
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_tab_indicator(
+ let mut element = inline_completion_accept_indicator(
"Jump to Edit",
Some(IconName::ArrowDown),
+ self.editor.focus_handle(cx),
+ window,
cx,
);
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
@@ -3460,7 +3693,13 @@ impl EditorElement {
element.prepaint_at(text_bounds.origin + offset, window, cx);
Some(element)
} else {
- let mut element = inline_completion_tab_indicator("Jump to Edit", None, cx);
+ let mut element = inline_completion_accept_indicator(
+ "Jump to Edit",
+ None,
+ self.editor.focus_handle(cx),
+ window,
+ cx,
+ );
let target_line_end = DisplayPoint::new(
target_display_point.row(),
@@ -3520,7 +3759,13 @@ impl EditorElement {
editor.display_to_pixel_point(target_line_end, editor_snapshot, window)
})?;
- let mut element = inline_completion_tab_indicator("Accept", None, cx);
+ let mut element = inline_completion_accept_indicator(
+ "Accept",
+ None,
+ self.editor.focus_handle(cx),
+ window,
+ cx,
+ );
element.prepaint_as_root(
text_bounds.origin + origin + point(PADDING_X, px(0.)),
@@ -3535,9 +3780,13 @@ impl EditorElement {
EditDisplayMode::DiffPopover => {}
}
- let highlighted_edits = edit_preview.as_ref().and_then(|edit_preview| {
- crate::inline_completion_edit_text(&snapshot, edits, edit_preview, false, cx)
- })?;
+ let highlighted_edits = crate::inline_completion_edit_text(
+ &snapshot,
+ edits,
+ edit_preview.as_ref()?,
+ false,
+ cx,
+ );
let line_count = highlighted_edits.text.lines().count();
@@ -3558,8 +3807,7 @@ impl EditorElement {
.width
};
- let styled_text = gpui::StyledText::new(highlighted_edits.text.clone())
- .with_highlights(&style.text, highlighted_edits.highlights);
+ let styled_text = highlighted_edits.to_styled_text(&style.text);
let mut element = div()
.bg(cx.theme().colors().editor_background)
@@ -5548,17 +5796,33 @@ fn header_jump_data(
}
}
-fn inline_completion_tab_indicator(
+fn inline_completion_accept_indicator(
label: impl Into<SharedString>,
icon: Option<IconName>,
+ focus_handle: FocusHandle,
+ window: &Window,
cx: &App,
) -> AnyElement {
- let tab_kbd = h_flex()
+ let bindings = window.bindings_for_action_in(&crate::AcceptInlineCompletion, &focus_handle);
+ let Some(accept_keystroke) = bindings
+ .last()
+ .and_then(|binding| binding.keystrokes().first())
+ else {
+ return div().into_any();
+ };
+
+ 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)
- .child("tab");
+ .gap_1()
+ .children(ui::render_modifiers(
+ &accept_keystroke.modifiers,
+ PlatformStyle::platform(),
+ Some(Color::Default),
+ ))
+ .child(accept_keystroke.key.clone());
let padding_right = if icon.is_some() { px(4.) } else { px(8.) };
@@ -5572,7 +5836,7 @@ fn inline_completion_tab_indicator(
.border_color(cx.theme().colors().text_accent.opacity(0.8))
.rounded_md()
.shadow_sm()
- .child(tab_kbd)
+ .child(accept_key)
.child(Label::new(label).size(LabelSize::Small))
.when_some(icon, |element, icon| {
element.child(
@@ -7059,8 +7323,11 @@ impl Element for EditorElement {
);
let mut code_actions_indicator = None;
if let Some(newest_selection_head) = newest_selection_head {
+ let newest_selection_point =
+ newest_selection_head.to_point(&snapshot.display_snapshot);
+
if (start_row..end_row).contains(&newest_selection_head.row()) {
- self.layout_context_menu(
+ self.layout_cursor_popovers(
line_height,
&text_hitbox,
content_origin,
@@ -7068,7 +7335,8 @@ impl Element for EditorElement {
scroll_pixel_position,
&line_layouts,
newest_selection_head,
- gutter_dimensions.width - gutter_dimensions.left_padding,
+ newest_selection_point,
+ &style,
window,
cx,
);
@@ -7113,6 +7381,16 @@ impl Element for EditorElement {
}
}
+ self.layout_gutter_menu(
+ line_height,
+ &text_hitbox,
+ content_origin,
+ scroll_pixel_position,
+ gutter_dimensions.width - gutter_dimensions.left_padding,
+ window,
+ cx,
+ );
+
let test_indicators = if gutter_settings.runnables {
self.layout_run_indicators(
line_height,
@@ -7994,6 +8272,11 @@ impl HighlightedRange {
}
}
+enum CursorPopoverType {
+ CodeContextMenu,
+ EditPrediction,
+}
+
pub fn scale_vertical_mouse_autoscroll_delta(delta: Pixels) -> f32 {
(delta.pow(1.5) / 100.0).into()
}
@@ -1253,7 +1253,7 @@ fn apply_hint_update(
editor.inlay_hint_cache.version += 1;
}
if displayed_inlays_changed {
- editor.splice_inlays(to_remove, to_insert, cx)
+ editor.splice_inlays(&to_remove, to_insert, cx)
}
}
@@ -304,8 +304,8 @@ fn assert_editor_active_move_completion(
.as_ref()
.expect("editor has no active completion");
- if let InlineCompletion::Move(anchor) = &completion_state.completion {
- assert(editor.buffer().read(cx).snapshot(cx), *anchor);
+ if let InlineCompletion::Move { target, .. } = &completion_state.completion {
+ assert(editor.buffer().read(cx).snapshot(cx), *target);
} else {
panic!("expected move completion");
}
@@ -864,7 +864,7 @@ mod tests {
})
.collect();
let snapshot = display_map.update(cx, |map, cx| {
- map.splice_inlays(Vec::new(), inlays, cx);
+ map.splice_inlays(&[], inlays, cx);
map.snapshot(cx)
});
@@ -26,7 +26,7 @@ use fs::MTime;
use futures::channel::oneshot;
use gpui::{
AnyElement, App, AppContext as _, Context, Entity, EventEmitter, HighlightStyle, Pixels,
- SharedString, Task, TaskLabel, Window,
+ SharedString, StyledText, Task, TaskLabel, TextStyle, Window,
};
use lsp::LanguageServerId;
use parking_lot::Mutex;
@@ -617,6 +617,11 @@ impl HighlightedText {
);
highlighted_text.build()
}
+
+ pub fn to_styled_text(&self, default_style: &TextStyle) -> StyledText {
+ gpui::StyledText::new(self.text.clone())
+ .with_highlights(default_style, self.highlights.iter().cloned())
+ }
}
impl HighlightedTextBuilder {
@@ -1,7 +1,9 @@
#![allow(missing_docs)]
use crate::PlatformStyle;
use crate::{h_flex, prelude::*, Icon, IconName, IconSize};
-use gpui::{relative, Action, App, FocusHandle, IntoElement, Keystroke, Window};
+use gpui::{
+ relative, Action, AnyElement, App, FocusHandle, IntoElement, Keystroke, Modifiers, Window,
+};
#[derive(Debug, IntoElement, Clone)]
pub struct KeyBinding {
@@ -41,30 +43,6 @@ impl KeyBinding {
Some(Self::new(key_binding))
}
- fn icon_for_key(&self, keystroke: &Keystroke) -> Option<IconName> {
- match keystroke.key.as_str() {
- "left" => Some(IconName::ArrowLeft),
- "right" => Some(IconName::ArrowRight),
- "up" => Some(IconName::ArrowUp),
- "down" => Some(IconName::ArrowDown),
- "backspace" => Some(IconName::Backspace),
- "delete" => Some(IconName::Delete),
- "return" => Some(IconName::Return),
- "enter" => Some(IconName::Return),
- "tab" => Some(IconName::Tab),
- "space" => Some(IconName::Space),
- "escape" => Some(IconName::Escape),
- "pagedown" => Some(IconName::PageDown),
- "pageup" => Some(IconName::PageUp),
- "shift" if self.platform_style == PlatformStyle::Mac => Some(IconName::Shift),
- "control" if self.platform_style == PlatformStyle::Mac => Some(IconName::Control),
- "platform" if self.platform_style == PlatformStyle::Mac => Some(IconName::Command),
- "function" if self.platform_style == PlatformStyle::Mac => Some(IconName::Control),
- "alt" if self.platform_style == PlatformStyle::Mac => Some(IconName::Option),
- _ => None,
- }
- }
-
pub fn new(key_binding: gpui::KeyBinding) -> Self {
Self {
key_binding,
@@ -96,63 +74,148 @@ impl RenderOnce for KeyBinding {
.gap(DynamicSpacing::Base04.rems(cx))
.flex_none()
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
- let key_icon = self.icon_for_key(keystroke);
-
h_flex()
.flex_none()
.py_0p5()
.rounded_sm()
.text_color(cx.theme().colors().text_muted)
- .when(keystroke.modifiers.function, |el| {
- match self.platform_style {
- PlatformStyle::Mac => el.child(Key::new("fn")),
- PlatformStyle::Linux | PlatformStyle::Windows => {
- el.child(Key::new("Fn")).child(Key::new("+"))
- }
- }
- })
- .when(keystroke.modifiers.control, |el| {
- match self.platform_style {
- PlatformStyle::Mac => el.child(KeyIcon::new(IconName::Control)),
- PlatformStyle::Linux | PlatformStyle::Windows => {
- el.child(Key::new("Ctrl")).child(Key::new("+"))
- }
- }
- })
- .when(keystroke.modifiers.alt, |el| match self.platform_style {
- PlatformStyle::Mac => el.child(KeyIcon::new(IconName::Option)),
- PlatformStyle::Linux | PlatformStyle::Windows => {
- el.child(Key::new("Alt")).child(Key::new("+"))
- }
- })
- .when(keystroke.modifiers.platform, |el| {
- match self.platform_style {
- PlatformStyle::Mac => el.child(KeyIcon::new(IconName::Command)),
- PlatformStyle::Linux => {
- el.child(Key::new("Super")).child(Key::new("+"))
- }
- PlatformStyle::Windows => {
- el.child(Key::new("Win")).child(Key::new("+"))
- }
- }
- })
- .when(keystroke.modifiers.shift, |el| match self.platform_style {
- PlatformStyle::Mac => el.child(KeyIcon::new(IconName::Shift)),
- PlatformStyle::Linux | PlatformStyle::Windows => {
- el.child(Key::new("Shift")).child(Key::new("+"))
- }
- })
- .map(|el| match key_icon {
- Some(icon) => el.child(KeyIcon::new(icon)),
- None => el.child(Key::new(keystroke.key.to_uppercase())),
- })
+ .children(render_modifiers(
+ &keystroke.modifiers,
+ self.platform_style,
+ None,
+ ))
+ .map(|el| el.child(render_key(&keystroke, self.platform_style, None)))
}))
}
}
+pub fn render_key(
+ keystroke: &Keystroke,
+ platform_style: PlatformStyle,
+ color: Option<Color>,
+) -> AnyElement {
+ let key_icon = icon_for_key(keystroke, platform_style);
+ match key_icon {
+ Some(icon) => KeyIcon::new(icon, color).into_any_element(),
+ None => Key::new(
+ if keystroke.key.len() > 1 {
+ keystroke.key.clone()
+ } else {
+ keystroke.key.to_uppercase()
+ },
+ color,
+ )
+ .into_any_element(),
+ }
+}
+
+fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
+ match keystroke.key.as_str() {
+ "left" => Some(IconName::ArrowLeft),
+ "right" => Some(IconName::ArrowRight),
+ "up" => Some(IconName::ArrowUp),
+ "down" => Some(IconName::ArrowDown),
+ "backspace" => Some(IconName::Backspace),
+ "delete" => Some(IconName::Delete),
+ "return" => Some(IconName::Return),
+ "enter" => Some(IconName::Return),
+ // "tab" => Some(IconName::Tab),
+ "space" => Some(IconName::Space),
+ "escape" => Some(IconName::Escape),
+ "pagedown" => Some(IconName::PageDown),
+ "pageup" => Some(IconName::PageUp),
+ "shift" if platform_style == PlatformStyle::Mac => Some(IconName::Shift),
+ "control" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
+ "platform" if platform_style == PlatformStyle::Mac => Some(IconName::Command),
+ "function" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
+ "alt" if platform_style == PlatformStyle::Mac => Some(IconName::Option),
+ _ => None,
+ }
+}
+
+pub fn render_modifiers(
+ modifiers: &Modifiers,
+ platform_style: PlatformStyle,
+ color: Option<Color>,
+) -> impl Iterator<Item = AnyElement> {
+ enum KeyOrIcon {
+ Key(&'static str),
+ Icon(IconName),
+ }
+
+ struct Modifier {
+ enabled: bool,
+ mac: KeyOrIcon,
+ linux: KeyOrIcon,
+ windows: KeyOrIcon,
+ }
+
+ let table = {
+ use KeyOrIcon::*;
+
+ [
+ Modifier {
+ enabled: modifiers.function,
+ mac: Icon(IconName::Control),
+ linux: Key("Fn"),
+ windows: Key("Fn"),
+ },
+ Modifier {
+ enabled: modifiers.control,
+ mac: Icon(IconName::Control),
+ linux: Key("Ctrl"),
+ windows: Key("Ctrl"),
+ },
+ Modifier {
+ enabled: modifiers.alt,
+ mac: Icon(IconName::Option),
+ linux: Key("Alt"),
+ windows: Key("Alt"),
+ },
+ Modifier {
+ enabled: modifiers.platform,
+ mac: Icon(IconName::Command),
+ linux: Key("Super"),
+ windows: Key("Win"),
+ },
+ Modifier {
+ enabled: modifiers.shift,
+ mac: Icon(IconName::Shift),
+ linux: Key("Shift"),
+ windows: Key("Shift"),
+ },
+ ]
+ };
+
+ table
+ .into_iter()
+ .flat_map(move |modifier| {
+ if modifier.enabled {
+ match platform_style {
+ PlatformStyle::Mac => Some(modifier.mac),
+ PlatformStyle::Linux => Some(modifier.linux)
+ .into_iter()
+ .chain(Some(KeyOrIcon::Key("+")))
+ .next(),
+ PlatformStyle::Windows => Some(modifier.windows)
+ .into_iter()
+ .chain(Some(KeyOrIcon::Key("+")))
+ .next(),
+ }
+ } else {
+ None
+ }
+ })
+ .map(move |key_or_icon| match key_or_icon {
+ KeyOrIcon::Key(key) => Key::new(key, color).into_any_element(),
+ KeyOrIcon::Icon(icon) => KeyIcon::new(icon, color).into_any_element(),
+ })
+}
+
#[derive(IntoElement)]
pub struct Key {
key: SharedString,
+ color: Option<Color>,
}
impl RenderOnce for Key {
@@ -174,33 +237,37 @@ impl RenderOnce for Key {
.h(rems_from_px(14.))
.text_ui(cx)
.line_height(relative(1.))
- .text_color(cx.theme().colors().text_muted)
+ .text_color(self.color.unwrap_or(Color::Muted).color(cx))
.child(self.key.clone())
}
}
impl Key {
- pub fn new(key: impl Into<SharedString>) -> Self {
- Self { key: key.into() }
+ pub fn new(key: impl Into<SharedString>, color: Option<Color>) -> Self {
+ Self {
+ key: key.into(),
+ color,
+ }
}
}
#[derive(IntoElement)]
pub struct KeyIcon {
icon: IconName,
+ color: Option<Color>,
}
impl RenderOnce for KeyIcon {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
Icon::new(self.icon)
.size(IconSize::XSmall)
- .color(Color::Muted)
+ .color(self.color.unwrap_or(Color::Muted))
}
}
impl KeyIcon {
- pub fn new(icon: IconName) -> Self {
- Self { icon }
+ pub fn new(icon: IconName, color: Option<Color>) -> Self {
+ Self { icon, color }
}
}
@@ -3441,7 +3441,7 @@ mod test {
let range = editor.selections.newest_anchor().range();
let inlay_text = " field: int,\n field2: string\n field3: float";
let inlay = Inlay::inline_completion(1, range.start, inlay_text);
- editor.splice_inlays(vec![], vec![inlay], cx);
+ editor.splice_inlays(&[], vec![inlay], cx);
});
cx.simulate_keystrokes("j");
@@ -3473,7 +3473,7 @@ mod test {
snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
let inlay_text = " hint";
let inlay = Inlay::inline_completion(1, end_of_line, inlay_text);
- editor.splice_inlays(vec![], vec![inlay], cx);
+ editor.splice_inlays(&[], vec![inlay], cx);
});
cx.simulate_keystrokes("$");
cx.assert_state(
@@ -1530,6 +1530,16 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
.log_err()
.flatten()
else {
+ this.update(&mut cx, |this, cx| {
+ if this.pending_completions[0].id == pending_completion_id {
+ this.pending_completions.remove(0);
+ } else {
+ this.pending_completions.clear();
+ }
+
+ cx.notify();
+ })
+ .ok();
return;
};