Compare commits

...

10 Commits

Author SHA1 Message Date
Bennet Bo Fenner
7f319fdcaf remove unused dependency 2024-12-02 11:02:53 +01:00
Bennet Bo Fenner
7f9b244317 clippy 2024-12-02 10:40:28 +01:00
Bennet Bo Fenner
aae838802f add missing function 2024-12-02 10:19:54 +01:00
Bennet Bo Fenner
89894f33af Merge branch 'main' into new-completion-proposal-api 2024-12-02 10:11:55 +01:00
Bennet Bo Fenner
e1e40767b1 Cleanup UI 2024-12-02 10:08:20 +01:00
Bennet Bo Fenner
69bdf4f993 cleanup 2024-12-02 09:50:40 +01:00
Bennet Bo Fenner
f35a5e80a7 WIP tab to jump to next inline completion 2024-12-01 21:42:09 +01:00
Bennet Bo Fenner
51a1eca23f Add hints when suggesting is outside of viewport 2024-12-01 13:17:38 +01:00
Bennet Bo Fenner
f97daec14d simplify completion edit api 2024-11-28 17:37:57 +01:00
Bennet Bo Fenner
8bc24708d4 editor: Change inline completion API to support predictive edits 2024-11-26 14:10:27 +01:00
8 changed files with 466 additions and 139 deletions

1
Cargo.lock generated
View File

@@ -6159,7 +6159,6 @@ version = "0.1.0"
dependencies = [
"gpui",
"language",
"project",
"text",
]

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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,

View File

@@ -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 {

View File

@@ -14,5 +14,4 @@ path = "src/inline_completion.rs"
[dependencies]
gpui.workspace = true
language.workspace = true
project.workspace = true
text.workspace = true

View File

@@ -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 {

View File

@@ -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 {