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