Apply additional edits when confirming a completion

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2022-02-01 17:38:11 +01:00
parent bcc57036a5
commit 6c7d2cf6b5
3 changed files with 145 additions and 43 deletions

View file

@ -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]

View file

@ -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()

View file

@ -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"))]