editor: Update completions to support CompletionTextEdit. (#1267)
- Fix completion menu width to longest item.
This commit is contained in:
parent
13fa76e00d
commit
692057f799
6 changed files with 144 additions and 22 deletions
|
|
@ -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)];
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue