editor: Add InlineCompletion LSP support (#1736)
Closes [1729](https://github.com/longbridge/gpui-component/issues/1729) ## Description Adds support for inline completions, which allows the editor to support agentic "tab to complete" solutions. This works, (tested macOS), but not sure of any gotchas or different approaches. ## Screenshot https://github.com/user-attachments/assets/8e437f1e-9792-4417-b070-0a1dd8de6024 ## How to Test User implements the `inline_completion` method in CompletionsProvider. Not implementing should have no effect. ## Checklist - [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and followed the guidelines. - [x] Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate. - [x] Passed `cargo run` for story tests related to the changes. - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific) --------- Co-authored-by: Jason Lee <huacnlee@gmail.com>
This commit is contained in:
parent
3ecd9c7606
commit
3c23ee9be4
7 changed files with 401 additions and 13 deletions
|
|
@ -34,7 +34,7 @@ sum-tree = { version = "0.2.0", package = "zed-sum-tree" }
|
|||
|
||||
anyhow = "1"
|
||||
log = "0.4"
|
||||
lsp-types = "0.97.0"
|
||||
lsp-types = { version = "0.97.0", features = ["proposed"] }
|
||||
notify = "7.0.0"
|
||||
raw-window-handle = "0.6.2"
|
||||
ropey = { version = "=2.0.0-beta.1", features = [
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ use gpui_component_assets::Assets;
|
|||
use gpui_component_story::Open;
|
||||
use lsp_types::{
|
||||
CodeAction, CodeActionKind, CompletionContext, CompletionItem, CompletionResponse,
|
||||
CompletionTextEdit, InsertReplaceEdit, TextEdit, WorkspaceEdit,
|
||||
CompletionTextEdit, InlineCompletionContext, InlineCompletionItem, InlineCompletionResponse,
|
||||
InsertReplaceEdit, InsertTextFormat, TextEdit, WorkspaceEdit,
|
||||
};
|
||||
|
||||
fn init() {
|
||||
|
|
@ -187,6 +188,45 @@ impl CompletionProvider for ExampleLspStore {
|
|||
})
|
||||
}
|
||||
|
||||
fn inline_completion(
|
||||
&self,
|
||||
rope: &Rope,
|
||||
offset: usize,
|
||||
_trigger: InlineCompletionContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<InputState>,
|
||||
) -> Task<Result<InlineCompletionResponse>> {
|
||||
let rope = rope.clone();
|
||||
cx.background_spawn(async move {
|
||||
// Get the current line text before cursor using RopeExt
|
||||
let point = rope.offset_to_point(offset);
|
||||
let line_start = rope.line_start_offset(point.row);
|
||||
let current_line = rope.slice(line_start..offset).to_string();
|
||||
|
||||
// Simple pattern matching for demo
|
||||
let suggestion =
|
||||
if current_line.trim_start().starts_with("fn ") && !current_line.contains('{') {
|
||||
Some("() {\n // Write your code here..\n}".into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(insert_text) = suggestion {
|
||||
Ok(InlineCompletionResponse::Array(vec![
|
||||
InlineCompletionItem {
|
||||
insert_text,
|
||||
filter_text: None,
|
||||
range: None,
|
||||
command: None,
|
||||
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
||||
},
|
||||
]))
|
||||
} else {
|
||||
Ok(InlineCompletionResponse::Array(vec![]))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
_offset: usize,
|
||||
|
|
|
|||
|
|
@ -558,6 +558,91 @@ impl TextElement {
|
|||
(line_number_width, line_number_len)
|
||||
}
|
||||
|
||||
/// Compute inline completion ghost lines for rendering.
|
||||
///
|
||||
/// Returns (first_line, ghost_lines) where:
|
||||
/// - first_line: Shaped text for the first line (goes after cursor on same line)
|
||||
/// - ghost_lines: Shaped lines for subsequent lines (shift content down)
|
||||
fn layout_inline_completion(
|
||||
state: &InputState,
|
||||
visible_range: &Range<usize>,
|
||||
font_size: Pixels,
|
||||
window: &mut Window,
|
||||
cx: &App,
|
||||
) -> (Option<ShapedLine>, Vec<ShapedLine>) {
|
||||
// Must be focused to show inline completion
|
||||
if !state.focus_handle.is_focused(window) {
|
||||
return (None, vec![]);
|
||||
}
|
||||
|
||||
let Some(completion_item) = state.inline_completion.item.as_ref() else {
|
||||
return (None, vec![]);
|
||||
};
|
||||
|
||||
// Get cursor row from cursor position
|
||||
let cursor_row = state.cursor_position().line as usize;
|
||||
|
||||
// Only show if cursor row is visible
|
||||
if cursor_row < visible_range.start || cursor_row >= visible_range.end {
|
||||
return (None, vec![]);
|
||||
}
|
||||
|
||||
let completion_text = &completion_item.insert_text;
|
||||
let completion_color = cx.theme().muted_foreground.opacity(0.5);
|
||||
|
||||
let text_style = window.text_style();
|
||||
let font = text_style.font();
|
||||
|
||||
let lines: Vec<&str> = completion_text.split('\n').collect();
|
||||
if lines.is_empty() {
|
||||
return (None, vec![]);
|
||||
}
|
||||
|
||||
// Shape first line (goes after cursor)
|
||||
let first_text: SharedString = lines[0].to_string().into();
|
||||
let first_line = if !first_text.is_empty() {
|
||||
let first_run = TextRun {
|
||||
len: first_text.len(),
|
||||
font: font.clone(),
|
||||
color: completion_color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
Some(
|
||||
window
|
||||
.text_system()
|
||||
.shape_line(first_text, font_size, &[first_run], None),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Shape ghost lines (lines 2+ that shift content down)
|
||||
let ghost_lines: Vec<ShapedLine> = lines[1..]
|
||||
.iter()
|
||||
.map(|line_text| {
|
||||
let text: SharedString = line_text.to_string().into();
|
||||
let len = text.len().max(1); // Ensure at least 1 for empty lines
|
||||
let run = TextRun {
|
||||
len,
|
||||
font: font.clone(),
|
||||
color: completion_color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
// Use space for empty lines so they take up height
|
||||
let shaped_text = if text.is_empty() { " ".into() } else { text };
|
||||
window
|
||||
.text_system()
|
||||
.shape_line(shaped_text, font_size, &[run], None)
|
||||
})
|
||||
.collect();
|
||||
|
||||
(first_line, ghost_lines)
|
||||
}
|
||||
|
||||
fn layout_lines(
|
||||
state: &InputState,
|
||||
display_text: &Rope,
|
||||
|
|
@ -725,6 +810,22 @@ pub(super) struct PrepaintState {
|
|||
hover_definition_hitbox: Option<Hitbox>,
|
||||
indent_guides_path: Option<Path<Pixels>>,
|
||||
bounds: Bounds<Pixels>,
|
||||
// Inline completion rendering data
|
||||
/// Shaped ghost lines to paint after cursor row (completion lines 2+)
|
||||
ghost_lines: Vec<ShapedLine>,
|
||||
/// First line of inline completion (painted after cursor on same line)
|
||||
ghost_first_line: Option<ShapedLine>,
|
||||
ghost_lines_height: Pixels,
|
||||
}
|
||||
|
||||
impl PrepaintState {
|
||||
/// Returns cursor bounds adjusted for scroll offset, if available.
|
||||
fn cursor_bounds_with_scroll(&self) -> Option<Bounds<Pixels>> {
|
||||
self.cursor_bounds.map(|mut bounds| {
|
||||
bounds.origin.y += self.cursor_scroll_offset.y;
|
||||
bounds
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for TextElement {
|
||||
|
|
@ -995,6 +1096,16 @@ impl Element for TextElement {
|
|||
}
|
||||
last_layout.lines = Rc::new(lines);
|
||||
|
||||
let (ghost_first_line, ghost_lines) = Self::layout_inline_completion(
|
||||
state,
|
||||
&last_layout.visible_range,
|
||||
text_size,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let ghost_line_count = ghost_lines.len();
|
||||
let ghost_lines_height = ghost_line_count as f32 * line_height;
|
||||
|
||||
let total_wrapped_lines = state.text_wrapper.len();
|
||||
let empty_bottom_height = if state.mode.is_code_editor() {
|
||||
bounds
|
||||
|
|
@ -1012,7 +1123,7 @@ impl Element for TextElement {
|
|||
} else {
|
||||
longest_line_width
|
||||
},
|
||||
(total_wrapped_lines as f32 * line_height + empty_bottom_height)
|
||||
(total_wrapped_lines as f32 * line_height + empty_bottom_height + ghost_lines_height)
|
||||
.max(bounds.size.height),
|
||||
);
|
||||
|
||||
|
|
@ -1122,6 +1233,9 @@ impl Element for TextElement {
|
|||
hover_definition_hitbox,
|
||||
document_color_paths,
|
||||
indent_guides_path,
|
||||
ghost_first_line,
|
||||
ghost_lines,
|
||||
ghost_lines_height,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1246,21 +1360,49 @@ impl Element for TextElement {
|
|||
window.paint_path(path.clone(), *color);
|
||||
}
|
||||
|
||||
// Paint text
|
||||
// Paint text with inline completion ghost line support
|
||||
let mut offset_y = mask_offset_y + invisible_top_padding;
|
||||
for line in prepaint.last_layout.lines.iter() {
|
||||
let ghost_lines = &prepaint.ghost_lines;
|
||||
let has_ghost_lines = !ghost_lines.is_empty();
|
||||
|
||||
for (ix, line) in prepaint.last_layout.lines.iter().enumerate() {
|
||||
let row = visible_range.start + ix;
|
||||
let p = point(
|
||||
origin.x + prepaint.last_layout.line_number_width,
|
||||
origin.y + offset_y,
|
||||
);
|
||||
|
||||
// Paint the actual line
|
||||
_ = line.paint(p, line_height, window, cx);
|
||||
offset_y += line.size(line_height).height;
|
||||
|
||||
// After the cursor row, paint ghost lines (which shifts subsequent content down)
|
||||
if has_ghost_lines && Some(row) == prepaint.current_row {
|
||||
let ghost_x = origin.x + prepaint.last_layout.line_number_width;
|
||||
|
||||
for ghost_line in ghost_lines {
|
||||
let ghost_p = point(ghost_x, origin.y + offset_y);
|
||||
|
||||
// Paint semi-transparent background for ghost line
|
||||
let ghost_bounds = Bounds::new(
|
||||
ghost_p,
|
||||
size(
|
||||
bounds.size.width - prepaint.last_layout.line_number_width,
|
||||
line_height,
|
||||
),
|
||||
);
|
||||
window.paint_quad(fill(ghost_bounds, cx.theme().editor_background()));
|
||||
|
||||
// Paint ghost line text
|
||||
_ = ghost_line.paint(ghost_p, line_height, window, cx);
|
||||
offset_y += line_height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Paint blinking cursor
|
||||
if focused && show_cursor {
|
||||
if let Some(mut cursor_bounds) = prepaint.cursor_bounds.take() {
|
||||
cursor_bounds.origin.y += prepaint.cursor_scroll_offset.y;
|
||||
if let Some(cursor_bounds) = prepaint.cursor_bounds_with_scroll() {
|
||||
window.paint_quad(fill(cursor_bounds, cx.theme().caret));
|
||||
}
|
||||
}
|
||||
|
|
@ -1270,13 +1412,12 @@ impl Element for TextElement {
|
|||
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
|
||||
offset_y += invisible_top_padding;
|
||||
|
||||
// Paint line number background
|
||||
window.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: input_bounds.origin,
|
||||
size: size(
|
||||
prepaint.last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN,
|
||||
input_bounds.size.height,
|
||||
input_bounds.size.height + prepaint.ghost_lines_height,
|
||||
),
|
||||
},
|
||||
cx.theme().editor_background(),
|
||||
|
|
@ -1304,6 +1445,11 @@ impl Element for TextElement {
|
|||
_ = line.paint(p, line_height, window, cx);
|
||||
offset_y += line_height;
|
||||
}
|
||||
|
||||
// Add ghost line height after cursor row for line numbers alignment
|
||||
if !prepaint.ghost_lines.is_empty() && prepaint.current_row.is_some() {
|
||||
offset_y += prepaint.ghost_lines_height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1324,6 +1470,23 @@ impl Element for TextElement {
|
|||
window.set_cursor_style(gpui::CursorStyle::PointingHand, &hitbox);
|
||||
}
|
||||
|
||||
// Paint inline completion first line suffix (after cursor on same line)
|
||||
if focused {
|
||||
if let Some(first_line) = &prepaint.ghost_first_line {
|
||||
if let Some(cursor_bounds) = prepaint.cursor_bounds_with_scroll() {
|
||||
let first_line_x = cursor_bounds.origin.x + cursor_bounds.size.width;
|
||||
let p = point(first_line_x, cursor_bounds.origin.y);
|
||||
|
||||
// Paint background to cover any existing text
|
||||
let bg_bounds = Bounds::new(p, size(first_line.width + px(4.), line_height));
|
||||
window.paint_quad(fill(bg_bounds, cx.theme().editor_background()));
|
||||
|
||||
// Paint first line completion text
|
||||
_ = first_line.paint(p, line_height, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.paint_mouse_listeners(window, cx);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,6 +222,10 @@ impl InputState {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// First, try to accept inline completion if present
|
||||
if self.accept_inline_completion(window, cx) {
|
||||
return;
|
||||
}
|
||||
self.indent(false, window, cx);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
use anyhow::Result;
|
||||
use gpui::{Context, EntityInputHandler, Task, Window};
|
||||
use lsp_types::{request::Completion, CompletionContext, CompletionItem, CompletionResponse};
|
||||
use lsp_types::{
|
||||
CompletionContext, CompletionItem, CompletionResponse, InlineCompletionContext,
|
||||
InlineCompletionItem, InlineCompletionResponse, InlineCompletionTriggerKind,
|
||||
request::Completion,
|
||||
};
|
||||
use ropey::Rope;
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc};
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc, time::Duration};
|
||||
|
||||
use crate::input::{
|
||||
popovers::{CompletionMenu, ContextMenu},
|
||||
InputState,
|
||||
popovers::{CompletionMenu, ContextMenu},
|
||||
};
|
||||
|
||||
/// Default debounce duration for inline completions.
|
||||
const DEFAULT_INLINE_COMPLETION_DEBOUNCE: Duration = Duration::from_millis(300);
|
||||
|
||||
/// A trait for providing code completions based on the current input state and context.
|
||||
pub trait CompletionProvider {
|
||||
/// Fetches completions based on the given byte offset.
|
||||
|
|
@ -27,6 +34,39 @@ pub trait CompletionProvider {
|
|||
cx: &mut Context<InputState>,
|
||||
) -> Task<Result<CompletionResponse>>;
|
||||
|
||||
/// Fetches an inline completion suggestion for the given position.
|
||||
///
|
||||
/// This is called after a debounce period when the user stops typing.
|
||||
/// The provider can analyze the text and cursor position to determine
|
||||
/// what inline completion suggestion to show.
|
||||
///
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `rope` - The current text content
|
||||
/// * `offset` - The cursor position in bytes
|
||||
///
|
||||
/// textDocument/inlineCompletion
|
||||
///
|
||||
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_inlineCompletion
|
||||
fn inline_completion(
|
||||
&self,
|
||||
_rope: &Rope,
|
||||
_offset: usize,
|
||||
_trigger: InlineCompletionContext,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<InputState>,
|
||||
) -> Task<Result<InlineCompletionResponse>> {
|
||||
Task::ready(Ok(InlineCompletionResponse::Array(vec![])))
|
||||
}
|
||||
|
||||
/// Returns the debounce duration for inline completions.
|
||||
///
|
||||
/// Default: 300ms
|
||||
#[inline]
|
||||
fn inline_completion_debounce(&self) -> Duration {
|
||||
DEFAULT_INLINE_COMPLETION_DEBOUNCE
|
||||
}
|
||||
|
||||
fn resolve_completions(
|
||||
&self,
|
||||
_completion_indices: Vec<usize>,
|
||||
|
|
@ -47,6 +87,22 @@ pub trait CompletionProvider {
|
|||
) -> bool;
|
||||
}
|
||||
|
||||
pub(crate) struct InlineCompletion {
|
||||
/// Completion item to display as an inline completion suggestion
|
||||
pub(crate) item: Option<InlineCompletionItem>,
|
||||
/// Task for debouncing inline completion requests
|
||||
pub(crate) task: Task<Result<InlineCompletionResponse>>,
|
||||
}
|
||||
|
||||
impl Default for InlineCompletion {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
item: None,
|
||||
task: Task::ready(Ok(InlineCompletionResponse::Array(vec![]))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub(crate) fn handle_completion_trigger(
|
||||
&mut self,
|
||||
|
|
@ -63,6 +119,10 @@ impl InputState {
|
|||
return;
|
||||
};
|
||||
|
||||
// Always schedule inline completion (debounced).
|
||||
// It will check if menu is open before showing the suggestion.
|
||||
self.schedule_inline_completion(window, cx);
|
||||
|
||||
let start = range.end;
|
||||
let new_offset = self.cursor();
|
||||
|
||||
|
|
@ -145,4 +205,105 @@ impl InputState {
|
|||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
/// Schedule an inline completion request after debouncing.
|
||||
pub(crate) fn schedule_inline_completion(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Clear any existing inline completion on text change
|
||||
self.clear_inline_completion(cx);
|
||||
|
||||
let Some(provider) = self.lsp.completion_provider.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let offset = self.cursor();
|
||||
let text = self.text.clone();
|
||||
let debounce = provider.inline_completion_debounce();
|
||||
|
||||
self.inline_completion.task = cx.spawn_in(window, async move |editor, cx| {
|
||||
// Debounce: wait before fetching to avoid unnecessary requests while typing
|
||||
smol::Timer::after(debounce).await;
|
||||
|
||||
// Now fetch the inline completion after the debounce period
|
||||
let task = editor.update_in(cx, |editor, window, cx| {
|
||||
// Check if cursor has moved during debounce
|
||||
if editor.cursor() != offset {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Don't fetch if completion menu is open
|
||||
if editor.is_context_menu_open(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let trigger = InlineCompletionContext {
|
||||
trigger_kind: InlineCompletionTriggerKind::Automatic,
|
||||
selected_completion_info: None,
|
||||
};
|
||||
|
||||
Some(provider.inline_completion(&text, offset, trigger, window, cx))
|
||||
})?;
|
||||
|
||||
let Some(task) = task else {
|
||||
return Ok(InlineCompletionResponse::Array(vec![]));
|
||||
};
|
||||
|
||||
let response = task.await?;
|
||||
|
||||
editor.update_in(cx, |editor, _window, cx| {
|
||||
// Only apply if cursor still hasn't moved
|
||||
if editor.cursor() != offset {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show if completion menu opened while we were fetching
|
||||
if editor.is_context_menu_open(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(item) = match response.clone() {
|
||||
InlineCompletionResponse::Array(items) => items.into_iter().next(),
|
||||
InlineCompletionResponse::List(comp_list) => comp_list.items.into_iter().next(),
|
||||
} {
|
||||
editor.inline_completion.item = Some(item);
|
||||
cx.notify();
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(response)
|
||||
});
|
||||
}
|
||||
|
||||
/// Check if an inline completion suggestion is currently displayed.
|
||||
#[inline]
|
||||
pub(crate) fn has_inline_completion(&self) -> bool {
|
||||
self.inline_completion.item.is_some()
|
||||
}
|
||||
|
||||
/// Clear the inline completion suggestion.
|
||||
pub(crate) fn clear_inline_completion(&mut self, cx: &mut Context<Self>) {
|
||||
self.inline_completion = InlineCompletion::default();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Accept the inline completion, inserting it at the cursor position.
|
||||
/// Returns true if a completion was accepted, false if there was none.
|
||||
pub(crate) fn accept_inline_completion(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let Some(completion_item) = self.inline_completion.item.take() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let cursor = self.cursor();
|
||||
let range_utf16 = self.range_to_utf16(&(cursor..cursor));
|
||||
let completion_text = completion_item.insert_text;
|
||||
self.replace_text_in_range_silent(Some(range_utf16), &completion_text, window, cx);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ impl InputState {
|
|||
self.pause_blink_cursor(cx);
|
||||
self.update_preferred_column();
|
||||
self.hide_context_menu(cx);
|
||||
self.clear_inline_completion(cx);
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ use crate::input::{
|
|||
search::{self, SearchPanel},
|
||||
text_wrapper::LineLayout,
|
||||
};
|
||||
use crate::input::{RopeExt as _, Selection};
|
||||
use crate::input::{InlineCompletion, RopeExt as _, Selection};
|
||||
use crate::{Root, history::History};
|
||||
use crate::{highlighter::DiagnosticSet, input::text_wrapper::LineItem};
|
||||
|
||||
|
|
@ -331,6 +331,7 @@ pub struct InputState {
|
|||
_subscriptions: Vec<Subscription>,
|
||||
|
||||
pub(super) _context_menu_task: Task<Result<()>>,
|
||||
pub(super) inline_completion: InlineCompletion,
|
||||
}
|
||||
|
||||
impl EventEmitter<InputEvent> for InputState {}
|
||||
|
|
@ -409,6 +410,7 @@ impl InputState {
|
|||
_subscriptions,
|
||||
_context_menu_task: Task::ready(Ok(())),
|
||||
_pending_update: false,
|
||||
inline_completion: InlineCompletion::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1142,6 +1144,11 @@ impl InputState {
|
|||
return;
|
||||
}
|
||||
|
||||
// Clear inline completion on enter (user chose not to accept it)
|
||||
if self.has_inline_completion() {
|
||||
self.clear_inline_completion(cx);
|
||||
}
|
||||
|
||||
if self.mode.is_multi_line() {
|
||||
// Get current line indent
|
||||
let indent = if self.mode.is_code_editor() {
|
||||
|
|
@ -1175,6 +1182,12 @@ impl InputState {
|
|||
return;
|
||||
}
|
||||
|
||||
// Clear inline completion on escape
|
||||
if self.has_inline_completion() {
|
||||
self.clear_inline_completion(cx);
|
||||
return; // Consume the escape, don't propagate
|
||||
}
|
||||
|
||||
if self.ime_marked_range.is_some() {
|
||||
self.unmark_text(window, cx);
|
||||
}
|
||||
|
|
@ -1192,6 +1205,9 @@ impl InputState {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Clear inline completion on any mouse interaction
|
||||
self.clear_inline_completion(cx);
|
||||
|
||||
// If there have IME marked range and is empty (Means pressed Esc to abort IME typing)
|
||||
// Clear the marked range.
|
||||
if let Some(ime_marked_range) = &self.ime_marked_range {
|
||||
|
|
@ -1596,6 +1612,8 @@ impl InputState {
|
|||
///
|
||||
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
|
||||
pub(crate) fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
|
||||
self.clear_inline_completion(cx);
|
||||
|
||||
let offset = offset.clamp(0, self.text.len());
|
||||
if self.selection_reversed {
|
||||
self.selected_range.start = offset
|
||||
|
|
@ -1697,6 +1715,7 @@ impl InputState {
|
|||
self.hover_popover = None;
|
||||
self.diagnostic_popover = None;
|
||||
self.context_menu = None;
|
||||
self.clear_inline_completion(cx);
|
||||
self.blink_cursor.update(cx, |cursor, cx| {
|
||||
cursor.stop(cx);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue