Apply additional edits when confirming a completion
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
parent
bcc57036a5
commit
6c7d2cf6b5
3 changed files with 145 additions and 43 deletions
|
@ -8,6 +8,7 @@ mod multi_buffer;
|
||||||
mod test;
|
mod test;
|
||||||
|
|
||||||
use aho_corasick::AhoCorasick;
|
use aho_corasick::AhoCorasick;
|
||||||
|
use anyhow::Result;
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, HashMap, HashSet};
|
use collections::{BTreeMap, HashMap, HashSet};
|
||||||
pub use display_map::DisplayPoint;
|
pub use display_map::DisplayPoint;
|
||||||
|
@ -295,7 +296,9 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
|
||||||
cx.add_action(Editor::unfold);
|
cx.add_action(Editor::unfold);
|
||||||
cx.add_action(Editor::fold_selected_ranges);
|
cx.add_action(Editor::fold_selected_ranges);
|
||||||
cx.add_action(Editor::show_completions);
|
cx.add_action(Editor::show_completions);
|
||||||
cx.add_action(Editor::confirm_completion);
|
cx.add_action(|editor: &mut Editor, _: &ConfirmCompletion, cx| {
|
||||||
|
editor.confirm_completion(cx).detach_and_log_err(cx);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
trait SelectionExt {
|
trait SelectionExt {
|
||||||
|
@ -1645,21 +1648,20 @@ impl Editor {
|
||||||
self.completion_state.take()
|
self.completion_state.take()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm_completion(&mut self, _: &ConfirmCompletion, cx: &mut ViewContext<Self>) {
|
fn confirm_completion(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||||
if let Some(completion_state) = self.hide_completions(cx) {
|
if let Some(completion_state) = self.hide_completions(cx) {
|
||||||
if let Some(completion) = completion_state
|
if let Some(completion) = completion_state
|
||||||
.completions
|
.matches
|
||||||
.get(completion_state.selected_item)
|
.get(completion_state.selected_item)
|
||||||
|
.and_then(|mat| completion_state.completions.get(mat.candidate_id))
|
||||||
{
|
{
|
||||||
self.buffer.update(cx, |buffer, cx| {
|
return self.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.edit_with_autoindent(
|
buffer.apply_completion(completion.clone(), cx)
|
||||||
[completion.old_range.clone()],
|
});
|
||||||
completion.new_text.clone(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_completions(&self) -> bool {
|
pub fn has_completions(&self) -> bool {
|
||||||
|
@ -6654,9 +6656,9 @@ mod tests {
|
||||||
|
|
||||||
editor.next_notification(&cx).await;
|
editor.next_notification(&cx).await;
|
||||||
|
|
||||||
editor.update(&mut cx, |editor, cx| {
|
let apply_additional_edits = editor.update(&mut cx, |editor, cx| {
|
||||||
editor.move_down(&MoveDown, cx);
|
editor.move_down(&MoveDown, cx);
|
||||||
editor.confirm_completion(&ConfirmCompletion, cx);
|
let apply_additional_edits = editor.confirm_completion(cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.text(cx),
|
editor.text(cx),
|
||||||
"
|
"
|
||||||
|
@ -6666,7 +6668,34 @@ mod tests {
|
||||||
"
|
"
|
||||||
.unindent()
|
.unindent()
|
||||||
);
|
);
|
||||||
|
apply_additional_edits
|
||||||
});
|
});
|
||||||
|
let (id, _) = fake
|
||||||
|
.receive_request::<lsp::request::ResolveCompletionItem>()
|
||||||
|
.await;
|
||||||
|
fake.respond(
|
||||||
|
id,
|
||||||
|
lsp::CompletionItem {
|
||||||
|
additional_text_edits: Some(vec![lsp::TextEdit::new(
|
||||||
|
lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 5)),
|
||||||
|
"\nadditional edit".to_string(),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
apply_additional_edits.await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
editor.read_with(&cx, |editor, cx| editor.text(cx)),
|
||||||
|
"
|
||||||
|
one.second_completion
|
||||||
|
two
|
||||||
|
three
|
||||||
|
additional edit
|
||||||
|
"
|
||||||
|
.unindent()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
mod anchor;
|
mod anchor;
|
||||||
|
|
||||||
pub use anchor::{Anchor, AnchorRangeExt};
|
pub use anchor::{Anchor, AnchorRangeExt};
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
||||||
|
@ -929,6 +929,34 @@ impl MultiBuffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn apply_completion(
|
||||||
|
&self,
|
||||||
|
completion: Completion<Anchor>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let buffer = if let Some(buffer) = self
|
||||||
|
.buffers
|
||||||
|
.borrow()
|
||||||
|
.get(&completion.old_range.start.buffer_id)
|
||||||
|
{
|
||||||
|
buffer.buffer.clone()
|
||||||
|
} else {
|
||||||
|
return Task::ready(Err(anyhow!("completion cannot be applied to any buffer")));
|
||||||
|
};
|
||||||
|
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.apply_completion(
|
||||||
|
Completion {
|
||||||
|
old_range: completion.old_range.start.text_anchor
|
||||||
|
..completion.old_range.end.text_anchor,
|
||||||
|
new_text: completion.new_text,
|
||||||
|
lsp_completion: completion.lsp_completion,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
|
pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
|
||||||
self.buffers
|
self.buffers
|
||||||
.borrow()
|
.borrow()
|
||||||
|
|
|
@ -114,7 +114,7 @@ pub struct Diagnostic {
|
||||||
pub is_disk_based: bool,
|
pub is_disk_based: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Completion<T> {
|
pub struct Completion<T> {
|
||||||
pub old_range: Range<T>,
|
pub old_range: Range<T>,
|
||||||
pub new_text: String,
|
pub new_text: String,
|
||||||
|
@ -165,6 +165,10 @@ pub enum Event {
|
||||||
pub trait File {
|
pub trait File {
|
||||||
fn as_local(&self) -> Option<&dyn LocalFile>;
|
fn as_local(&self) -> Option<&dyn LocalFile>;
|
||||||
|
|
||||||
|
fn is_local(&self) -> bool {
|
||||||
|
self.as_local().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
fn mtime(&self) -> SystemTime;
|
fn mtime(&self) -> SystemTime;
|
||||||
|
|
||||||
/// Returns the path of this file relative to the worktree's root directory.
|
/// Returns the path of this file relative to the worktree's root directory.
|
||||||
|
@ -567,21 +571,7 @@ impl Buffer {
|
||||||
if let Some(edits) = edits {
|
if let Some(edits) = edits {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
if this.version == version {
|
if this.version == version {
|
||||||
for edit in &edits {
|
this.apply_lsp_edits(edits, cx)
|
||||||
let range = range_from_lsp(edit.range);
|
|
||||||
if this.clip_point_utf16(range.start, Bias::Left) != range.start
|
|
||||||
|| this.clip_point_utf16(range.end, Bias::Left) != range.end
|
|
||||||
{
|
|
||||||
return Err(anyhow!(
|
|
||||||
"invalid formatting edits received from language server"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for edit in edits.into_iter().rev() {
|
|
||||||
this.edit([range_from_lsp(edit.range)], edit.new_text, cx);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("buffer edited since starting to format"))
|
Err(anyhow!("buffer edited since starting to format"))
|
||||||
}
|
}
|
||||||
|
@ -1390,13 +1380,6 @@ impl Buffer {
|
||||||
self.edit_internal(ranges_iter, new_text, true, cx)
|
self.edit_internal(ranges_iter, new_text, true, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
impl Buffer
|
|
||||||
pub fn edit
|
|
||||||
pub fn edit_internal
|
|
||||||
pub fn edit_with_autoindent
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub fn edit_internal<I, S, T>(
|
pub fn edit_internal<I, S, T>(
|
||||||
&mut self,
|
&mut self,
|
||||||
ranges_iter: I,
|
ranges_iter: I,
|
||||||
|
@ -1485,6 +1468,29 @@ impl Buffer {
|
||||||
self.send_operation(Operation::Buffer(text::Operation::Edit(edit)), cx);
|
self.send_operation(Operation::Buffer(text::Operation::Edit(edit)), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_lsp_edits(
|
||||||
|
&mut self,
|
||||||
|
edits: Vec<lsp::TextEdit>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Result<()> {
|
||||||
|
for edit in &edits {
|
||||||
|
let range = range_from_lsp(edit.range);
|
||||||
|
if self.clip_point_utf16(range.start, Bias::Left) != range.start
|
||||||
|
|| self.clip_point_utf16(range.end, Bias::Left) != range.end
|
||||||
|
{
|
||||||
|
return Err(anyhow!(
|
||||||
|
"invalid formatting edits received from language server"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for edit in edits.into_iter().rev() {
|
||||||
|
self.edit([range_from_lsp(edit.range)], edit.new_text, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn did_edit(
|
fn did_edit(
|
||||||
&mut self,
|
&mut self,
|
||||||
old_version: &clock::Global,
|
old_version: &clock::Global,
|
||||||
|
@ -1752,13 +1758,17 @@ impl Buffer {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let old_range = this.anchor_before(old_range.start)..this.anchor_after(old_range.end);
|
let clipped_start = this.clip_point_utf16(old_range.start, Bias::Left);
|
||||||
|
let clipped_end = this.clip_point_utf16(old_range.end, Bias::Left) ;
|
||||||
Some(Completion {
|
if clipped_start == old_range.start && clipped_end == old_range.end {
|
||||||
old_range,
|
Some(Completion {
|
||||||
new_text,
|
old_range: this.anchor_before(old_range.start)..this.anchor_after(old_range.end),
|
||||||
lsp_completion,
|
new_text,
|
||||||
})
|
lsp_completion,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}).collect())
|
}).collect())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1766,6 +1776,41 @@ impl Buffer {
|
||||||
Task::ready(Ok(Default::default()))
|
Task::ready(Ok(Default::default()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn apply_completion(
|
||||||
|
&mut self,
|
||||||
|
completion: Completion<Anchor>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
self.edit_with_autoindent([completion.old_range], completion.new_text.clone(), cx);
|
||||||
|
|
||||||
|
let file = if let Some(file) = self.file.as_ref() {
|
||||||
|
file
|
||||||
|
} else {
|
||||||
|
return Task::ready(Ok(Default::default()));
|
||||||
|
};
|
||||||
|
if file.is_local() {
|
||||||
|
let server = if let Some(lang) = self.language_server.as_ref() {
|
||||||
|
lang.server.clone()
|
||||||
|
} else {
|
||||||
|
return Task::ready(Ok(Default::default()));
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let resolved_completion = server
|
||||||
|
.request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion)
|
||||||
|
.await?;
|
||||||
|
if let Some(additional_edits) = resolved_completion.additional_text_edits {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.apply_lsp_edits(additional_edits, cx)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok::<_, anyhow::Error>(())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return Task::ready(Ok(Default::default()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue