editor: Update completions to support CompletionTextEdit. (#1267)

- Fix completion menu width to longest item.
This commit is contained in:
Jason Lee 2025-09-22 11:39:50 +08:00 committed by GitHub
parent 13fa76e00d
commit 692057f799
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 144 additions and 22 deletions

View file

@ -20,8 +20,8 @@ use gpui_component::{
v_flex, ActiveTheme, ContextModal, IconName, IndexPath, Selectable, Sizable,
};
use lsp_types::{
CodeAction, CodeActionKind, CompletionContext, CompletionItem, CompletionResponse, TextEdit,
WorkspaceEdit,
CodeAction, CodeActionKind, CompletionContext, CompletionItem, CompletionResponse,
CompletionTextEdit, InsertReplaceEdit, TextEdit, WorkspaceEdit,
};
use story::Assets;
@ -166,11 +166,31 @@ impl ExampleLspStore {
}
}
fn completion_item(
range: &lsp_types::Range,
label: &str,
replace_text: &str,
documentation: &str,
) -> CompletionItem {
CompletionItem {
label: label.to_string(),
kind: Some(lsp_types::CompletionItemKind::FUNCTION),
text_edit: Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit {
new_text: replace_text.to_string(),
insert: range.clone(),
replace: range.clone(),
})),
documentation: Some(lsp_types::Documentation::String(documentation.to_string())),
insert_text: None,
..Default::default()
}
}
impl CompletionProvider for ExampleLspStore {
fn completions(
&self,
rope: &Rope,
_offset: usize,
offset: usize,
trigger: CompletionContext,
_: &mut Window,
cx: &mut Context<InputState>,
@ -181,6 +201,7 @@ impl CompletionProvider for ExampleLspStore {
}
let _ = rope.to_string(); // Just to use the rope parameter.
let pos = rope.offset_to_position(offset);
// Simulate to delay for fetching completions
let items = self.completions.clone();
@ -188,11 +209,41 @@ impl CompletionProvider for ExampleLspStore {
// Simulate a slow completion source, to test Editor async handling.
smol::Timer::after(Duration::from_millis(20)).await;
let range = lsp_types::Range::new(
pos,
lsp_types::Position {
line: pos.line,
character: pos.character + 1,
},
);
if trigger_character.starts_with("/") {
let items = vec![
completion_item(
&range,
"/date",
format!("{}", chrono::Local::now().date_naive()).as_str(),
"Insert current date",
),
completion_item(&range, "/thanks", "Thank you!", "Insert Thank you!"),
completion_item(&range, "/+1", "👍", "Insert 👍"),
completion_item(&range, "/-1", "👎", "Insert 👎"),
completion_item(&range, "/smile", "😊", "Insert 😊"),
completion_item(&range, "/sad", "😢", "Insert 😢"),
completion_item(&range, "/launch", "🚀", "Insert 🚀"),
];
return Ok(vec![CompletionResponse::Array(items)]);
}
let items = items
.iter()
.filter(|item| item.label.starts_with(&trigger_character))
.take(10)
.map(|item| item.clone())
.map(|item| {
let mut item = item.clone();
item.insert_text = Some(item.label.replace(&trigger_character, ""));
item
})
.collect::<Vec<_>>();
let responses = vec![CompletionResponse::Array(items)];

View file

@ -176,35 +176,39 @@
"documentation": "Standard library for Rust."
},
{
"label": "vec",
"label": "vec!",
"documentation": "Create a growable array.\n\n**Example:**\n```rust\nlet mut v = Vec::new();\nv.push(1);\nv.push(2);\n```"
},
{
"label": "format",
"label": "format!",
"documentation": "Format a string."
},
{
"label": "println",
"label": "println!",
"documentation": "Print to the standard output.\n\n**Example:**\n```rust\nprintln!(\"Hello, world!\");\n```"
},
{
"label": "eprintln",
"label": "eprintln!",
"documentation": "Print to the standard error."
},
{
"label": "dbg",
"label": "dbg!",
"documentation": "Debug print a value."
},
{
"label": "todo",
"label": "todo!",
"documentation": "Mark unfinished code."
},
{
"label": "unimplemented",
"label": "unimplemented!",
"documentation": "Mark unimplemented code."
},
{
"label": "unreachable",
"label": "unreachable!",
"documentation": "Mark code that should never be executed."
},
{
"label": "unimplemented!(\"your format message: {}\", ...)",
"documentation": "Mark unimplemented code with a custom message."
}
]

View file

@ -6,7 +6,7 @@ use gpui::{
HighlightStyle, InteractiveElement as _, IntoElement, ParentElement, Pixels, Point, Render,
RenderOnce, SharedString, Styled, StyledText, Subscription, Window,
};
use lsp_types::CompletionItem;
use lsp_types::{CompletionItem, CompletionTextEdit};
const MAX_MENU_WIDTH: Pixels = px(320.);
const MAX_MENU_HEIGHT: Pixels = px(480.);
@ -17,7 +17,7 @@ use crate::{
input::{
self,
popovers::{popover, render_markdown},
InputState,
InputState, RopeExt,
},
label::Label,
list::{List, ListDelegate, ListEvent},
@ -229,20 +229,38 @@ impl CompletionMenu {
}
fn select_item(&mut self, item: &CompletionItem, window: &mut Window, cx: &mut Context<Self>) {
let range = self.trigger_start_offset.unwrap_or(self.offset)..self.offset;
let insert_text = item
.insert_text
.as_deref()
.unwrap_or(&item.label)
.to_string();
let offset = self.offset;
let item = item.clone();
let mut range = self.trigger_start_offset.unwrap_or(self.offset)..self.offset;
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state.update_in(cx, |state, window, cx| {
state.completion_inserting = true;
let mut new_text = item.label.clone();
if let Some(text_edit) = item.text_edit.as_ref() {
match text_edit {
CompletionTextEdit::Edit(edit) => {
new_text = edit.new_text.clone();
range.start = state.text.position_to_offset(&edit.range.start);
range.end = state.text.position_to_offset(&edit.range.end);
}
CompletionTextEdit::InsertAndReplace(edit) => {
new_text = edit.new_text.clone();
range.start = state.text.position_to_offset(&edit.replace.start);
range.end = state.text.position_to_offset(&edit.replace.end);
}
}
} else if let Some(insert_text) = item.insert_text.clone() {
new_text = insert_text;
range = offset..offset;
}
state.replace_text_in_range(
Some(state.range_to_utf16(&range)),
&insert_text,
&new_text,
window,
cx,
);
@ -335,9 +353,17 @@ impl CompletionMenu {
self.offset = offset;
self.open = true;
self.list.update(cx, |this, cx| {
let longest_ix = items
.iter()
.enumerate()
.max_by_key(|(_, item)| item.label.len())
.map(|(ix, _)| ix)
.unwrap_or(0);
this.delegate_mut().query = self.query.clone();
this.delegate_mut().set_items(items);
this.set_selected_index(Some(IndexPath::new(0)), window, cx);
this.set_item_to_measure_index(IndexPath::new(longest_ix), window, cx);
});
cx.notify();

View file

@ -2193,6 +2193,27 @@ impl InputState {
last_bounds.origin + end_pos + point(px(0.), last_layout.line_height),
))
}
/// Replace text by [`lsp_types::Range`].
///
/// See also: [`EntityInputHandler::replace_text_in_range`]
#[allow(unused)]
pub(crate) fn replace_text_in_lsp_range(
&mut self,
lsp_range: &lsp_types::Range,
new_text: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
let start = self.text.position_to_offset(&lsp_range.start);
let end = self.text.position_to_offset(&lsp_range.end);
self.replace_text_in_range(
Some(self.range_to_utf16(&(start..end))),
new_text,
window,
cx,
);
}
}
impl EntityInputHandler for InputState {

View file

@ -143,6 +143,10 @@ impl RowsCache {
path
}
pub(crate) fn measured_size(&self) -> MeasuredEntrySize {
self.measured_size
}
pub(crate) fn prepare_if_needed<F>(
&mut self,
sections_count: usize,

View file

@ -60,6 +60,7 @@ pub struct List<D: ListDelegate> {
pub(crate) size: Size,
rows_cache: RowsCache,
selected_index: Option<IndexPath>,
item_to_measure_index: IndexPath,
deferred_scroll_to_index: Option<(IndexPath, ScrollStrategy)>,
mouse_right_clicked_index: Option<IndexPath>,
reset_on_cancel: bool,
@ -86,6 +87,7 @@ where
query_input: Some(query_input),
last_query: None,
selected_index: None,
item_to_measure_index: IndexPath::default(),
deferred_scroll_to_index: None,
mouse_right_clicked_index: None,
scroll_handle: VirtualListScrollHandle::new(),
@ -187,6 +189,17 @@ where
self.selected_index
}
/// Set a specific list item for measurement.
pub fn set_item_to_measure_index(
&mut self,
ix: IndexPath,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.item_to_measure_index = ix;
cx.notify();
}
fn render_scrollbar(&self, _: &mut Window, _: &mut Context<Self>) -> Option<impl IntoElement> {
if !self.scrollbar_visible {
return None;
@ -450,10 +463,13 @@ where
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let measured_size = self.rows_cache.measured_size();
v_flex()
.flex_grow()
.relative()
.h_full()
.min_w(measured_size.item_size.width)
.when_some(self.max_height, |this, h| this.max_h(h))
.overflow_hidden()
.when(items_count == 0, |this| {
@ -523,7 +539,7 @@ where
// Measure the item_height and section header/footer height.
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
measured_size.item_size = self
.render_list_item(IndexPath::default(), window, cx)
.render_list_item(self.item_to_measure_index, window, cx)
.into_any_element()
.layout_as_root(available_space, window, cx);