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,
|
v_flex, ActiveTheme, ContextModal, IconName, IndexPath, Selectable, Sizable,
|
||||||
};
|
};
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
CodeAction, CodeActionKind, CompletionContext, CompletionItem, CompletionResponse, TextEdit,
|
CodeAction, CodeActionKind, CompletionContext, CompletionItem, CompletionResponse,
|
||||||
WorkspaceEdit,
|
CompletionTextEdit, InsertReplaceEdit, TextEdit, WorkspaceEdit,
|
||||||
};
|
};
|
||||||
use story::Assets;
|
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 {
|
impl CompletionProvider for ExampleLspStore {
|
||||||
fn completions(
|
fn completions(
|
||||||
&self,
|
&self,
|
||||||
rope: &Rope,
|
rope: &Rope,
|
||||||
_offset: usize,
|
offset: usize,
|
||||||
trigger: CompletionContext,
|
trigger: CompletionContext,
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut Context<InputState>,
|
cx: &mut Context<InputState>,
|
||||||
|
|
@ -181,6 +201,7 @@ impl CompletionProvider for ExampleLspStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = rope.to_string(); // Just to use the rope parameter.
|
let _ = rope.to_string(); // Just to use the rope parameter.
|
||||||
|
let pos = rope.offset_to_position(offset);
|
||||||
|
|
||||||
// Simulate to delay for fetching completions
|
// Simulate to delay for fetching completions
|
||||||
let items = self.completions.clone();
|
let items = self.completions.clone();
|
||||||
|
|
@ -188,11 +209,41 @@ impl CompletionProvider for ExampleLspStore {
|
||||||
// Simulate a slow completion source, to test Editor async handling.
|
// Simulate a slow completion source, to test Editor async handling.
|
||||||
smol::Timer::after(Duration::from_millis(20)).await;
|
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
|
let items = items
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|item| item.label.starts_with(&trigger_character))
|
.filter(|item| item.label.starts_with(&trigger_character))
|
||||||
.take(10)
|
.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<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let responses = vec![CompletionResponse::Array(items)];
|
let responses = vec![CompletionResponse::Array(items)];
|
||||||
|
|
|
||||||
|
|
@ -176,35 +176,39 @@
|
||||||
"documentation": "Standard library for Rust."
|
"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```"
|
"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."
|
"documentation": "Format a string."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "println",
|
"label": "println!",
|
||||||
"documentation": "Print to the standard output.\n\n**Example:**\n```rust\nprintln!(\"Hello, world!\");\n```"
|
"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."
|
"documentation": "Print to the standard error."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "dbg",
|
"label": "dbg!",
|
||||||
"documentation": "Debug print a value."
|
"documentation": "Debug print a value."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "todo",
|
"label": "todo!",
|
||||||
"documentation": "Mark unfinished code."
|
"documentation": "Mark unfinished code."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "unimplemented",
|
"label": "unimplemented!",
|
||||||
"documentation": "Mark unimplemented code."
|
"documentation": "Mark unimplemented code."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "unreachable",
|
"label": "unreachable!",
|
||||||
"documentation": "Mark code that should never be executed."
|
"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,
|
HighlightStyle, InteractiveElement as _, IntoElement, ParentElement, Pixels, Point, Render,
|
||||||
RenderOnce, SharedString, Styled, StyledText, Subscription, Window,
|
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_WIDTH: Pixels = px(320.);
|
||||||
const MAX_MENU_HEIGHT: Pixels = px(480.);
|
const MAX_MENU_HEIGHT: Pixels = px(480.);
|
||||||
|
|
@ -17,7 +17,7 @@ use crate::{
|
||||||
input::{
|
input::{
|
||||||
self,
|
self,
|
||||||
popovers::{popover, render_markdown},
|
popovers::{popover, render_markdown},
|
||||||
InputState,
|
InputState, RopeExt,
|
||||||
},
|
},
|
||||||
label::Label,
|
label::Label,
|
||||||
list::{List, ListDelegate, ListEvent},
|
list::{List, ListDelegate, ListEvent},
|
||||||
|
|
@ -229,20 +229,38 @@ impl CompletionMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_item(&mut self, item: &CompletionItem, window: &mut Window, cx: &mut Context<Self>) {
|
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 offset = self.offset;
|
||||||
let insert_text = item
|
let item = item.clone();
|
||||||
.insert_text
|
let mut range = self.trigger_start_offset.unwrap_or(self.offset)..self.offset;
|
||||||
.as_deref()
|
|
||||||
.unwrap_or(&item.label)
|
|
||||||
.to_string();
|
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
|
|
||||||
cx.spawn_in(window, async move |_, cx| {
|
cx.spawn_in(window, async move |_, cx| {
|
||||||
state.update_in(cx, |state, window, cx| {
|
state.update_in(cx, |state, window, cx| {
|
||||||
state.completion_inserting = true;
|
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(
|
state.replace_text_in_range(
|
||||||
Some(state.range_to_utf16(&range)),
|
Some(state.range_to_utf16(&range)),
|
||||||
&insert_text,
|
&new_text,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
@ -335,9 +353,17 @@ impl CompletionMenu {
|
||||||
self.offset = offset;
|
self.offset = offset;
|
||||||
self.open = true;
|
self.open = true;
|
||||||
self.list.update(cx, |this, cx| {
|
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().query = self.query.clone();
|
||||||
this.delegate_mut().set_items(items);
|
this.delegate_mut().set_items(items);
|
||||||
this.set_selected_index(Some(IndexPath::new(0)), window, cx);
|
this.set_selected_index(Some(IndexPath::new(0)), window, cx);
|
||||||
|
this.set_item_to_measure_index(IndexPath::new(longest_ix), window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
|
||||||
|
|
@ -2193,6 +2193,27 @@ impl InputState {
|
||||||
last_bounds.origin + end_pos + point(px(0.), last_layout.line_height),
|
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 {
|
impl EntityInputHandler for InputState {
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,10 @@ impl RowsCache {
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn measured_size(&self) -> MeasuredEntrySize {
|
||||||
|
self.measured_size
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn prepare_if_needed<F>(
|
pub(crate) fn prepare_if_needed<F>(
|
||||||
&mut self,
|
&mut self,
|
||||||
sections_count: usize,
|
sections_count: usize,
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ pub struct List<D: ListDelegate> {
|
||||||
pub(crate) size: Size,
|
pub(crate) size: Size,
|
||||||
rows_cache: RowsCache,
|
rows_cache: RowsCache,
|
||||||
selected_index: Option<IndexPath>,
|
selected_index: Option<IndexPath>,
|
||||||
|
item_to_measure_index: IndexPath,
|
||||||
deferred_scroll_to_index: Option<(IndexPath, ScrollStrategy)>,
|
deferred_scroll_to_index: Option<(IndexPath, ScrollStrategy)>,
|
||||||
mouse_right_clicked_index: Option<IndexPath>,
|
mouse_right_clicked_index: Option<IndexPath>,
|
||||||
reset_on_cancel: bool,
|
reset_on_cancel: bool,
|
||||||
|
|
@ -86,6 +87,7 @@ where
|
||||||
query_input: Some(query_input),
|
query_input: Some(query_input),
|
||||||
last_query: None,
|
last_query: None,
|
||||||
selected_index: None,
|
selected_index: None,
|
||||||
|
item_to_measure_index: IndexPath::default(),
|
||||||
deferred_scroll_to_index: None,
|
deferred_scroll_to_index: None,
|
||||||
mouse_right_clicked_index: None,
|
mouse_right_clicked_index: None,
|
||||||
scroll_handle: VirtualListScrollHandle::new(),
|
scroll_handle: VirtualListScrollHandle::new(),
|
||||||
|
|
@ -187,6 +189,17 @@ where
|
||||||
self.selected_index
|
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> {
|
fn render_scrollbar(&self, _: &mut Window, _: &mut Context<Self>) -> Option<impl IntoElement> {
|
||||||
if !self.scrollbar_visible {
|
if !self.scrollbar_visible {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -450,10 +463,13 @@ where
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
|
let measured_size = self.rows_cache.measured_size();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.flex_grow()
|
.flex_grow()
|
||||||
.relative()
|
.relative()
|
||||||
.h_full()
|
.h_full()
|
||||||
|
.min_w(measured_size.item_size.width)
|
||||||
.when_some(self.max_height, |this, h| this.max_h(h))
|
.when_some(self.max_height, |this, h| this.max_h(h))
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.when(items_count == 0, |this| {
|
.when(items_count == 0, |this| {
|
||||||
|
|
@ -523,7 +539,7 @@ where
|
||||||
// Measure the item_height and section header/footer height.
|
// Measure the item_height and section header/footer height.
|
||||||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||||
measured_size.item_size = self
|
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()
|
.into_any_element()
|
||||||
.layout_as_root(available_space, window, cx);
|
.layout_as_root(available_space, window, cx);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue