Compare commits
10 Commits
bash-timeo
...
new-comple
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f319fdcaf | ||
|
|
7f9b244317 | ||
|
|
aae838802f | ||
|
|
89894f33af | ||
|
|
e1e40767b1 | ||
|
|
69bdf4f993 | ||
|
|
f35a5e80a7 | ||
|
|
51a1eca23f | ||
|
|
f97daec14d | ||
|
|
8bc24708d4 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -6159,7 +6159,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"project",
|
||||
"text",
|
||||
]
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{Completion, Copilot};
|
||||
use anyhow::Result;
|
||||
use client::telemetry::Telemetry;
|
||||
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
|
||||
use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
|
||||
use inline_completion::{CompletionEdit, CompletionProposal, Direction, InlineCompletionProvider};
|
||||
use language::{
|
||||
language_settings::{all_language_settings, AllLanguageSettings},
|
||||
Buffer, OffsetRangeExt, ToOffset,
|
||||
@@ -267,13 +267,12 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
if completion_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
let position = cursor_position.bias_right(buffer);
|
||||
Some(CompletionProposal {
|
||||
inlays: vec![InlayProposal::Suggestion(
|
||||
cursor_position.bias_right(buffer),
|
||||
completion_text.into(),
|
||||
)],
|
||||
text: completion_text.into(),
|
||||
delete_range: None,
|
||||
edits: vec![CompletionEdit {
|
||||
text: completion_text.into(),
|
||||
range: position..position,
|
||||
}],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -87,7 +87,7 @@ use hunk_diff::{diff_hunk_to_display, ExpandedHunks};
|
||||
use indent_guides::ActiveIndentGuidesState;
|
||||
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||
pub use inline_completion::Direction;
|
||||
use inline_completion::{InlayProposal, InlineCompletionProvider, InlineCompletionProviderHandle};
|
||||
use inline_completion::{InlineCompletionProvider, InlineCompletionProviderHandle};
|
||||
pub use items::MAX_TAB_TITLE_LEN;
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
@@ -437,20 +437,115 @@ pub fn make_inlay_hints_style(cx: &WindowContext) -> HighlightStyle {
|
||||
|
||||
type CompletionId = usize;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct CompletionState {
|
||||
// render_inlay_ids represents the inlay hints that are inserted
|
||||
// for rendering the inline completions. They may be discontinuous
|
||||
// in the event that the completion provider returns some intersection
|
||||
// with the existing content.
|
||||
render_inlay_ids: Vec<InlayId>,
|
||||
// text is the resulting rope that is inserted when the user accepts a completion.
|
||||
text: Rope,
|
||||
// position is the position of the cursor when the completion was triggered.
|
||||
position: multi_buffer::Anchor,
|
||||
// delete_range is the range of text that this completion state covers.
|
||||
// if the completion is accepted, this range should be deleted.
|
||||
delete_range: Option<Range<multi_buffer::Anchor>>,
|
||||
proposal: inline_completion::CompletionProposal,
|
||||
active_edit: (usize, ComputedCompletionEdit),
|
||||
}
|
||||
|
||||
impl CompletionState {
|
||||
pub fn active_edit(&self) -> &ComputedCompletionEdit {
|
||||
&self.active_edit.1
|
||||
}
|
||||
|
||||
pub fn advance(
|
||||
self,
|
||||
excerpt_id: ExcerptId,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) -> (ComputedCompletionEdit, Option<Self>) {
|
||||
let (curr_ix, old_computed_edit) = self.active_edit;
|
||||
let next_ix = curr_ix + 1;
|
||||
|
||||
let mut this = None;
|
||||
if next_ix < self.proposal.edits.len() {
|
||||
if let Some(computed_edit) = ComputedCompletionEdit::from_edit(
|
||||
&self.proposal.edits[next_ix],
|
||||
excerpt_id,
|
||||
snapshot,
|
||||
) {
|
||||
this = Some(Self {
|
||||
proposal: self.proposal,
|
||||
active_edit: (next_ix, computed_edit),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
(old_computed_edit, this)
|
||||
}
|
||||
}
|
||||
|
||||
struct CompletionEditDeletion;
|
||||
|
||||
enum ComputedCompletionEdit {
|
||||
Insertion {
|
||||
text: Rope,
|
||||
position: multi_buffer::Anchor,
|
||||
render_inlay_ids: Vec<InlayId>,
|
||||
},
|
||||
Diff {
|
||||
text: Rope,
|
||||
range: Range<multi_buffer::Anchor>,
|
||||
additions: Vec<Range<usize>>,
|
||||
deletions: Vec<Range<multi_buffer::Anchor>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ComputedCompletionEdit {
|
||||
fn from_edit(
|
||||
edit: &inline_completion::CompletionEdit,
|
||||
excerpt_id: ExcerptId,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) -> Option<Self> {
|
||||
let text = edit.text.clone();
|
||||
|
||||
let start = snapshot.anchor_in_excerpt(excerpt_id, edit.range.start)?;
|
||||
let end = snapshot.anchor_in_excerpt(excerpt_id, edit.range.end)?;
|
||||
|
||||
let old_text = snapshot.text_for_range(start..end).collect::<String>();
|
||||
|
||||
if old_text.is_empty() {
|
||||
Some(Self::Insertion {
|
||||
position: start,
|
||||
text,
|
||||
render_inlay_ids: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
let new_text = edit.text.to_string();
|
||||
let diff = similar::TextDiff::from_chars(&old_text, &new_text);
|
||||
|
||||
let start_offset = start.to_offset(snapshot);
|
||||
|
||||
let mut deletions = Vec::new();
|
||||
let mut additions = Vec::new();
|
||||
for op in diff.ops() {
|
||||
let (change, old, new) = op.as_tag_tuple();
|
||||
let old = cmp::min(old.start + start_offset, snapshot.len())
|
||||
..cmp::min(old.end + start_offset, snapshot.len());
|
||||
match change {
|
||||
similar::DiffTag::Equal => continue,
|
||||
similar::DiffTag::Delete => deletions.push(old.to_anchors(snapshot)),
|
||||
similar::DiffTag::Insert => additions.push(new),
|
||||
similar::DiffTag::Replace => {
|
||||
deletions.push(old.to_anchors(snapshot));
|
||||
additions.push(new);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(Self::Diff {
|
||||
text,
|
||||
range: start..end,
|
||||
additions,
|
||||
deletions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn position(&self) -> Anchor {
|
||||
match self {
|
||||
ComputedCompletionEdit::Insertion { position, .. } => *position,
|
||||
ComputedCompletionEdit::Diff { range, .. } => range.start,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)]
|
||||
@@ -2739,7 +2834,8 @@ impl Editor {
|
||||
self.refresh_code_actions(cx);
|
||||
self.refresh_document_highlights(cx);
|
||||
refresh_matching_bracket_highlights(self, cx);
|
||||
self.discard_inline_completion(false, cx);
|
||||
// TODO: Figure out how to make this work for old providers
|
||||
// self.discard_inline_completion(false, cx);
|
||||
linked_editing_ranges::refresh_linked_ranges(self, cx);
|
||||
if self.git_blame_inline_enabled {
|
||||
self.start_inline_blame_timer(cx);
|
||||
@@ -5310,22 +5406,27 @@ impl Editor {
|
||||
_: &AcceptInlineCompletion,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let Some(completion) = self.take_active_inline_completion(cx) else {
|
||||
if self.move_to_active_inline_completion(cx) {
|
||||
return;
|
||||
}
|
||||
let Some(completion_edit) = self.take_active_inline_completion(false, cx) else {
|
||||
return;
|
||||
};
|
||||
if let Some(provider) = self.inline_completion_provider() {
|
||||
provider.accept(cx);
|
||||
}
|
||||
|
||||
let (selection_range, text) = match completion_edit {
|
||||
ComputedCompletionEdit::Insertion { position, text, .. } => (position..position, text),
|
||||
ComputedCompletionEdit::Diff { range, text, .. } => (range, text),
|
||||
};
|
||||
|
||||
self.change_selections(None, cx, |s| s.select_ranges([selection_range]));
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: None,
|
||||
text: completion.text.to_string().into(),
|
||||
text: text.to_string().into(),
|
||||
});
|
||||
|
||||
if let Some(range) = completion.delete_range {
|
||||
self.change_selections(None, cx, |s| s.select_ranges([range]))
|
||||
}
|
||||
self.insert_with_autoindent_mode(&completion.text.to_string(), None, cx);
|
||||
self.insert_with_autoindent_mode(&text.to_string(), None, cx);
|
||||
self.refresh_inline_completion(true, true, cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -5335,35 +5436,42 @@ impl Editor {
|
||||
_: &AcceptPartialInlineCompletion,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if self.move_to_active_inline_completion(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.selections.count() == 1 && self.has_active_inline_completion(cx) {
|
||||
if let Some(completion) = self.take_active_inline_completion(cx) {
|
||||
let mut partial_completion = completion
|
||||
.text
|
||||
.chars()
|
||||
.by_ref()
|
||||
.take_while(|c| c.is_alphabetic())
|
||||
.collect::<String>();
|
||||
if partial_completion.is_empty() {
|
||||
partial_completion = completion
|
||||
.text
|
||||
let mut is_insertion = false;
|
||||
if let Some(state) = &self.active_inline_completion {
|
||||
if let ComputedCompletionEdit::Insertion { text, .. } = state.active_edit() {
|
||||
let mut partial_completion = text
|
||||
.chars()
|
||||
.by_ref()
|
||||
.take_while(|c| c.is_whitespace() || !c.is_alphabetic())
|
||||
.take_while(|c| c.is_alphabetic())
|
||||
.collect::<String>();
|
||||
if partial_completion.is_empty() {
|
||||
partial_completion = text
|
||||
.chars()
|
||||
.by_ref()
|
||||
.take_while(|c| c.is_whitespace() || !c.is_alphabetic())
|
||||
.collect::<String>();
|
||||
}
|
||||
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: None,
|
||||
text: partial_completion.clone().into(),
|
||||
});
|
||||
|
||||
self.insert_with_autoindent_mode(&partial_completion, None, cx);
|
||||
|
||||
self.refresh_inline_completion(true, true, cx);
|
||||
is_insertion = true;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: None,
|
||||
text: partial_completion.clone().into(),
|
||||
});
|
||||
|
||||
if let Some(range) = completion.delete_range {
|
||||
self.change_selections(None, cx, |s| s.select_ranges([range]))
|
||||
}
|
||||
self.insert_with_autoindent_mode(&partial_completion, None, cx);
|
||||
|
||||
self.refresh_inline_completion(true, true, cx);
|
||||
cx.notify();
|
||||
if is_insertion {
|
||||
self.take_active_inline_completion(false, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5377,34 +5485,75 @@ impl Editor {
|
||||
provider.discard(should_report_inline_completion_event, cx);
|
||||
}
|
||||
|
||||
self.take_active_inline_completion(cx).is_some()
|
||||
self.take_active_inline_completion(true, cx).is_some()
|
||||
}
|
||||
|
||||
pub fn has_active_inline_completion(&self, cx: &AppContext) -> bool {
|
||||
if let Some(completion) = self.active_inline_completion.as_ref() {
|
||||
let buffer = self.buffer.read(cx).read(cx);
|
||||
completion.position.is_valid(&buffer)
|
||||
completion.active_edit().position().is_valid(&buffer)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn move_to_active_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if let Some(target) = self.cursor_needs_repositioning_for_inline_completion(cx) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges(vec![target..target])
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_needs_repositioning_for_inline_completion(&self, cx: &AppContext) -> Option<Anchor> {
|
||||
let completion = self.active_inline_completion.as_ref()?;
|
||||
|
||||
let cursor_position = self.selections.newest_anchor().head();
|
||||
let active_edit_position = completion.active_edit().position();
|
||||
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
|
||||
// TODO: Decide which approach we want to use to determine if we should move cursor to completion:
|
||||
// - Check if cursor is on the same line as completion
|
||||
// - Evaluate if cursor is within N rows of completion
|
||||
if cursor_position.to_point(&snapshot).row == active_edit_position.to_point(&snapshot).row {
|
||||
return None;
|
||||
}
|
||||
Some(active_edit_position)
|
||||
}
|
||||
|
||||
fn take_active_inline_completion(
|
||||
&mut self,
|
||||
discard_subsequent_edits: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<CompletionState> {
|
||||
let completion = self.active_inline_completion.take()?;
|
||||
let render_inlay_ids = completion.render_inlay_ids.clone();
|
||||
self.display_map.update(cx, |map, cx| {
|
||||
map.splice_inlays(render_inlay_ids, Default::default(), cx);
|
||||
});
|
||||
let buffer = self.buffer.read(cx).read(cx);
|
||||
) -> Option<ComputedCompletionEdit> {
|
||||
let state = self.active_inline_completion.take()?;
|
||||
let cursor = self.selections.newest_anchor().head();
|
||||
let excerpt_id = cursor.excerpt_id;
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
|
||||
if completion.position.is_valid(&buffer) {
|
||||
Some(completion)
|
||||
let (edit, state) = if discard_subsequent_edits {
|
||||
(state.active_edit.1, None)
|
||||
} else {
|
||||
None
|
||||
state.advance(excerpt_id, &snapshot)
|
||||
};
|
||||
match &edit {
|
||||
ComputedCompletionEdit::Insertion {
|
||||
render_inlay_ids, ..
|
||||
} => {
|
||||
self.display_map.update(cx, |map, cx| {
|
||||
map.splice_inlays(render_inlay_ids.clone(), Default::default(), cx);
|
||||
});
|
||||
}
|
||||
ComputedCompletionEdit::Diff { .. } => {
|
||||
self.clear_highlights::<CompletionEditDeletion>(cx);
|
||||
}
|
||||
}
|
||||
self.active_inline_completion = state;
|
||||
Some(edit)
|
||||
}
|
||||
|
||||
fn update_visible_inline_completion(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@@ -5417,53 +5566,67 @@ impl Editor {
|
||||
&& self.completion_tasks.is_empty()
|
||||
&& selection.start == selection.end
|
||||
{
|
||||
if let Some(provider) = self.inline_completion_provider() {
|
||||
if let Some((buffer, cursor_buffer_position)) =
|
||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)
|
||||
{
|
||||
if let Some(proposal) =
|
||||
provider.active_completion_text(&buffer, cursor_buffer_position, cx)
|
||||
if self.active_inline_completion.is_none() {
|
||||
if let Some(provider) = self.inline_completion_provider() {
|
||||
if let Some((buffer, cursor_buffer_position)) =
|
||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)
|
||||
{
|
||||
let mut to_remove = Vec::new();
|
||||
if let Some(completion) = self.active_inline_completion.take() {
|
||||
to_remove.extend(completion.render_inlay_ids.iter());
|
||||
}
|
||||
|
||||
let to_add = proposal
|
||||
.inlays
|
||||
.iter()
|
||||
.filter_map(|inlay| {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let id = post_inc(&mut self.next_inlay_id);
|
||||
match inlay {
|
||||
InlayProposal::Hint(position, hint) => {
|
||||
let position =
|
||||
snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
||||
Some(Inlay::hint(id, position, hint))
|
||||
}
|
||||
InlayProposal::Suggestion(position, text) => {
|
||||
let position =
|
||||
snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
||||
Some(Inlay::suggestion(id, position, text.clone()))
|
||||
}
|
||||
if let Some(proposal) =
|
||||
provider.active_completion_text(&buffer, cursor_buffer_position, cx)
|
||||
{
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
if let Some(edit) = proposal.edits.get(0) {
|
||||
if let Some(computed) =
|
||||
ComputedCompletionEdit::from_edit(edit, excerpt_id, &snapshot)
|
||||
{
|
||||
self.active_inline_completion = Some(CompletionState {
|
||||
proposal,
|
||||
active_edit: (0, computed),
|
||||
});
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.active_inline_completion = Some(CompletionState {
|
||||
position: cursor,
|
||||
text: proposal.text,
|
||||
delete_range: proposal.delete_range.and_then(|range| {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let start = snapshot.anchor_in_excerpt(excerpt_id, range.start);
|
||||
let end = snapshot.anchor_in_excerpt(excerpt_id, range.end);
|
||||
Some(start?..end?)
|
||||
}),
|
||||
render_inlay_ids: to_add.iter().map(|i| i.id).collect(),
|
||||
if let Some(state) = self.active_inline_completion.as_mut() {
|
||||
let edit = &mut state.active_edit.1;
|
||||
match edit {
|
||||
ComputedCompletionEdit::Insertion {
|
||||
position,
|
||||
text,
|
||||
render_inlay_ids,
|
||||
..
|
||||
} => {
|
||||
let id = post_inc(&mut self.next_inlay_id);
|
||||
let new_inlays = vec![Inlay::suggestion(id, *position, text.clone())];
|
||||
let new_inlay_ids: Vec<_> = new_inlays.iter().map(|i| i.id).collect();
|
||||
self.display_map.update(cx, {
|
||||
let old_inlay_ids = render_inlay_ids.clone();
|
||||
move |map, cx| map.splice_inlays(old_inlay_ids, new_inlays.clone(), cx)
|
||||
});
|
||||
*render_inlay_ids = new_inlay_ids;
|
||||
|
||||
self.display_map
|
||||
.update(cx, move |map, cx| map.splice_inlays(to_remove, to_add, cx));
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
ComputedCompletionEdit::Diff { deletions, .. } => {
|
||||
self.display_map.update(cx, {
|
||||
let deletions = deletions.clone();
|
||||
move |map, cx| {
|
||||
map.highlight_text(
|
||||
TypeId::of::<CompletionEditDeletion>(),
|
||||
deletions,
|
||||
HighlightStyle {
|
||||
background_color: Some(
|
||||
cx.theme().status().deleted_background,
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
return;
|
||||
|
||||
@@ -16,13 +16,13 @@ use crate::{
|
||||
items::BufferSearchHighlights,
|
||||
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
|
||||
scroll::scroll_amount::ScrollAmount,
|
||||
BlockId, ChunkReplacement, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint,
|
||||
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
|
||||
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
|
||||
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, JumpData, LineDown, LineUp, OpenExcerpts,
|
||||
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
|
||||
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
|
||||
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
BlockId, ChunkReplacement, CodeActionsMenu, ComputedCompletionEdit, CursorShape, CustomBlockId,
|
||||
DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
|
||||
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions,
|
||||
HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, JumpData, LineDown, LineUp,
|
||||
OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap,
|
||||
ToPoint, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED,
|
||||
MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
};
|
||||
use client::ParticipantIndex;
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
@@ -31,7 +31,7 @@ use gpui::{
|
||||
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
|
||||
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
|
||||
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
|
||||
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
|
||||
FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
|
||||
StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
|
||||
@@ -2724,6 +2724,132 @@ impl EditorElement {
|
||||
true
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_inline_completion_popover(
|
||||
&self,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
visible_row_range: Range<DisplayRow>,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
text_style: &gpui::TextStyle,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
const X_OFFSET: f32 = 25.;
|
||||
const PADDING_Y: f32 = 2.;
|
||||
|
||||
let accept_completion_keystroke = cx.keystroke_text_for_action_in(
|
||||
&crate::AcceptInlineCompletion,
|
||||
&self.editor.read(cx).focus_handle,
|
||||
);
|
||||
|
||||
let edit = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.active_inline_completion
|
||||
.as_ref()?
|
||||
.active_edit();
|
||||
|
||||
let show_go_to_edit_hint = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.cursor_needs_repositioning_for_inline_completion(cx)
|
||||
.is_some();
|
||||
|
||||
let upper_left = edit.position().to_display_point(editor_snapshot);
|
||||
let is_visible = visible_row_range.contains(&upper_left.row());
|
||||
|
||||
let container_element = div()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.px_1();
|
||||
|
||||
let (origin, mut element) = if show_go_to_edit_hint {
|
||||
if is_visible {
|
||||
let element = container_element
|
||||
.child(Label::new(format!(
|
||||
"{} jump to edit",
|
||||
accept_completion_keystroke
|
||||
)))
|
||||
.into_any();
|
||||
|
||||
let len = editor_snapshot.line_len(upper_left.row());
|
||||
let popup_position = DisplayPoint::new(upper_left.row(), len);
|
||||
let origin = self.editor.update(cx, |editor, cx| {
|
||||
editor.display_to_pixel_point(popup_position, editor_snapshot, cx)
|
||||
})?;
|
||||
(
|
||||
text_bounds.origin + origin + point(px(X_OFFSET), px(0.)),
|
||||
element,
|
||||
)
|
||||
} else {
|
||||
let is_above = visible_row_range.start > upper_left.row();
|
||||
|
||||
let mut element = container_element
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(format!(
|
||||
"{} jump to edit",
|
||||
accept_completion_keystroke
|
||||
)))
|
||||
.child(Icon::new(if is_above {
|
||||
IconName::ArrowUp
|
||||
} else {
|
||||
IconName::ArrowDown
|
||||
})),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
|
||||
|
||||
let offset_y = if is_above {
|
||||
px(PADDING_Y)
|
||||
} else {
|
||||
text_bounds.size.height - size.height - px(PADDING_Y)
|
||||
};
|
||||
|
||||
let origin = text_bounds.origin
|
||||
+ point((text_bounds.size.width - size.width) / 2., offset_y);
|
||||
(origin, element)
|
||||
}
|
||||
} else if let ComputedCompletionEdit::Diff {
|
||||
text, additions, ..
|
||||
} = edit
|
||||
{
|
||||
let row = &line_layouts[upper_left.row().minus(visible_row_range.start) as usize];
|
||||
let origin = text_bounds.origin
|
||||
+ point(
|
||||
row.width + px(X_OFFSET) - scroll_pixel_position.x,
|
||||
upper_left.row().as_f32() * line_height - scroll_pixel_position.y,
|
||||
);
|
||||
|
||||
let text = gpui::StyledText::new(text.to_string()).with_highlights(
|
||||
text_style,
|
||||
additions.iter().map(|range| {
|
||||
(
|
||||
range.clone(),
|
||||
HighlightStyle {
|
||||
background_color: Some(cx.theme().status().created_background),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
let element = container_element.child(text).into_any();
|
||||
(origin, element)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
element.prepaint_as_root(origin, AvailableSpace::min_size(), cx);
|
||||
Some(element)
|
||||
}
|
||||
|
||||
fn layout_mouse_context_menu(
|
||||
&self,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
@@ -3952,6 +4078,16 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_inline_completion_popover(
|
||||
&mut self,
|
||||
layout: &mut EditorLayout,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if let Some(inline_completion_popover) = layout.inline_completion_popover.as_mut() {
|
||||
inline_completion_popover.paint(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_mouse_context_menu(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
|
||||
if let Some(mouse_context_menu) = layout.mouse_context_menu.as_mut() {
|
||||
mouse_context_menu.paint(cx);
|
||||
@@ -5582,6 +5718,17 @@ impl Element for EditorElement {
|
||||
);
|
||||
}
|
||||
|
||||
let inline_completion_popover = self.layout_inline_completion_popover(
|
||||
&text_hitbox.bounds,
|
||||
&snapshot,
|
||||
start_row..end_row,
|
||||
&line_layouts,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
&style.text,
|
||||
cx,
|
||||
);
|
||||
|
||||
let mouse_context_menu =
|
||||
self.layout_mouse_context_menu(&snapshot, start_row..end_row, cx);
|
||||
|
||||
@@ -5664,6 +5811,7 @@ impl Element for EditorElement {
|
||||
cursors,
|
||||
visible_cursors,
|
||||
selections,
|
||||
inline_completion_popover,
|
||||
mouse_context_menu,
|
||||
test_indicators,
|
||||
code_actions_indicator,
|
||||
@@ -5753,6 +5901,7 @@ impl Element for EditorElement {
|
||||
}
|
||||
|
||||
self.paint_scrollbar(layout, cx);
|
||||
self.paint_inline_completion_popover(layout, cx);
|
||||
self.paint_mouse_context_menu(layout, cx);
|
||||
});
|
||||
})
|
||||
@@ -5808,6 +5957,7 @@ pub struct EditorLayout {
|
||||
test_indicators: Vec<AnyElement>,
|
||||
crease_toggles: Vec<Option<AnyElement>>,
|
||||
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
|
||||
inline_completion_popover: Option<AnyElement>,
|
||||
mouse_context_menu: Option<AnyElement>,
|
||||
tab_invisible: ShapedLine,
|
||||
space_invisible: ShapedLine,
|
||||
|
||||
@@ -3089,6 +3089,26 @@ impl<'a> WindowContext<'a> {
|
||||
.unwrap_or_else(|| action.name().to_string())
|
||||
}
|
||||
|
||||
/// Represent this action as a key binding string, to display in the UI.
|
||||
pub fn keystroke_text_for_action_in(
|
||||
&self,
|
||||
action: &dyn Action,
|
||||
focus_handle: &FocusHandle,
|
||||
) -> String {
|
||||
self.bindings_for_action_in(action, focus_handle)
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|binding| {
|
||||
binding
|
||||
.keystrokes()
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
})
|
||||
.unwrap_or_else(|| action.name().to_string())
|
||||
}
|
||||
|
||||
/// Dispatch a mouse or keyboard event on the window.
|
||||
#[profiling::function]
|
||||
pub fn dispatch_event(&mut self, event: PlatformInput) -> DispatchEventResult {
|
||||
|
||||
@@ -14,5 +14,4 @@ path = "src/inline_completion.rs"
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
project.workspace = true
|
||||
text.workspace = true
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{AppContext, Model, ModelContext};
|
||||
use language::Buffer;
|
||||
use std::ops::Range;
|
||||
use text::{Anchor, Rope};
|
||||
use text::Rope;
|
||||
|
||||
// TODO: Find a better home for `Direction`.
|
||||
//
|
||||
@@ -13,15 +14,13 @@ pub enum Direction {
|
||||
Next,
|
||||
}
|
||||
|
||||
pub enum InlayProposal {
|
||||
Hint(Anchor, project::InlayHint),
|
||||
Suggestion(Anchor, Rope),
|
||||
pub struct CompletionProposal {
|
||||
pub edits: Vec<CompletionEdit>,
|
||||
}
|
||||
|
||||
pub struct CompletionProposal {
|
||||
pub inlays: Vec<InlayProposal>,
|
||||
pub struct CompletionEdit {
|
||||
pub text: Rope,
|
||||
pub delete_range: Option<Range<Anchor>>,
|
||||
pub range: Range<language::Anchor>,
|
||||
}
|
||||
|
||||
pub trait InlineCompletionProvider: 'static + Sized {
|
||||
|
||||
@@ -3,7 +3,7 @@ use anyhow::Result;
|
||||
use client::telemetry::Telemetry;
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
|
||||
use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
|
||||
use inline_completion::{CompletionEdit, CompletionProposal, Direction, InlineCompletionProvider};
|
||||
use language::{language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot};
|
||||
use std::{
|
||||
ops::{AddAssign, Range},
|
||||
@@ -57,7 +57,7 @@ fn completion_state_from_diff(
|
||||
.text_for_range(delete_range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
let mut inlays: Vec<InlayProposal> = Vec::new();
|
||||
let mut edits: Vec<CompletionEdit> = Vec::new();
|
||||
|
||||
let completion_graphemes: Vec<&str> = completion_text.graphemes(true).collect();
|
||||
let buffer_graphemes: Vec<&str> = buffer_text.graphemes(true).collect();
|
||||
@@ -74,11 +74,12 @@ fn completion_state_from_diff(
|
||||
match k {
|
||||
Some(k) => {
|
||||
if k != 0 {
|
||||
let offset = snapshot.anchor_after(offset);
|
||||
// the range from the current position to item is an inlay.
|
||||
inlays.push(InlayProposal::Suggestion(
|
||||
snapshot.anchor_after(offset),
|
||||
completion_graphemes[i..i + k].join("").into(),
|
||||
));
|
||||
edits.push(CompletionEdit {
|
||||
text: completion_graphemes[i..i + k].join("").into(),
|
||||
range: offset..offset,
|
||||
});
|
||||
}
|
||||
i += k + 1;
|
||||
j += 1;
|
||||
@@ -93,18 +94,15 @@ fn completion_state_from_diff(
|
||||
}
|
||||
|
||||
if j == buffer_graphemes.len() && i < completion_graphemes.len() {
|
||||
let offset = snapshot.anchor_after(offset);
|
||||
// there is leftover completion text, so drop it as an inlay.
|
||||
inlays.push(InlayProposal::Suggestion(
|
||||
snapshot.anchor_after(offset),
|
||||
completion_graphemes[i..].join("").into(),
|
||||
));
|
||||
edits.push(CompletionEdit {
|
||||
text: completion_graphemes[i..].join("").into(),
|
||||
range: offset..offset,
|
||||
});
|
||||
}
|
||||
|
||||
CompletionProposal {
|
||||
inlays,
|
||||
text: completion_text.into(),
|
||||
delete_range: Some(delete_range),
|
||||
}
|
||||
CompletionProposal { edits }
|
||||
}
|
||||
|
||||
impl InlineCompletionProvider for SupermavenCompletionProvider {
|
||||
|
||||
Reference in New Issue
Block a user