zeta: Improve UX for simultaneous LSP and prediction completions (#24024)
Release Notes: - N/A --------- Co-authored-by: Michael Sloan <michael@zed.dev> Co-authored-by: Danilo <danilo@zed.dev> Co-authored-by: Richard <richard@zed.dev>
This commit is contained in:
parent
b6e680ea3d
commit
4c29e1ff07
16 changed files with 1196 additions and 762 deletions
|
@ -509,6 +509,13 @@
|
||||||
"tab": "editor::AcceptInlineCompletion"
|
"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",
|
"context": "Editor && showing_code_actions",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|
|
@ -586,6 +586,13 @@
|
||||||
"tab": "editor::AcceptInlineCompletion"
|
"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",
|
"context": "Editor && showing_code_actions",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|
|
@ -341,7 +341,6 @@ mod tests {
|
||||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
assert!(editor.context_menu_visible());
|
assert!(editor.context_menu_visible());
|
||||||
assert!(!editor.context_menu_contains_inline_completion());
|
|
||||||
assert!(!editor.has_active_inline_completion());
|
assert!(!editor.has_active_inline_completion());
|
||||||
// Since we have both, the copilot suggestion is not shown inline
|
// Since we have both, the copilot suggestion is not shown inline
|
||||||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||||
|
@ -399,7 +398,6 @@ mod tests {
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
cx.update_editor(|editor, _, cx| {
|
cx.update_editor(|editor, _, cx| {
|
||||||
assert!(!editor.context_menu_visible());
|
assert!(!editor.context_menu_visible());
|
||||||
assert!(!editor.context_menu_contains_inline_completion());
|
|
||||||
assert!(editor.has_active_inline_completion());
|
assert!(editor.has_active_inline_completion());
|
||||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||||
assert_eq!(editor.text(cx), "one.c\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| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
assert!(!editor.context_menu_visible());
|
assert!(!editor.context_menu_visible());
|
||||||
assert!(editor.has_active_inline_completion());
|
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.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||||
assert_eq!(editor.text(cx), "one.c\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);
|
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||||
cx.update_editor(|editor, _, cx| {
|
cx.update_editor(|editor, _, cx| {
|
||||||
assert!(editor.context_menu_visible());
|
assert!(editor.context_menu_visible());
|
||||||
assert!(!editor.context_menu_contains_inline_completion());
|
|
||||||
assert!(!editor.has_active_inline_completion(),);
|
assert!(!editor.has_active_inline_completion(),);
|
||||||
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
|
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, pulsating_between, px, uniform_list, Animation, AnimationExt, AnyElement,
|
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, Entity, FontWeight,
|
||||||
BackgroundExecutor, Div, Entity, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString,
|
ListSizingBehavior, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
|
||||||
Size, StrikethroughStyle, StyledText, UniformListScrollHandle, WeakEntity,
|
UniformListScrollHandle, WeakEntity,
|
||||||
};
|
};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use language::{CodeLabel, CompletionDocumentation};
|
use language::{CodeLabel, CompletionDocumentation};
|
||||||
|
@ -10,8 +10,7 @@ use lsp::LanguageServerId;
|
||||||
use multi_buffer::{Anchor, ExcerptId};
|
use multi_buffer::{Anchor, ExcerptId};
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use project::{CodeAction, Completion, TaskSourceKind};
|
use project::{CodeAction, Completion, TaskSourceKind};
|
||||||
use settings::Settings;
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
cmp::{min, Reverse},
|
cmp::{min, Reverse},
|
||||||
|
@ -26,11 +25,9 @@ use workspace::Workspace;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actions::{ConfirmCodeAction, ConfirmCompletion},
|
actions::{ConfirmCodeAction, ConfirmCompletion},
|
||||||
display_map::DisplayPoint,
|
|
||||||
render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,
|
render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,
|
||||||
CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,
|
CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,
|
||||||
};
|
};
|
||||||
use crate::{AcceptInlineCompletion, InlineCompletionMenuHint, InlineCompletionText};
|
|
||||||
|
|
||||||
pub const MENU_GAP: Pixels = px(4.);
|
pub const MENU_GAP: Pixels = px(4.);
|
||||||
pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
|
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 {
|
match self {
|
||||||
CodeContextMenu::Completions(menu) => menu.origin(cursor_position),
|
CodeContextMenu::Completions(menu) => menu.origin(),
|
||||||
CodeContextMenu::CodeActions(menu) => menu.origin(cursor_position),
|
CodeContextMenu::CodeActions(menu) => menu.origin(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,7 +151,7 @@ impl CodeContextMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum ContextMenuOrigin {
|
pub enum ContextMenuOrigin {
|
||||||
EditorPoint(DisplayPoint),
|
Cursor,
|
||||||
GutterIndicator(DisplayRow),
|
GutterIndicator(DisplayRow),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,18 +163,13 @@ pub struct CompletionsMenu {
|
||||||
pub buffer: Entity<Buffer>,
|
pub buffer: Entity<Buffer>,
|
||||||
pub completions: Rc<RefCell<Box<[Completion]>>>,
|
pub completions: Rc<RefCell<Box<[Completion]>>>,
|
||||||
match_candidates: Rc<[StringMatchCandidate]>,
|
match_candidates: Rc<[StringMatchCandidate]>,
|
||||||
pub entries: Rc<RefCell<Vec<CompletionEntry>>>,
|
pub entries: Rc<RefCell<Vec<StringMatch>>>,
|
||||||
pub selected_item: usize,
|
pub selected_item: usize,
|
||||||
scroll_handle: UniformListScrollHandle,
|
scroll_handle: UniformListScrollHandle,
|
||||||
resolve_completions: bool,
|
resolve_completions: bool,
|
||||||
show_completion_documentation: bool,
|
show_completion_documentation: bool,
|
||||||
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
|
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
|
||||||
}
|
pub previewing_inline_completion: bool,
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) enum CompletionEntry {
|
|
||||||
Match(StringMatch),
|
|
||||||
InlineCompletionHint(InlineCompletionMenuHint),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompletionsMenu {
|
impl CompletionsMenu {
|
||||||
|
@ -208,6 +200,7 @@ impl CompletionsMenu {
|
||||||
scroll_handle: UniformListScrollHandle::new(),
|
scroll_handle: UniformListScrollHandle::new(),
|
||||||
resolve_completions: true,
|
resolve_completions: true,
|
||||||
last_rendered_range: RefCell::new(None).into(),
|
last_rendered_range: RefCell::new(None).into(),
|
||||||
|
previewing_inline_completion: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,13 +237,11 @@ impl CompletionsMenu {
|
||||||
let entries = choices
|
let entries = choices
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(id, completion)| {
|
.map(|(id, completion)| StringMatch {
|
||||||
CompletionEntry::Match(StringMatch {
|
candidate_id: id,
|
||||||
candidate_id: id,
|
score: 1.,
|
||||||
score: 1.,
|
positions: vec![],
|
||||||
positions: vec![],
|
string: completion.clone(),
|
||||||
string: completion.clone(),
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
Self {
|
Self {
|
||||||
|
@ -266,6 +257,7 @@ impl CompletionsMenu {
|
||||||
resolve_completions: false,
|
resolve_completions: false,
|
||||||
show_completion_documentation: false,
|
show_completion_documentation: false,
|
||||||
last_rendered_range: RefCell::new(None).into(),
|
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(
|
pub fn resolve_visible_completions(
|
||||||
&mut self,
|
&mut self,
|
||||||
provider: Option<&dyn CompletionProvider>,
|
provider: Option<&dyn CompletionProvider>,
|
||||||
|
@ -406,17 +380,15 @@ impl CompletionsMenu {
|
||||||
// This filtering doesn't happen if the completions are currently being updated.
|
// This filtering doesn't happen if the completions are currently being updated.
|
||||||
let completions = self.completions.borrow();
|
let completions = self.completions.borrow();
|
||||||
let candidate_ids = entry_indices
|
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());
|
.filter(|i| completions[*i].documentation.is_none());
|
||||||
|
|
||||||
// Current selection is always resolved even if it already has documentation, to handle
|
// Current selection is always resolved even if it already has documentation, to handle
|
||||||
// out-of-spec language servers that return more results later.
|
// out-of-spec language servers that return more results later.
|
||||||
let candidate_ids = match Self::entry_candidate_id(&entries[self.selected_item]) {
|
let selected_candidate_id = entries[self.selected_item].candidate_id;
|
||||||
None => candidate_ids.collect::<Vec<usize>>(),
|
let candidate_ids = iter::once(selected_candidate_id)
|
||||||
Some(selected_candidate_id) => iter::once(selected_candidate_id)
|
.chain(candidate_ids.filter(|id| *id != selected_candidate_id))
|
||||||
.chain(candidate_ids.filter(|id| *id != selected_candidate_id))
|
.collect::<Vec<usize>>();
|
||||||
.collect::<Vec<usize>>(),
|
|
||||||
};
|
|
||||||
drop(entries);
|
drop(entries);
|
||||||
|
|
||||||
if candidate_ids.is_empty() {
|
if candidate_ids.is_empty() {
|
||||||
|
@ -438,19 +410,16 @@ impl CompletionsMenu {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entry_candidate_id(entry: &CompletionEntry) -> Option<usize> {
|
pub fn is_empty(&self) -> bool {
|
||||||
match entry {
|
self.entries.borrow().is_empty()
|
||||||
CompletionEntry::Match(entry) => Some(entry.candidate_id),
|
|
||||||
CompletionEntry::InlineCompletionHint { .. } => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn visible(&self) -> bool {
|
pub fn visible(&self) -> bool {
|
||||||
!self.entries.borrow().is_empty()
|
!self.is_empty() && !self.previewing_inline_completion
|
||||||
}
|
}
|
||||||
|
|
||||||
fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
|
fn origin(&self) -> ContextMenuOrigin {
|
||||||
ContextMenuOrigin::EditorPoint(cursor_position)
|
ContextMenuOrigin::Cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(
|
fn render(
|
||||||
|
@ -468,23 +437,18 @@ impl CompletionsMenu {
|
||||||
.borrow()
|
.borrow()
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.max_by_key(|(_, mat)| match mat {
|
.max_by_key(|(_, mat)| {
|
||||||
CompletionEntry::Match(mat) => {
|
let completion = &completions[mat.candidate_id];
|
||||||
let completion = &completions[mat.candidate_id];
|
let documentation = &completion.documentation;
|
||||||
let documentation = &completion.documentation;
|
|
||||||
|
|
||||||
let mut len = completion.label.text.chars().count();
|
let mut len = completion.label.text.chars().count();
|
||||||
if let Some(CompletionDocumentation::SingleLine(text)) = documentation {
|
if let Some(CompletionDocumentation::SingleLine(text)) = documentation {
|
||||||
if show_completion_documentation {
|
if show_completion_documentation {
|
||||||
len += text.chars().count();
|
len += text.chars().count();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
len
|
len
|
||||||
}
|
|
||||||
CompletionEntry::InlineCompletionHint(hint) => {
|
|
||||||
"Zed AI / ".chars().count() + hint.label().chars().count()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.map(|(ix, _)| ix);
|
.map(|(ix, _)| ix);
|
||||||
drop(completions);
|
drop(completions);
|
||||||
|
@ -508,179 +472,83 @@ impl CompletionsMenu {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(ix, mat)| {
|
.map(|(ix, mat)| {
|
||||||
let item_ix = start_ix + ix;
|
let item_ix = start_ix + ix;
|
||||||
let buffer_font = theme::ThemeSettings::get_global(cx).buffer_font.clone();
|
let completion = &completions_guard[mat.candidate_id];
|
||||||
let base_label = h_flex()
|
let documentation = if show_completion_documentation {
|
||||||
.gap_1()
|
&completion.documentation
|
||||||
.child(div().font(buffer_font.clone()).child("Zed AI"))
|
} else {
|
||||||
.child(div().px_0p5().child("/").opacity(0.2));
|
&None
|
||||||
|
};
|
||||||
|
|
||||||
match mat {
|
let filter_start = completion.label.filter_range.start;
|
||||||
CompletionEntry::Match(mat) => {
|
let highlights = gpui::combine_highlights(
|
||||||
let candidate_id = mat.candidate_id;
|
mat.ranges().map(|range| {
|
||||||
let completion = &completions_guard[candidate_id];
|
(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
let documentation = if show_completion_documentation {
|
(range, highlight)
|
||||||
&completion.documentation
|
},
|
||||||
} else {
|
),
|
||||||
&None
|
);
|
||||||
};
|
|
||||||
|
|
||||||
let filter_start = completion.label.filter_range.start;
|
let completion_label = StyledText::new(completion.label.text.clone())
|
||||||
let highlights = gpui::combine_highlights(
|
.with_highlights(&style.text, highlights);
|
||||||
mat.ranges().map(|range| {
|
let documentation_label = if let Some(
|
||||||
(
|
CompletionDocumentation::SingleLine(text),
|
||||||
filter_start + range.start..filter_start + range.end,
|
) = documentation
|
||||||
FontWeight::BOLD.into(),
|
{
|
||||||
)
|
if text.trim().is_empty() {
|
||||||
}),
|
None
|
||||||
styled_runs_for_code_label(&completion.label, &style.syntax)
|
} else {
|
||||||
.map(|(range, mut highlight)| {
|
Some(
|
||||||
// Ignore font weight for syntax highlighting, as we'll use it
|
Label::new(text.clone())
|
||||||
// for fuzzy matches.
|
.ml_4()
|
||||||
highlight.font_weight = None;
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
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),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CompletionEntry::InlineCompletionHint(
|
} else {
|
||||||
hint @ InlineCompletionMenuHint::None,
|
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);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
|
|
||||||
CompletionEntry::InlineCompletionHint(
|
let color_swatch = completion
|
||||||
hint @ InlineCompletionMenuHint::Loaded { .. },
|
.color()
|
||||||
) => div().min_w(px(250.)).max_w(px(500.)).child(
|
.map(|color| div().size_4().bg(color).rounded_sm());
|
||||||
ListItem::new("inline-completion")
|
|
||||||
.inset(true)
|
div().min_w(px(280.)).max_w(px(540.)).child(
|
||||||
.toggle_state(item_ix == selected_item)
|
ListItem::new(mat.candidate_id)
|
||||||
.start_slot(Icon::new(IconName::ZedPredict))
|
.inset(true)
|
||||||
.child(
|
.toggle_state(item_ix == selected_item)
|
||||||
base_label.child(
|
.on_click(cx.listener(move |editor, _event, window, cx| {
|
||||||
StyledText::new(hint.label())
|
cx.stop_propagation();
|
||||||
.with_highlights(&style.text, None),
|
if let Some(task) = editor.confirm_completion(
|
||||||
),
|
&ConfirmCompletion {
|
||||||
)
|
item_ix: Some(item_ix),
|
||||||
.on_click(cx.listener(move |editor, _event, window, cx| {
|
},
|
||||||
cx.stop_propagation();
|
window,
|
||||||
editor.accept_inline_completion(
|
cx,
|
||||||
&AcceptInlineCompletion {},
|
) {
|
||||||
window,
|
task.detach_and_log_err(cx)
|
||||||
cx,
|
}
|
||||||
);
|
}))
|
||||||
})),
|
.start_slot::<Div>(color_swatch)
|
||||||
),
|
.child(h_flex().overflow_hidden().child(completion_label))
|
||||||
}
|
.end_slot::<Label>(documentation_label),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
},
|
},
|
||||||
|
@ -706,45 +574,25 @@ impl CompletionsMenu {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let multiline_docs = match &self.entries.borrow()[self.selected_item] {
|
let mat = &self.entries.borrow()[self.selected_item];
|
||||||
CompletionEntry::Match(mat) => {
|
let multiline_docs = match self.completions.borrow_mut()[mat.candidate_id]
|
||||||
match self.completions.borrow_mut()[mat.candidate_id]
|
.documentation
|
||||||
.documentation
|
.as_ref()?
|
||||||
.as_ref()?
|
{
|
||||||
{
|
CompletionDocumentation::MultiLinePlainText(text) => {
|
||||||
CompletionDocumentation::MultiLinePlainText(text) => {
|
div().child(SharedString::from(text.clone()))
|
||||||
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 }) => {
|
CompletionDocumentation::MultiLineMarkdown(parsed) if !parsed.text.is_empty() => div()
|
||||||
match text {
|
.child(render_parsed_markdown(
|
||||||
InlineCompletionText::Edit(highlighted_edits) => div()
|
"completions_markdown",
|
||||||
.mx_1()
|
parsed,
|
||||||
.rounded_md()
|
&style,
|
||||||
.bg(cx.theme().colors().editor_background)
|
workspace,
|
||||||
.child(
|
cx,
|
||||||
gpui::StyledText::new(highlighted_edits.text.clone())
|
)),
|
||||||
.with_highlights(&style.text, highlighted_edits.highlights.clone()),
|
CompletionDocumentation::MultiLineMarkdown(_) => return None,
|
||||||
),
|
CompletionDocumentation::SingleLine(_) => return None,
|
||||||
InlineCompletionText::Move(text) => div().child(text.clone()),
|
CompletionDocumentation::Undocumented => return None,
|
||||||
}
|
|
||||||
}
|
|
||||||
CompletionEntry::InlineCompletionHint(_) => return None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
|
@ -763,11 +611,6 @@ impl CompletionsMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
|
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 {
|
let mut matches = if let Some(query) = query {
|
||||||
fuzzy::match_strings(
|
fuzzy::match_strings(
|
||||||
&self.match_candidates,
|
&self.match_candidates,
|
||||||
|
@ -861,25 +704,15 @@ impl CompletionsMenu {
|
||||||
}
|
}
|
||||||
drop(completions);
|
drop(completions);
|
||||||
|
|
||||||
let mut entries = self.entries.borrow_mut();
|
*self.entries.borrow_mut() = matches;
|
||||||
let new_selection = if let Some(CompletionEntry::InlineCompletionHint(_)) = entries.first()
|
self.selected_item = 0;
|
||||||
{
|
// This keeps the display consistent when y_flipped.
|
||||||
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.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
|
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)]
|
#[derive(Clone)]
|
||||||
|
@ -1077,11 +910,11 @@ impl CodeActionsMenu {
|
||||||
!self.actions.is_empty()
|
!self.actions.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
|
fn origin(&self) -> ContextMenuOrigin {
|
||||||
if let Some(row) = self.deployed_from_indicator {
|
if let Some(row) = self.deployed_from_indicator {
|
||||||
ContextMenuOrigin::GutterIndicator(row)
|
ContextMenuOrigin::GutterIndicator(row)
|
||||||
} else {
|
} else {
|
||||||
ContextMenuOrigin::EditorPoint(cursor_position)
|
ContextMenuOrigin::Cursor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -508,7 +508,7 @@ impl DisplayMap {
|
||||||
|
|
||||||
pub(crate) fn splice_inlays(
|
pub(crate) fn splice_inlays(
|
||||||
&mut self,
|
&mut self,
|
||||||
to_remove: Vec<InlayId>,
|
to_remove: &[InlayId],
|
||||||
to_insert: Vec<Inlay>,
|
to_insert: Vec<Inlay>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -545,7 +545,7 @@ impl InlayMap {
|
||||||
|
|
||||||
pub fn splice(
|
pub fn splice(
|
||||||
&mut self,
|
&mut self,
|
||||||
to_remove: Vec<InlayId>,
|
to_remove: &[InlayId],
|
||||||
to_insert: Vec<Inlay>,
|
to_insert: Vec<Inlay>,
|
||||||
) -> (InlaySnapshot, Vec<InlayEdit>) {
|
) -> (InlaySnapshot, Vec<InlayEdit>) {
|
||||||
let snapshot = &mut self.snapshot;
|
let snapshot = &mut self.snapshot;
|
||||||
|
@ -653,7 +653,7 @@ impl InlayMap {
|
||||||
}
|
}
|
||||||
log::info!("removing inlays: {:?}", to_remove);
|
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)
|
(snapshot, edits)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1171,7 +1171,7 @@ mod tests {
|
||||||
let mut next_inlay_id = 0;
|
let mut next_inlay_id = 0;
|
||||||
|
|
||||||
let (inlay_snapshot, _) = inlay_map.splice(
|
let (inlay_snapshot, _) = inlay_map.splice(
|
||||||
Vec::new(),
|
&[],
|
||||||
vec![Inlay {
|
vec![Inlay {
|
||||||
id: InlayId::Hint(post_inc(&mut next_inlay_id)),
|
id: InlayId::Hint(post_inc(&mut next_inlay_id)),
|
||||||
position: buffer.read(cx).snapshot(cx).anchor_after(3),
|
position: buffer.read(cx).snapshot(cx).anchor_after(3),
|
||||||
|
@ -1247,7 +1247,7 @@ mod tests {
|
||||||
assert_eq!(inlay_snapshot.text(), "abxyDzefghi");
|
assert_eq!(inlay_snapshot.text(), "abxyDzefghi");
|
||||||
|
|
||||||
let (inlay_snapshot, _) = inlay_map.splice(
|
let (inlay_snapshot, _) = inlay_map.splice(
|
||||||
Vec::new(),
|
&[],
|
||||||
vec![
|
vec![
|
||||||
Inlay {
|
Inlay {
|
||||||
id: InlayId::Hint(post_inc(&mut next_inlay_id)),
|
id: InlayId::Hint(post_inc(&mut next_inlay_id)),
|
||||||
|
@ -1444,7 +1444,11 @@ mod tests {
|
||||||
|
|
||||||
// The inlays can be manually removed.
|
// The inlays can be manually removed.
|
||||||
let (inlay_snapshot, _) = inlay_map.splice(
|
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(),
|
Vec::new(),
|
||||||
);
|
);
|
||||||
assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
|
assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
|
||||||
|
@ -1458,7 +1462,7 @@ mod tests {
|
||||||
let mut next_inlay_id = 0;
|
let mut next_inlay_id = 0;
|
||||||
|
|
||||||
let (inlay_snapshot, _) = inlay_map.splice(
|
let (inlay_snapshot, _) = inlay_map.splice(
|
||||||
Vec::new(),
|
&[],
|
||||||
vec![
|
vec![
|
||||||
Inlay {
|
Inlay {
|
||||||
id: InlayId::Hint(post_inc(&mut next_inlay_id)),
|
id: InlayId::Hint(post_inc(&mut next_inlay_id)),
|
||||||
|
|
|
@ -73,17 +73,18 @@ use zed_predict_onboarding::ZedPredictModal;
|
||||||
|
|
||||||
use code_context_menus::{
|
use code_context_menus::{
|
||||||
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
|
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
|
||||||
CompletionEntry, CompletionsMenu, ContextMenuOrigin,
|
CompletionsMenu, ContextMenuOrigin,
|
||||||
};
|
};
|
||||||
use git::blame::GitBlame;
|
use git::blame::GitBlame;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, impl_actions, point, prelude::*, px, relative, size, Action, AnyElement, App,
|
div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation,
|
||||||
AsyncWindowContext, AvailableSpace, Bounds, ClipboardEntry, ClipboardItem, Context,
|
AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Bounds, ClipboardEntry,
|
||||||
DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent,
|
ClipboardItem, Context, DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter,
|
||||||
Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext,
|
FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla,
|
||||||
MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size,
|
InteractiveText, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement,
|
||||||
Styled, StyledText, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection,
|
Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, Task, TextStyle,
|
||||||
UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window,
|
TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity,
|
||||||
|
WeakFocusHandle, Window,
|
||||||
};
|
};
|
||||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||||
use hover_popover::{hide_hover, HoverState};
|
use hover_popover::{hide_hover, HoverState};
|
||||||
|
@ -107,7 +108,7 @@ pub use proposed_changes_editor::{
|
||||||
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
|
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
|
||||||
};
|
};
|
||||||
use similar::{ChangeTag, TextDiff};
|
use similar::{ChangeTag, TextDiff};
|
||||||
use std::iter::Peekable;
|
use std::iter::{self, Peekable};
|
||||||
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
||||||
|
|
||||||
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
|
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
|
||||||
|
@ -163,7 +164,7 @@ use ui::{
|
||||||
h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize,
|
h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize,
|
||||||
Tooltip,
|
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::item::{ItemHandle, PreviewTabsSettings};
|
||||||
use workspace::notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt};
|
use workspace::notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
|
@ -465,32 +466,6 @@ pub fn make_suggestion_styles(cx: &mut App) -> InlineCompletionStyles {
|
||||||
|
|
||||||
type CompletionId = usize;
|
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 {
|
pub(crate) enum EditDisplayMode {
|
||||||
TabAccept,
|
TabAccept,
|
||||||
DiffPopover,
|
DiffPopover,
|
||||||
|
@ -504,7 +479,11 @@ enum InlineCompletion {
|
||||||
display_mode: EditDisplayMode,
|
display_mode: EditDisplayMode,
|
||||||
snapshot: BufferSnapshot,
|
snapshot: BufferSnapshot,
|
||||||
},
|
},
|
||||||
Move(Anchor),
|
Move {
|
||||||
|
target: Anchor,
|
||||||
|
range_around_target: Range<text::Anchor>,
|
||||||
|
snapshot: BufferSnapshot,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InlineCompletionState {
|
struct InlineCompletionState {
|
||||||
|
@ -513,6 +492,15 @@ struct InlineCompletionState {
|
||||||
invalidation_range: Range<Anchor>,
|
invalidation_range: Range<Anchor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl InlineCompletionState {
|
||||||
|
pub fn is_move(&self) -> bool {
|
||||||
|
match &self.completion {
|
||||||
|
InlineCompletion::Move { .. } => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum InlineCompletionHighlight {}
|
enum InlineCompletionHighlight {}
|
||||||
|
|
||||||
pub enum MenuInlineCompletionsPolicy {
|
pub enum MenuInlineCompletionsPolicy {
|
||||||
|
@ -687,6 +675,8 @@ pub struct Editor {
|
||||||
inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
|
inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
|
||||||
code_action_providers: Vec<Rc<dyn CodeActionProvider>>,
|
code_action_providers: Vec<Rc<dyn CodeActionProvider>>,
|
||||||
active_inline_completion: Option<InlineCompletionState>,
|
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
|
// enable_inline_completions is a switch that Vim can use to disable
|
||||||
// inline completions based on its mode.
|
// inline completions based on its mode.
|
||||||
enable_inline_completions: bool,
|
enable_inline_completions: bool,
|
||||||
|
@ -1381,6 +1371,7 @@ impl Editor {
|
||||||
hovered_link_state: Default::default(),
|
hovered_link_state: Default::default(),
|
||||||
inline_completion_provider: None,
|
inline_completion_provider: None,
|
||||||
active_inline_completion: None,
|
active_inline_completion: None,
|
||||||
|
stale_inline_completion_in_menu: None,
|
||||||
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
|
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
|
||||||
|
|
||||||
gutter_hovered: false,
|
gutter_hovered: false,
|
||||||
|
@ -1496,7 +1487,7 @@ impl Editor {
|
||||||
match self.context_menu.borrow().as_ref() {
|
match self.context_menu.borrow().as_ref() {
|
||||||
Some(CodeContextMenu::Completions(_)) => {
|
Some(CodeContextMenu::Completions(_)) => {
|
||||||
key_context.add("menu");
|
key_context.add("menu");
|
||||||
key_context.add("showing_completions")
|
key_context.add("showing_completions");
|
||||||
}
|
}
|
||||||
Some(CodeContextMenu::CodeActions(_)) => {
|
Some(CodeContextMenu::CodeActions(_)) => {
|
||||||
key_context.add("menu");
|
key_context.add("menu");
|
||||||
|
@ -2611,9 +2602,6 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.hide_context_menu(window, cx).is_some() {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3601,10 +3589,11 @@ impl Editor {
|
||||||
} else {
|
} else {
|
||||||
self.inlay_hint_cache.clear();
|
self.inlay_hint_cache.clear();
|
||||||
self.splice_inlays(
|
self.splice_inlays(
|
||||||
self.visible_inlay_hints(cx)
|
&self
|
||||||
|
.visible_inlay_hints(cx)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|inlay| inlay.id)
|
.map(|inlay| inlay.id)
|
||||||
.collect(),
|
.collect::<Vec<InlayId>>(),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -3622,7 +3611,7 @@ impl Editor {
|
||||||
to_remove,
|
to_remove,
|
||||||
to_insert,
|
to_insert,
|
||||||
})) => {
|
})) => {
|
||||||
self.splice_inlays(to_remove, to_insert, cx);
|
self.splice_inlays(&to_remove, to_insert, cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ControlFlow::Break(None) => return,
|
ControlFlow::Break(None) => return,
|
||||||
|
@ -3635,7 +3624,7 @@ impl Editor {
|
||||||
to_insert,
|
to_insert,
|
||||||
}) = self.inlay_hint_cache.remove_excerpts(excerpts_removed)
|
}) = 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;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -3658,7 +3647,7 @@ impl Editor {
|
||||||
ignore_debounce,
|
ignore_debounce,
|
||||||
cx,
|
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(
|
pub fn splice_inlays(
|
||||||
&self,
|
&self,
|
||||||
to_remove: Vec<InlayId>,
|
to_remove: &[InlayId],
|
||||||
to_insert: Vec<Inlay>,
|
to_insert: Vec<Inlay>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
@ -3905,17 +3894,15 @@ impl Editor {
|
||||||
let mut menu = menu.unwrap();
|
let mut menu = menu.unwrap();
|
||||||
menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx);
|
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 editor.show_inline_completions_in_menu(cx) {
|
||||||
if let Some(hint) = editor.inline_completion_menu_hint(window, cx) {
|
editor.update_visible_inline_completion(window, cx);
|
||||||
menu.show_inline_completion_hint(hint);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
editor.discard_inline_completion(false, cx);
|
editor.discard_inline_completion(false, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
*editor.context_menu.borrow_mut() =
|
|
||||||
Some(CodeContextMenu::Completions(menu));
|
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
} else if editor.completion_tasks.len() <= 1 {
|
} else if editor.completion_tasks.len() <= 1 {
|
||||||
// If there are no more completion tasks and the last menu was
|
// 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>>> {
|
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
|
||||||
use language::ToOffset as _;
|
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 =
|
let completions_menu =
|
||||||
if let CodeContextMenu::Completions(menu) = self.hide_context_menu(window, cx)? {
|
if let CodeContextMenu::Completions(menu) = self.hide_context_menu(window, cx)? {
|
||||||
menu
|
menu
|
||||||
|
@ -4019,19 +3978,9 @@ impl Editor {
|
||||||
|
|
||||||
let entries = completions_menu.entries.borrow();
|
let entries = completions_menu.entries.borrow();
|
||||||
let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?;
|
let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?;
|
||||||
let mat = match mat {
|
if self.show_inline_completions_in_menu(cx) {
|
||||||
CompletionEntry::InlineCompletionHint(_) => {
|
self.discard_inline_completion(true, cx);
|
||||||
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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let candidate_id = mat.candidate_id;
|
let candidate_id = mat.candidate_id;
|
||||||
drop(entries);
|
drop(entries);
|
||||||
|
|
||||||
|
@ -4863,10 +4812,10 @@ impl Editor {
|
||||||
self.report_inline_completion_event(true, cx);
|
self.report_inline_completion_event(true, cx);
|
||||||
|
|
||||||
match &active_inline_completion.completion {
|
match &active_inline_completion.completion {
|
||||||
InlineCompletion::Move(position) => {
|
InlineCompletion::Move { target, .. } => {
|
||||||
let position = *position;
|
let target = *target;
|
||||||
self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
|
self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
|
||||||
selections.select_anchor_ranges([position..position]);
|
selections.select_anchor_ranges([target..target]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
InlineCompletion::Edit { edits, .. } => {
|
InlineCompletion::Edit { edits, .. } => {
|
||||||
|
@ -4911,10 +4860,10 @@ impl Editor {
|
||||||
self.report_inline_completion_event(true, cx);
|
self.report_inline_completion_event(true, cx);
|
||||||
|
|
||||||
match &active_inline_completion.completion {
|
match &active_inline_completion.completion {
|
||||||
InlineCompletion::Move(position) => {
|
InlineCompletion::Move { target, .. } => {
|
||||||
let position = *position;
|
let target = *target;
|
||||||
self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
|
self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
|
||||||
selections.select_anchor_ranges([position..position]);
|
selections.select_anchor_ranges([target..target]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
InlineCompletion::Edit { edits, .. } => {
|
InlineCompletion::Edit { edits, .. } => {
|
||||||
|
@ -4973,7 +4922,7 @@ impl Editor {
|
||||||
provider.discard(cx);
|
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) {
|
fn report_inline_completion_event(&self, accepted: bool, cx: &App) {
|
||||||
|
@ -5010,19 +4959,58 @@ impl Editor {
|
||||||
self.active_inline_completion.is_some()
|
self.active_inline_completion.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn take_active_inline_completion(
|
fn take_active_inline_completion(&mut self, cx: &mut Context<Self>) -> bool {
|
||||||
&mut self,
|
let Some(active_inline_completion) = self.active_inline_completion.take() else {
|
||||||
cx: &mut Context<Self>,
|
return false;
|
||||||
) -> Option<InlineCompletion> {
|
};
|
||||||
let active_inline_completion = self.active_inline_completion.take()?;
|
|
||||||
self.splice_inlays(active_inline_completion.inlay_ids, Default::default(), cx);
|
self.splice_inlays(&active_inline_completion.inlay_ids, Default::default(), cx);
|
||||||
self.clear_highlights::<InlineCompletionHighlight>(cx);
|
self.clear_highlights::<InlineCompletionHighlight>(cx);
|
||||||
Some(active_inline_completion.completion)
|
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>,
|
||||||
|
) {
|
||||||
|
// 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(
|
fn update_visible_inline_completion(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let selection = self.selections.newest_anchor();
|
let selection = self.selections.newest_anchor();
|
||||||
|
@ -5031,7 +5019,8 @@ impl Editor {
|
||||||
let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
|
let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
|
||||||
let excerpt_id = cursor.excerpt_id;
|
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.context_menu.borrow().is_some()
|
||||||
|| (!self.completion_tasks.is_empty() && !self.has_active_inline_completion()));
|
|| (!self.completion_tasks.is_empty() && !self.has_active_inline_completion()));
|
||||||
if completions_menu_has_precedence
|
if completions_menu_has_precedence
|
||||||
|
@ -5080,50 +5069,73 @@ impl Editor {
|
||||||
|
|
||||||
let cursor_row = cursor.to_point(&multibuffer).row;
|
let cursor_row = cursor.to_point(&multibuffer).row;
|
||||||
|
|
||||||
|
let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?;
|
||||||
|
|
||||||
let mut inlay_ids = Vec::new();
|
let mut inlay_ids = Vec::new();
|
||||||
let invalidation_row_range;
|
let invalidation_row_range;
|
||||||
let completion = if cursor_row < edit_start_row {
|
let move_invalidation_row_range = if cursor_row < edit_start_row {
|
||||||
invalidation_row_range = cursor_row..edit_end_row;
|
Some(cursor_row..edit_end_row)
|
||||||
InlineCompletion::Move(first_edit_start)
|
|
||||||
} else if cursor_row > edit_end_row {
|
} else if cursor_row > edit_end_row {
|
||||||
invalidation_row_range = edit_start_row..cursor_row;
|
Some(edit_start_row..cursor_row)
|
||||||
InlineCompletion::Move(first_edit_start)
|
|
||||||
} else {
|
} else {
|
||||||
if edits
|
None
|
||||||
.iter()
|
};
|
||||||
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
|
let completion = if let Some(move_invalidation_row_range) = move_invalidation_row_range {
|
||||||
{
|
invalidation_row_range = move_invalidation_row_range;
|
||||||
let mut inlays = Vec::new();
|
let target = first_edit_start;
|
||||||
for (range, new_text) in &edits {
|
let target_point = text::ToPoint::to_point(&target.text_anchor, &snapshot);
|
||||||
let inlay = Inlay::inline_completion(
|
// TODO: Base this off of TreeSitter or word boundaries?
|
||||||
post_inc(&mut self.next_inlay_id),
|
let target_excerpt_begin = snapshot.anchor_before(snapshot.clip_point(
|
||||||
range.start,
|
Point::new(target_point.row, target_point.column.saturating_sub(10)),
|
||||||
new_text.as_str(),
|
Bias::Left,
|
||||||
);
|
));
|
||||||
inlay_ids.push(inlay.id);
|
let target_excerpt_end = snapshot.anchor_after(snapshot.clip_point(
|
||||||
inlays.push(inlay);
|
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(vec![], inlays, cx);
|
self.splice_inlays(&[], inlays, cx);
|
||||||
} else {
|
} else {
|
||||||
let background_color = cx.theme().status().deleted_background;
|
let background_color = cx.theme().status().deleted_background;
|
||||||
self.highlight_text::<InlineCompletionHighlight>(
|
self.highlight_text::<InlineCompletionHighlight>(
|
||||||
edits.iter().map(|(range, _)| range.clone()).collect(),
|
edits.iter().map(|(range, _)| range.clone()).collect(),
|
||||||
HighlightStyle {
|
HighlightStyle {
|
||||||
background_color: Some(background_color),
|
background_color: Some(background_color),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidation_row_range = edit_start_row..edit_end_row;
|
invalidation_row_range = edit_start_row..edit_end_row;
|
||||||
|
|
||||||
let display_mode = if all_edits_insertions_or_deletions(&edits, &multibuffer) {
|
let display_mode = if all_edits_insertions_or_deletions(&edits, &multibuffer) {
|
||||||
if provider.show_tab_accept_marker()
|
if provider.show_tab_accept_marker() {
|
||||||
&& first_edit_start_point.row == last_edit_end_point.row
|
|
||||||
&& !edits.iter().any(|(_, edit)| edit.contains('\n'))
|
|
||||||
{
|
|
||||||
EditDisplayMode::TabAccept
|
EditDisplayMode::TabAccept
|
||||||
} else {
|
} else {
|
||||||
EditDisplayMode::Inline
|
EditDisplayMode::Inline
|
||||||
|
@ -5132,8 +5144,6 @@ impl Editor {
|
||||||
EditDisplayMode::DiffPopover
|
EditDisplayMode::DiffPopover
|
||||||
};
|
};
|
||||||
|
|
||||||
let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?;
|
|
||||||
|
|
||||||
InlineCompletion::Edit {
|
InlineCompletion::Edit {
|
||||||
edits,
|
edits,
|
||||||
edit_preview: inline_completion.edit_preview,
|
edit_preview: inline_completion.edit_preview,
|
||||||
|
@ -5149,69 +5159,18 @@ impl Editor {
|
||||||
multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)),
|
multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
self.stale_inline_completion_in_menu = None;
|
||||||
self.active_inline_completion = Some(InlineCompletionState {
|
self.active_inline_completion = Some(InlineCompletionState {
|
||||||
inlay_ids,
|
inlay_ids,
|
||||||
completion,
|
completion,
|
||||||
invalidation_range,
|
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();
|
cx.notify();
|
||||||
|
|
||||||
Some(())
|
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>> {
|
pub fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
|
||||||
Some(self.inline_completion_provider.as_ref()?.provider.clone())
|
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 {
|
pub fn context_menu_visible(&self) -> bool {
|
||||||
self.context_menu
|
self.context_menu
|
||||||
.borrow()
|
.borrow()
|
||||||
|
@ -5447,26 +5405,300 @@ impl Editor {
|
||||||
.map_or(false, |menu| menu.visible())
|
.map_or(false, |menu| menu.visible())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "test-support")]
|
fn context_menu_origin(&self) -> Option<ContextMenuOrigin> {
|
||||||
pub fn context_menu_contains_inline_completion(&self) -> bool {
|
|
||||||
self.context_menu
|
self.context_menu
|
||||||
.borrow()
|
.borrow()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(false, |menu| match menu {
|
.map(|menu| menu.origin())
|
||||||
CodeContextMenu::Completions(menu) => {
|
|
||||||
menu.entries.borrow().first().map_or(false, |entry| {
|
|
||||||
matches!(entry, CompletionEntry::InlineCompletionHint(_))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
CodeContextMenu::CodeActions(_) => false,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn context_menu_origin(&self, cursor_position: DisplayPoint) -> Option<ContextMenuOrigin> {
|
fn edit_prediction_cursor_popover_height(&self) -> Pixels {
|
||||||
self.context_menu
|
px(32.)
|
||||||
.borrow()
|
}
|
||||||
.as_ref()
|
|
||||||
.map(|menu| menu.origin(cursor_position))
|
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))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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 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(
|
fn render_context_menu(
|
||||||
|
@ -5477,13 +5709,12 @@ impl Editor {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
self.context_menu.borrow().as_ref().and_then(|menu| {
|
let menu = self.context_menu.borrow();
|
||||||
if menu.visible() {
|
let menu = menu.as_ref()?;
|
||||||
Some(menu.render(style, max_height_in_lines, y_flipped, window, cx))
|
if !menu.visible() {
|
||||||
} else {
|
return None;
|
||||||
None
|
};
|
||||||
}
|
Some(menu.render(style, max_height_in_lines, y_flipped, window, cx))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_context_menu_aside(
|
fn render_context_menu_aside(
|
||||||
|
@ -5514,7 +5745,8 @@ impl Editor {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
self.completion_tasks.clear();
|
self.completion_tasks.clear();
|
||||||
let context_menu = self.context_menu.borrow_mut().take();
|
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);
|
self.update_visible_inline_completion(window, cx);
|
||||||
}
|
}
|
||||||
context_menu
|
context_menu
|
||||||
|
@ -15859,7 +16091,7 @@ fn inline_completion_edit_text(
|
||||||
edit_preview: &EditPreview,
|
edit_preview: &EditPreview,
|
||||||
include_deletions: bool,
|
include_deletions: bool,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Option<HighlightedText> {
|
) -> HighlightedText {
|
||||||
let edits = edits
|
let edits = edits
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(anchor, text)| {
|
.map(|(anchor, text)| {
|
||||||
|
@ -15870,7 +16102,7 @@ fn inline_completion_edit_text(
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.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(
|
pub fn highlight_diagnostic_message(
|
||||||
|
|
|
@ -11707,10 +11707,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
|
||||||
.entries
|
.entries
|
||||||
.borrow()
|
.borrow()
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|c| match c {
|
.map(|mat| mat.string.clone())
|
||||||
CompletionEntry::Match(mat) => Some(mat.string.clone()),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.collect::<Vec<String>>(),
|
.collect::<Vec<String>>(),
|
||||||
items_out
|
items_out
|
||||||
.iter()
|
.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> {
|
fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
|
||||||
let entries = menu.entries.borrow();
|
let entries = menu.entries.borrow();
|
||||||
entries
|
entries.iter().map(|mat| mat.string.clone()).collect()
|
||||||
.iter()
|
|
||||||
.flat_map(|e| match e {
|
|
||||||
CompletionEntry::Match(mat) => Some(mat.string.clone()),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -15469,8 +15460,7 @@ async fn assert_highlighted_edits(
|
||||||
&edit_preview,
|
&edit_preview,
|
||||||
include_deletions,
|
include_deletions,
|
||||||
cx,
|
cx,
|
||||||
)
|
);
|
||||||
.expect("Missing highlighted edits");
|
|
||||||
assertion_fn(highlighted_edits, cx)
|
assertion_fn(highlighted_edits, cx)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,11 +32,12 @@ use gpui::{
|
||||||
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
|
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
|
||||||
relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds,
|
relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds,
|
||||||
ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase,
|
ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase,
|
||||||
Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
|
Edges, Element, ElementInputHandler, Entity, FocusHandle, Focusable as _, FontId,
|
||||||
Hsla, InteractiveElement, IntoElement, Length, ModifiersChangedEvent, MouseButton,
|
GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Keystroke, Length,
|
||||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
|
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
|
||||||
Subscription, TextRun, TextStyleRefinement, WeakEntity, Window,
|
StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement,
|
||||||
|
WeakEntity, Window,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
|
@ -525,6 +526,8 @@ impl EditorElement {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) {
|
) {
|
||||||
|
editor.update_inline_completion_preview(&event.modifiers, window, cx);
|
||||||
|
|
||||||
let mouse_position = window.mouse_position();
|
let mouse_position = window.mouse_position();
|
||||||
if !text_hitbox.is_hovered(window) {
|
if !text_hitbox.is_hovered(window) {
|
||||||
return;
|
return;
|
||||||
|
@ -1010,12 +1013,7 @@ impl EditorElement {
|
||||||
layouts.push(layout);
|
layouts.push(layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
let player = if editor.read_only(cx) {
|
let player = editor.current_user_player_color(cx);
|
||||||
cx.theme().players().read_only()
|
|
||||||
} else {
|
|
||||||
self.style.local_player
|
|
||||||
};
|
|
||||||
|
|
||||||
selections.push((player, layouts));
|
selections.push((player, layouts));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1077,11 +1075,6 @@ impl EditorElement {
|
||||||
|
|
||||||
selections.extend(remote_selections.into_values());
|
selections.extend(remote_selections.into_values());
|
||||||
} else if !editor.is_focused(window) && editor.show_cursor_when_unfocused {
|
} 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
|
let layouts = snapshot
|
||||||
.buffer_snapshot
|
.buffer_snapshot
|
||||||
.selections_in_range(&(start_anchor..end_anchor), true)
|
.selections_in_range(&(start_anchor..end_anchor), true)
|
||||||
|
@ -1097,6 +1090,7 @@ impl EditorElement {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
let player = editor.current_user_player_color(cx);
|
||||||
selections.push((player, layouts));
|
selections.push((player, layouts));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -3157,7 +3151,7 @@ impl EditorElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn layout_context_menu(
|
fn layout_cursor_popovers(
|
||||||
&self,
|
&self,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
text_hitbox: &Hitbox,
|
text_hitbox: &Hitbox,
|
||||||
|
@ -3165,44 +3159,53 @@ impl EditorElement {
|
||||||
start_row: DisplayRow,
|
start_row: DisplayRow,
|
||||||
scroll_pixel_position: gpui::Point<Pixels>,
|
scroll_pixel_position: gpui::Point<Pixels>,
|
||||||
line_layouts: &[LineWithInvisibles],
|
line_layouts: &[LineWithInvisibles],
|
||||||
newest_selection_head: DisplayPoint,
|
cursor: DisplayPoint,
|
||||||
gutter_overshoot: Pixels,
|
cursor_point: Point,
|
||||||
|
style: &EditorStyle,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
let Some(context_menu_origin) = self
|
let mut min_menu_height = Pixels::ZERO;
|
||||||
.editor
|
let mut max_menu_height = Pixels::ZERO;
|
||||||
.read(cx)
|
let mut height_above_menu = Pixels::ZERO;
|
||||||
.context_menu_origin(newest_selection_head)
|
let height_below_menu = Pixels::ZERO;
|
||||||
else {
|
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;
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
let cursor_row_layout = &line_layouts[cursor.row().minus(start_row) as usize];
|
||||||
let target_position = content_origin
|
let target_position = content_origin
|
||||||
+ match context_menu_origin {
|
+ gpui::Point {
|
||||||
crate::ContextMenuOrigin::EditorPoint(display_point) => {
|
x: cmp::max(
|
||||||
let cursor_row_layout =
|
px(0.),
|
||||||
&line_layouts[display_point.row().minus(start_row) as usize];
|
cursor_row_layout.x_for_index(cursor.column() as usize)
|
||||||
gpui::Point {
|
- scroll_pixel_position.x,
|
||||||
x: cmp::max(
|
),
|
||||||
px(0.),
|
y: cmp::max(
|
||||||
cursor_row_layout.x_for_index(display_point.column() as usize)
|
px(0.),
|
||||||
- scroll_pixel_position.x,
|
cursor.row().next_row().as_f32() * line_height - scroll_pixel_position.y,
|
||||||
),
|
),
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let viewport_bounds =
|
let viewport_bounds =
|
||||||
|
@ -3211,17 +3214,241 @@ impl EditorElement {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
// If the context menu's max height won't fit below, then flip it above the line and display
|
let min_height = height_above_menu + min_menu_height + height_below_menu;
|
||||||
// it in reverse order. If the available space above is less than below.
|
let max_height = height_above_menu + max_menu_height + height_below_menu;
|
||||||
let unconstrained_max_height = line_height * 12. + POPOVER_Y_PADDING;
|
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(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
|
||||||
|
+ 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 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)]
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 bottom_y_when_flipped = target_position.y - line_height;
|
||||||
let available_above = bottom_y_when_flipped - text_hitbox.top();
|
let available_above = bottom_y_when_flipped - text_hitbox.top();
|
||||||
let available_below = text_hitbox.bottom() - target_position.y;
|
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 y_flipped = y_overflows_below && available_above > available_below;
|
||||||
let mut height = cmp::min(
|
let mut height = cmp::min(
|
||||||
unconstrained_max_height,
|
max_height,
|
||||||
if y_flipped {
|
if y_flipped {
|
||||||
available_above
|
available_above
|
||||||
} else {
|
} 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 {
|
if height < min_height {
|
||||||
let available_above = bottom_y_when_flipped;
|
let available_above = bottom_y_when_flipped;
|
||||||
let available_below = viewport_bounds.bottom() - target_position.y;
|
let available_below = viewport_bounds.bottom() - target_position.y;
|
||||||
if available_below > 3. * line_height {
|
if available_below > min_height {
|
||||||
y_flipped = false;
|
y_flipped = false;
|
||||||
height = min_height;
|
height = min_height;
|
||||||
} else if available_above > 3. * line_height {
|
} else if available_above > min_height {
|
||||||
y_flipped = true;
|
y_flipped = true;
|
||||||
height = min_height;
|
height = min_height;
|
||||||
} else if available_above > available_below {
|
} 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.
|
// TODO: Use viewport_bounds.width as a max width so that it doesn't get clipped on the left
|
||||||
let Some(mut menu_element) = self.editor.update(cx, |editor, cx| {
|
// for very narrow windows.
|
||||||
editor.render_context_menu(&self.style, max_height_in_lines, y_flipped, window, cx)
|
let popovers = make_sized_popovers(height, max_width_for_stable_x, y_flipped, window, cx);
|
||||||
}) else {
|
if popovers.is_empty() {
|
||||||
return;
|
return None;
|
||||||
};
|
}
|
||||||
|
|
||||||
let menu_size = menu_element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
let max_width = popovers
|
||||||
let menu_position = gpui::Point {
|
.iter()
|
||||||
|
.map(|(_, _, size)| size.width)
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut current_position = gpui::Point {
|
||||||
// Snap the right edge of the list to the right edge of the window if its horizontal bounds
|
// Snap the right edge of the list to the right edge of the window if its horizontal bounds
|
||||||
// overflow. Include space for the scrollbar.
|
// overflow. Include space for the scrollbar.
|
||||||
x: target_position
|
x: target_position
|
||||||
.x
|
.x
|
||||||
.min((viewport_bounds.right() - menu_size.width).max(Pixels::ZERO)),
|
.min((viewport_bounds.right() - max_width).max(Pixels::ZERO)),
|
||||||
y: if y_flipped {
|
y: if y_flipped {
|
||||||
bottom_y_when_flipped - menu_size.height
|
bottom_y_when_flipped
|
||||||
} else {
|
} else {
|
||||||
target_position.y
|
target_position.y
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
window.defer_draw(menu_element, menu_position, 1);
|
|
||||||
|
|
||||||
// Layout documentation aside
|
let laid_out_popovers = popovers
|
||||||
let menu_bounds = Bounds::new(menu_position, menu_size);
|
.into_iter()
|
||||||
let max_menu_size = size(menu_size.width, unconstrained_max_height);
|
.map(|(popover_type, element, size)| {
|
||||||
let max_menu_bounds = if y_flipped {
|
if y_flipped {
|
||||||
Bounds::new(
|
current_position.y -= size.height;
|
||||||
point(
|
}
|
||||||
menu_position.x,
|
let position = current_position;
|
||||||
bottom_y_when_flipped - max_menu_size.height,
|
window.defer_draw(element, current_position, 1);
|
||||||
),
|
if !y_flipped {
|
||||||
max_menu_size,
|
current_position.y += size.height + MENU_GAP;
|
||||||
)
|
} else {
|
||||||
} else {
|
current_position.y -= MENU_GAP;
|
||||||
Bounds::new(target_position, max_menu_size)
|
}
|
||||||
};
|
(popover_type, Bounds::new(position, size))
|
||||||
self.layout_context_menu_aside(
|
})
|
||||||
text_hitbox,
|
.collect::<Vec<_>>();
|
||||||
y_flipped,
|
|
||||||
menu_position,
|
Some((laid_out_popovers, y_flipped))
|
||||||
menu_bounds,
|
|
||||||
max_menu_bounds,
|
|
||||||
unconstrained_max_height,
|
|
||||||
line_height,
|
|
||||||
viewport_bounds,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn layout_context_menu_aside(
|
fn layout_context_menu_aside(
|
||||||
&self,
|
&self,
|
||||||
text_hitbox: &Hitbox,
|
|
||||||
y_flipped: bool,
|
y_flipped: bool,
|
||||||
menu_position: gpui::Point<Pixels>,
|
|
||||||
menu_bounds: Bounds<Pixels>,
|
menu_bounds: Bounds<Pixels>,
|
||||||
max_menu_bounds: Bounds<Pixels>,
|
target_bounds: Bounds<Pixels>,
|
||||||
|
max_target_bounds: Bounds<Pixels>,
|
||||||
max_height: Pixels,
|
max_height: Pixels,
|
||||||
line_height: Pixels,
|
text_hitbox: &Hitbox,
|
||||||
viewport_bounds: Bounds<Pixels>,
|
viewport_bounds: Bounds<Pixels>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
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 available_within_viewport = target_bounds.space_within(&viewport_bounds);
|
||||||
let positioned_aside = if available_within_viewport.right >= MENU_ASIDE_MIN_WIDTH {
|
let positioned_aside = if available_within_viewport.right >= MENU_ASIDE_MIN_WIDTH {
|
||||||
let max_width = cmp::min(
|
let max_width = cmp::min(
|
||||||
|
@ -3336,7 +3548,7 @@ impl EditorElement {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
aside.layout_as_root(AvailableSpace::min_size(), window, cx);
|
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))
|
Some((aside, right_position))
|
||||||
} else {
|
} else {
|
||||||
let max_size = size(
|
let max_size = size(
|
||||||
|
@ -3359,8 +3571,11 @@ impl EditorElement {
|
||||||
};
|
};
|
||||||
let actual_size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
|
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 top_position = point(
|
||||||
let bottom_position = point(menu_position.x, target_bounds.bottom());
|
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>| {
|
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
|
// 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(
|
fn render_context_menu_aside(
|
||||||
&self,
|
&self,
|
||||||
max_size: Size<Pixels>,
|
max_size: Size<Pixels>,
|
||||||
|
@ -3434,12 +3663,14 @@ impl EditorElement {
|
||||||
let active_inline_completion = self.editor.read(cx).active_inline_completion.as_ref()?;
|
let active_inline_completion = self.editor.read(cx).active_inline_completion.as_ref()?;
|
||||||
|
|
||||||
match &active_inline_completion.completion {
|
match &active_inline_completion.completion {
|
||||||
InlineCompletion::Move(target_position) => {
|
InlineCompletion::Move { target, .. } => {
|
||||||
let target_display_point = target_position.to_display_point(editor_snapshot);
|
let target_display_point = target.to_display_point(editor_snapshot);
|
||||||
if target_display_point.row().as_f32() < scroll_top {
|
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",
|
"Jump to Edit",
|
||||||
Some(IconName::ArrowUp),
|
Some(IconName::ArrowUp),
|
||||||
|
self.editor.focus_handle(cx),
|
||||||
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
let size = element.layout_as_root(AvailableSpace::min_size(), 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);
|
element.prepaint_at(text_bounds.origin + offset, window, cx);
|
||||||
Some(element)
|
Some(element)
|
||||||
} else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
|
} 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",
|
"Jump to Edit",
|
||||||
Some(IconName::ArrowDown),
|
Some(IconName::ArrowDown),
|
||||||
|
self.editor.focus_handle(cx),
|
||||||
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
let size = element.layout_as_root(AvailableSpace::min_size(), 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);
|
element.prepaint_at(text_bounds.origin + offset, window, cx);
|
||||||
Some(element)
|
Some(element)
|
||||||
} else {
|
} 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(
|
let target_line_end = DisplayPoint::new(
|
||||||
target_display_point.row(),
|
target_display_point.row(),
|
||||||
|
@ -3520,7 +3759,13 @@ impl EditorElement {
|
||||||
editor.display_to_pixel_point(target_line_end, editor_snapshot, window)
|
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(
|
element.prepaint_as_root(
|
||||||
text_bounds.origin + origin + point(PADDING_X, px(0.)),
|
text_bounds.origin + origin + point(PADDING_X, px(0.)),
|
||||||
|
@ -3535,9 +3780,13 @@ impl EditorElement {
|
||||||
EditDisplayMode::DiffPopover => {}
|
EditDisplayMode::DiffPopover => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let highlighted_edits = edit_preview.as_ref().and_then(|edit_preview| {
|
let highlighted_edits = crate::inline_completion_edit_text(
|
||||||
crate::inline_completion_edit_text(&snapshot, edits, edit_preview, false, cx)
|
&snapshot,
|
||||||
})?;
|
edits,
|
||||||
|
edit_preview.as_ref()?,
|
||||||
|
false,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
let line_count = highlighted_edits.text.lines().count();
|
let line_count = highlighted_edits.text.lines().count();
|
||||||
|
|
||||||
|
@ -3558,8 +3807,7 @@ impl EditorElement {
|
||||||
.width
|
.width
|
||||||
};
|
};
|
||||||
|
|
||||||
let styled_text = gpui::StyledText::new(highlighted_edits.text.clone())
|
let styled_text = highlighted_edits.to_styled_text(&style.text);
|
||||||
.with_highlights(&style.text, highlighted_edits.highlights);
|
|
||||||
|
|
||||||
let mut element = div()
|
let mut element = div()
|
||||||
.bg(cx.theme().colors().editor_background)
|
.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>,
|
label: impl Into<SharedString>,
|
||||||
icon: Option<IconName>,
|
icon: Option<IconName>,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
window: &Window,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> AnyElement {
|
) -> 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()
|
.px_0p5()
|
||||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||||
.text_size(TextSize::XSmall.rems(cx))
|
.text_size(TextSize::XSmall.rems(cx))
|
||||||
.text_color(cx.theme().colors().text)
|
.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.) };
|
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))
|
.border_color(cx.theme().colors().text_accent.opacity(0.8))
|
||||||
.rounded_md()
|
.rounded_md()
|
||||||
.shadow_sm()
|
.shadow_sm()
|
||||||
.child(tab_kbd)
|
.child(accept_key)
|
||||||
.child(Label::new(label).size(LabelSize::Small))
|
.child(Label::new(label).size(LabelSize::Small))
|
||||||
.when_some(icon, |element, icon| {
|
.when_some(icon, |element, icon| {
|
||||||
element.child(
|
element.child(
|
||||||
|
@ -7059,8 +7323,11 @@ impl Element for EditorElement {
|
||||||
);
|
);
|
||||||
let mut code_actions_indicator = None;
|
let mut code_actions_indicator = None;
|
||||||
if let Some(newest_selection_head) = newest_selection_head {
|
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()) {
|
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
||||||
self.layout_context_menu(
|
self.layout_cursor_popovers(
|
||||||
line_height,
|
line_height,
|
||||||
&text_hitbox,
|
&text_hitbox,
|
||||||
content_origin,
|
content_origin,
|
||||||
|
@ -7068,7 +7335,8 @@ impl Element for EditorElement {
|
||||||
scroll_pixel_position,
|
scroll_pixel_position,
|
||||||
&line_layouts,
|
&line_layouts,
|
||||||
newest_selection_head,
|
newest_selection_head,
|
||||||
gutter_dimensions.width - gutter_dimensions.left_padding,
|
newest_selection_point,
|
||||||
|
&style,
|
||||||
window,
|
window,
|
||||||
cx,
|
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 {
|
let test_indicators = if gutter_settings.runnables {
|
||||||
self.layout_run_indicators(
|
self.layout_run_indicators(
|
||||||
line_height,
|
line_height,
|
||||||
|
@ -7994,6 +8272,11 @@ impl HighlightedRange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CursorPopoverType {
|
||||||
|
CodeContextMenu,
|
||||||
|
EditPrediction,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scale_vertical_mouse_autoscroll_delta(delta: Pixels) -> f32 {
|
pub fn scale_vertical_mouse_autoscroll_delta(delta: Pixels) -> f32 {
|
||||||
(delta.pow(1.5) / 100.0).into()
|
(delta.pow(1.5) / 100.0).into()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1253,7 +1253,7 @@ fn apply_hint_update(
|
||||||
editor.inlay_hint_cache.version += 1;
|
editor.inlay_hint_cache.version += 1;
|
||||||
}
|
}
|
||||||
if displayed_inlays_changed {
|
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()
|
.as_ref()
|
||||||
.expect("editor has no active completion");
|
.expect("editor has no active completion");
|
||||||
|
|
||||||
if let InlineCompletion::Move(anchor) = &completion_state.completion {
|
if let InlineCompletion::Move { target, .. } = &completion_state.completion {
|
||||||
assert(editor.buffer().read(cx).snapshot(cx), *anchor);
|
assert(editor.buffer().read(cx).snapshot(cx), *target);
|
||||||
} else {
|
} else {
|
||||||
panic!("expected move completion");
|
panic!("expected move completion");
|
||||||
}
|
}
|
||||||
|
|
|
@ -864,7 +864,7 @@ mod tests {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let snapshot = display_map.update(cx, |map, cx| {
|
let snapshot = display_map.update(cx, |map, cx| {
|
||||||
map.splice_inlays(Vec::new(), inlays, cx);
|
map.splice_inlays(&[], inlays, cx);
|
||||||
map.snapshot(cx)
|
map.snapshot(cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ use fs::MTime;
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext as _, Context, Entity, EventEmitter, HighlightStyle, Pixels,
|
AnyElement, App, AppContext as _, Context, Entity, EventEmitter, HighlightStyle, Pixels,
|
||||||
SharedString, Task, TaskLabel, Window,
|
SharedString, StyledText, Task, TaskLabel, TextStyle, Window,
|
||||||
};
|
};
|
||||||
use lsp::LanguageServerId;
|
use lsp::LanguageServerId;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -617,6 +617,11 @@ impl HighlightedText {
|
||||||
);
|
);
|
||||||
highlighted_text.build()
|
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 {
|
impl HighlightedTextBuilder {
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
#![allow(missing_docs)]
|
#![allow(missing_docs)]
|
||||||
use crate::PlatformStyle;
|
use crate::PlatformStyle;
|
||||||
use crate::{h_flex, prelude::*, Icon, IconName, IconSize};
|
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)]
|
#[derive(Debug, IntoElement, Clone)]
|
||||||
pub struct KeyBinding {
|
pub struct KeyBinding {
|
||||||
|
@ -41,30 +43,6 @@ impl KeyBinding {
|
||||||
Some(Self::new(key_binding))
|
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 {
|
pub fn new(key_binding: gpui::KeyBinding) -> Self {
|
||||||
Self {
|
Self {
|
||||||
key_binding,
|
key_binding,
|
||||||
|
@ -96,63 +74,148 @@ impl RenderOnce for KeyBinding {
|
||||||
.gap(DynamicSpacing::Base04.rems(cx))
|
.gap(DynamicSpacing::Base04.rems(cx))
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
|
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
|
||||||
let key_icon = self.icon_for_key(keystroke);
|
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.rounded_sm()
|
.rounded_sm()
|
||||||
.text_color(cx.theme().colors().text_muted)
|
.text_color(cx.theme().colors().text_muted)
|
||||||
.when(keystroke.modifiers.function, |el| {
|
.children(render_modifiers(
|
||||||
match self.platform_style {
|
&keystroke.modifiers,
|
||||||
PlatformStyle::Mac => el.child(Key::new("fn")),
|
self.platform_style,
|
||||||
PlatformStyle::Linux | PlatformStyle::Windows => {
|
None,
|
||||||
el.child(Key::new("Fn")).child(Key::new("+"))
|
))
|
||||||
}
|
.map(|el| el.child(render_key(&keystroke, self.platform_style, None)))
|
||||||
}
|
|
||||||
})
|
|
||||||
.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())),
|
|
||||||
})
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)]
|
#[derive(IntoElement)]
|
||||||
pub struct Key {
|
pub struct Key {
|
||||||
key: SharedString,
|
key: SharedString,
|
||||||
|
color: Option<Color>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Key {
|
impl RenderOnce for Key {
|
||||||
|
@ -174,33 +237,37 @@ impl RenderOnce for Key {
|
||||||
.h(rems_from_px(14.))
|
.h(rems_from_px(14.))
|
||||||
.text_ui(cx)
|
.text_ui(cx)
|
||||||
.line_height(relative(1.))
|
.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())
|
.child(self.key.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Key {
|
impl Key {
|
||||||
pub fn new(key: impl Into<SharedString>) -> Self {
|
pub fn new(key: impl Into<SharedString>, color: Option<Color>) -> Self {
|
||||||
Self { key: key.into() }
|
Self {
|
||||||
|
key: key.into(),
|
||||||
|
color,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct KeyIcon {
|
pub struct KeyIcon {
|
||||||
icon: IconName,
|
icon: IconName,
|
||||||
|
color: Option<Color>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for KeyIcon {
|
impl RenderOnce for KeyIcon {
|
||||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
Icon::new(self.icon)
|
Icon::new(self.icon)
|
||||||
.size(IconSize::XSmall)
|
.size(IconSize::XSmall)
|
||||||
.color(Color::Muted)
|
.color(self.color.unwrap_or(Color::Muted))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyIcon {
|
impl KeyIcon {
|
||||||
pub fn new(icon: IconName) -> Self {
|
pub fn new(icon: IconName, color: Option<Color>) -> Self {
|
||||||
Self { icon }
|
Self { icon, color }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3441,7 +3441,7 @@ mod test {
|
||||||
let range = editor.selections.newest_anchor().range();
|
let range = editor.selections.newest_anchor().range();
|
||||||
let inlay_text = " field: int,\n field2: string\n field3: float";
|
let inlay_text = " field: int,\n field2: string\n field3: float";
|
||||||
let inlay = Inlay::inline_completion(1, range.start, inlay_text);
|
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");
|
cx.simulate_keystrokes("j");
|
||||||
|
@ -3473,7 +3473,7 @@ mod test {
|
||||||
snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
|
snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
|
||||||
let inlay_text = " hint";
|
let inlay_text = " hint";
|
||||||
let inlay = Inlay::inline_completion(1, end_of_line, inlay_text);
|
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.simulate_keystrokes("$");
|
||||||
cx.assert_state(
|
cx.assert_state(
|
||||||
|
|
|
@ -1530,6 +1530,16 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||||
.log_err()
|
.log_err()
|
||||||
.flatten()
|
.flatten()
|
||||||
else {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue