diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 065bc2d74a..da6ea878ac 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -383,6 +383,8 @@ pub enum SoftWrap { Column(u32), } +type CompletionId = usize; + pub type BuildSettings = Arc EditorSettings>; pub struct Editor { @@ -416,7 +418,8 @@ pub struct Editor { highlighted_ranges: BTreeMap>)>, nav_history: Option, completion_state: Option, - completions_task: Option>>, + completion_tasks: Vec<(CompletionId, Task>)>, + next_completion_id: CompletionId, } pub struct EditorSnapshot { @@ -457,6 +460,7 @@ struct SnippetState { struct InvalidationStack(Vec); struct CompletionState { + id: CompletionId, initial_position: Anchor, completions: Arc<[Completion]>, match_candidates: Vec, @@ -617,7 +621,8 @@ impl Editor { highlighted_ranges: Default::default(), nav_history: None, completion_state: None, - completions_task: None, + completion_tasks: Default::default(), + next_completion_id: 0, }; let selection = Selection { id: post_inc(&mut this.next_selection_id), @@ -1660,11 +1665,13 @@ impl Editor { .buffer .update(cx, |buffer, cx| buffer.completions(position.clone(), cx)); - self.completions_task = Some(cx.spawn_weak(|this, mut cx| { + let id = post_inc(&mut self.next_completion_id); + let task = cx.spawn_weak(|this, mut cx| { async move { let completions = completions.await?; let mut completion_state = CompletionState { + id, initial_position: position, match_candidates: completions .iter() @@ -1688,6 +1695,14 @@ impl Editor { if let Some(this) = cx.read(|cx| this.upgrade(cx)) { this.update(&mut cx, |this, cx| { + if let Some(prev_completion_state) = this.completion_state.as_ref() { + if prev_completion_state.id > completion_state.id { + return; + } + } + + this.completion_tasks + .retain(|(id, _)| *id > completion_state.id); if completion_state.matches.is_empty() { this.hide_completions(cx); } else if this.focused { @@ -1700,12 +1715,13 @@ impl Editor { Ok::<_, anyhow::Error>(()) } .log_err() - })); + }); + self.completion_tasks.push((id, task)); } fn hide_completions(&mut self, cx: &mut ViewContext) -> Option { cx.notify(); - self.completions_task.take(); + self.completion_tasks.clear(); self.completion_state.take() } @@ -1731,31 +1747,39 @@ impl Editor { }; let snapshot = self.buffer.read(cx).snapshot(cx); let old_range = completion.old_range.to_offset(&snapshot); + let old_text = snapshot + .text_for_range(old_range.clone()) + .collect::(); let selections = self.local_selections::(cx); - let mut common_prefix_len = None; + let newest_selection = selections.iter().max_by_key(|s| s.id)?; + let lookbehind = newest_selection.start.saturating_sub(old_range.start); + let lookahead = old_range.end.saturating_sub(newest_selection.end); + let mut common_prefix_len = old_text + .bytes() + .zip(text.bytes()) + .take_while(|(a, b)| a == b) + .count(); + let mut ranges = Vec::new(); for selection in &selections { - let start = selection.start.saturating_sub(old_range.len()); - let prefix_len = snapshot - .bytes_at(start) - .zip(completion.new_text.bytes()) - .take_while(|(a, b)| a == b) - .count(); - if common_prefix_len.is_none() { - common_prefix_len = Some(prefix_len); - } - - if common_prefix_len == Some(prefix_len) { - ranges.push(start + prefix_len..selection.end); + if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) { + let start = selection.start.saturating_sub(lookbehind); + let end = selection.end + lookahead; + ranges.push(start + common_prefix_len..end); } else { - common_prefix_len.take(); + common_prefix_len = 0; ranges.clear(); - ranges.extend(selections.iter().map(|s| s.start..s.end)); + ranges.extend(selections.iter().map(|s| { + if s.id == newest_selection.id { + old_range.clone() + } else { + s.start..s.end + } + })); break; } } - let common_prefix_len = common_prefix_len.unwrap_or(0); let text = &text[common_prefix_len..]; self.start_transaction(cx); @@ -4024,7 +4048,8 @@ impl Editor { if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) { let query = Self::completion_query(&buffer, cursor_position); - smol::block_on(completion_state.filter(query.as_deref(), cx.background().clone())); + cx.background() + .block(completion_state.filter(query.as_deref(), cx.background().clone())); self.show_completions(&ShowCompletions, cx); } else { self.hide_completions(cx); @@ -4954,6 +4979,7 @@ fn styled_runs_for_completion_label<'a>( mod tests { use super::*; use language::{FakeFile, LanguageConfig}; + use lsp::FakeLanguageServer; use std::{cell::RefCell, path::Path, rc::Rc, time::Instant}; use text::Point; use unindent::Unindent; @@ -7269,41 +7295,20 @@ mod tests { let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); editor.update(&mut cx, |editor, cx| { - editor.select_ranges([3..3], None, cx); + editor.select_ranges([Point::new(0, 3)..Point::new(0, 3)], None, cx); editor.handle_input(&Input(".".to_string()), cx); }); - let (id, params) = fake.receive_request::().await; - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Url::from_file_path("/the/file").unwrap() - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(0, 4) - ); - - fake.respond( - id, - Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)), - new_text: "first_completion".to_string(), - })), - ..Default::default() - }, - lsp::CompletionItem { - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)), - new_text: "second_completion".to_string(), - })), - ..Default::default() - }, - ])), + handle_completion_request( + &mut fake, + "/the/file", + Point::new(0, 4), + &[ + (Point::new(0, 4)..Point::new(0, 4), "first_completion"), + (Point::new(0, 4)..Point::new(0, 4), "second_completion"), + ], ) .await; - editor.next_notification(&cx).await; let apply_additional_edits = editor.update(&mut cx, |editor, cx| { @@ -7321,21 +7326,11 @@ mod tests { apply_additional_edits }); - let (id, _) = fake - .receive_request::() - .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() - }, + handle_resolve_completion_request( + &mut fake, + Some((Point::new(2, 5)..Point::new(2, 5), "\nadditional edit")), ) .await; - apply_additional_edits.await.unwrap(); assert_eq!( editor.read_with(&cx, |editor, cx| editor.text(cx)), @@ -7347,6 +7342,130 @@ mod tests { " .unindent() ); + + editor.update(&mut cx, |editor, cx| { + editor.select_ranges( + [ + Point::new(1, 3)..Point::new(1, 3), + Point::new(2, 5)..Point::new(2, 5), + ], + None, + cx, + ); + + editor.handle_input(&Input(" ".to_string()), cx); + assert!(editor.completion_state.is_none()); + editor.handle_input(&Input("s".to_string()), cx); + assert!(editor.completion_state.is_none()); + }); + + handle_completion_request( + &mut fake, + "/the/file", + Point::new(2, 7), + &[ + (Point::new(2, 6)..Point::new(2, 7), "fourth_completion"), + (Point::new(2, 6)..Point::new(2, 7), "fifth_completion"), + (Point::new(2, 6)..Point::new(2, 7), "sixth_completion"), + ], + ) + .await; + editor + .condition(&cx, |editor, _| editor.completion_state.is_some()) + .await; + + editor.update(&mut cx, |editor, cx| { + editor.handle_input(&Input("i".to_string()), cx); + }); + + handle_completion_request( + &mut fake, + "/the/file", + Point::new(2, 8), + &[ + (Point::new(2, 6)..Point::new(2, 8), "fourth_completion"), + (Point::new(2, 6)..Point::new(2, 8), "fifth_completion"), + (Point::new(2, 6)..Point::new(2, 8), "sixth_completion"), + ], + ) + .await; + editor.next_notification(&cx).await; + + let apply_additional_edits = editor.update(&mut cx, |editor, cx| { + let apply_additional_edits = editor.confirm_completion(None, cx).unwrap(); + assert_eq!( + editor.text(cx), + " + one.second_completion + two sixth_completion + three sixth_completion + additional edit + " + .unindent() + ); + apply_additional_edits + }); + handle_resolve_completion_request(&mut fake, None).await; + apply_additional_edits.await.unwrap(); + + async fn handle_completion_request( + fake: &mut FakeLanguageServer, + path: &str, + position: Point, + completions: &[(Range, &str)], + ) { + let (id, params) = fake.receive_request::().await; + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path(path).unwrap() + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(position.row, position.column) + ); + + let completions = completions + .iter() + .map(|(range, new_text)| lsp::CompletionItem { + label: new_text.to_string(), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(range.start.row, range.start.column), + lsp::Position::new(range.start.row, range.start.column), + ), + new_text: new_text.to_string(), + })), + ..Default::default() + }) + .collect(); + fake.respond(id, Some(lsp::CompletionResponse::Array(completions))) + .await; + } + + async fn handle_resolve_completion_request( + fake: &mut FakeLanguageServer, + edit: Option<(Range, &str)>, + ) { + let (id, _) = fake + .receive_request::() + .await; + fake.respond( + id, + lsp::CompletionItem { + additional_text_edits: edit.map(|(range, new_text)| { + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(range.start.row, range.start.column), + lsp::Position::new(range.end.row, range.end.column), + ), + new_text.to_string(), + )] + }), + ..Default::default() + }, + ) + .await; + } } #[gpui::test] diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 0554e1b8b5..3870323048 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1314,12 +1314,6 @@ impl MultiBufferSnapshot { } } - pub fn bytes_at<'a, T: ToOffset>(&'a self, position: T) -> impl 'a + Iterator { - self.bytes_in_range(position.to_offset(self)..self.len()) - .flatten() - .copied() - } - pub fn buffer_rows<'a>(&'a self, start_row: u32) -> MultiBufferRows<'a> { let mut result = MultiBufferRows { buffer_row_range: 0..0, diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 5e37acbc1d..8021391978 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -236,16 +236,14 @@ impl Deterministic { } } - fn block_on(&self, future: &mut AnyLocalFuture) -> Option> { + fn block(&self, future: &mut F, max_ticks: usize) -> Option + where + F: Unpin + Future, + { let unparker = self.parker.lock().unparker(); let waker = waker_fn(move || { unparker.unpark(); }); - let max_ticks = { - let mut state = self.state.lock(); - let range = state.block_on_ticks.clone(); - state.rng.gen_range(range) - }; let mut cx = Context::from_waker(&waker); for _ in 0..max_ticks { @@ -258,7 +256,7 @@ impl Deterministic { runnable.run(); } else { drop(state); - if let Poll::Ready(result) = future.as_mut().poll(&mut cx) { + if let Poll::Ready(result) = future.poll(&mut cx) { return Some(result); } let mut state = self.state.lock(); @@ -488,6 +486,19 @@ impl Background { Task::send(any_task) } + pub fn block(&self, future: F) -> T + where + F: Future, + { + smol::pin!(future); + match self { + Self::Production { .. } => smol::block_on(&mut future), + Self::Deterministic { executor, .. } => { + executor.block(&mut future, usize::MAX).unwrap() + } + } + } + pub fn block_with_timeout( &self, timeout: Duration, @@ -501,7 +512,14 @@ impl Background { if !timeout.is_zero() { let output = match self { Self::Production { .. } => smol::block_on(util::timeout(timeout, &mut future)).ok(), - Self::Deterministic { executor, .. } => executor.block_on(&mut future), + Self::Deterministic { executor, .. } => { + let max_ticks = { + let mut state = executor.state.lock(); + let range = state.block_on_ticks.clone(); + state.rng.gen_range(range) + }; + executor.block(&mut future, max_ticks) + } }; if let Some(output) = output { return Ok(*output.downcast().unwrap());