From 93bcde953c13add97d83971f5a65d27f5309b754 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 8 Feb 2022 14:24:45 -0800 Subject: [PATCH] Consolidate logic for completion alongside code actions in Project This way, completions are dealt with more consistently with code actions, and the logic is not spread across so many places. The `language::File` trait and the multibuffer no longer need to deal with completions. Completions are no longer generic over an anchor type. --- crates/diagnostics/src/diagnostics.rs | 7 +- crates/editor/src/editor.rs | 141 ++++++++++------- crates/editor/src/element.rs | 1 + crates/editor/src/items.rs | 1 + crates/editor/src/multi_buffer.rs | 69 --------- crates/find/src/find.rs | 2 +- crates/language/src/buffer.rs | 193 +---------------------- crates/language/src/proto.rs | 4 +- crates/project/src/project.rs | 213 ++++++++++++++++++++++++-- crates/project/src/worktree.rs | 72 +-------- crates/server/src/rpc.rs | 3 +- script/drop-test-dbs | 16 ++ 12 files changed, 322 insertions(+), 400 deletions(-) create mode 100755 script/drop-test-dbs diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index ac197a7456eb6ae94f27cbcab1ea9847a14de547..b12d77426bf8b9d1fe7cbbd1203f781d96e0e501 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -145,7 +145,12 @@ impl ProjectDiagnosticsEditor { let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id())); let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone()); let editor = cx.add_view(|cx| { - let mut editor = Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx); + let mut editor = Editor::for_buffer( + excerpts.clone(), + build_settings.clone(), + Some(workspace.clone()), + cx, + ); editor.set_vertical_scroll_margin(5, cx); editor }); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ae9381d2f0884e1717dde08dff8d408f29719870..d8117fb494dd5fcc9d447ae7198f5f8d415f0754 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -415,6 +415,7 @@ pub struct Editor { scroll_top_anchor: Option, autoscroll_request: Option, build_settings: BuildSettings, + workspace: Option>, focused: bool, show_local_cursors: bool, blink_epoch: usize, @@ -515,7 +516,8 @@ impl ContextMenu { struct CompletionsMenu { id: CompletionId, initial_position: Anchor, - completions: Arc<[Completion]>, + buffer: ModelHandle, + completions: Arc<[Completion]>, match_candidates: Vec, matches: Arc<[StringMatch]>, selected_item: usize, @@ -750,7 +752,7 @@ impl Editor { pub fn single_line(build_settings: BuildSettings, cx: &mut ViewContext) -> Self { let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let mut view = Self::for_buffer(buffer, build_settings, cx); + let mut view = Self::for_buffer(buffer, build_settings, None, cx); view.mode = EditorMode::SingleLine; view } @@ -762,7 +764,7 @@ impl Editor { ) -> Self { let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let mut view = Self::for_buffer(buffer, build_settings, cx); + let mut view = Self::for_buffer(buffer, build_settings, None, cx); view.mode = EditorMode::AutoHeight { max_lines }; view } @@ -770,13 +772,19 @@ impl Editor { pub fn for_buffer( buffer: ModelHandle, build_settings: BuildSettings, + workspace: Option>, cx: &mut ViewContext, ) -> Self { - Self::new(buffer, build_settings, cx) + Self::new(buffer, build_settings, workspace, cx) } pub fn clone(&self, cx: &mut ViewContext) -> Self { - let mut clone = Self::new(self.buffer.clone(), self.build_settings.clone(), cx); + let mut clone = Self::new( + self.buffer.clone(), + self.build_settings.clone(), + self.workspace.clone(), + cx, + ); clone.scroll_position = self.scroll_position; clone.scroll_top_anchor = self.scroll_top_anchor.clone(); clone.nav_history = self @@ -789,6 +797,7 @@ impl Editor { pub fn new( buffer: ModelHandle, build_settings: BuildSettings, + workspace: Option>, cx: &mut ViewContext, ) -> Self { let settings = build_settings(cx); @@ -823,6 +832,7 @@ impl Editor { select_larger_syntax_node_stack: Vec::new(), active_diagnostics: None, build_settings, + workspace, scroll_position: Vector2F::zero(), scroll_top_anchor: None, autoscroll_request: None, @@ -1872,16 +1882,26 @@ impl Editor { } fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { + let project = if let Some(workspace) = self.workspace.as_ref().and_then(|w| w.upgrade(cx)) { + workspace.read(cx).project().clone() + } else { + return; + }; + let position = if let Some(selection) = self.newest_anchor_selection() { selection.head() } else { return; }; + let (buffer, buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(position.clone(), cx); let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone()); - let completions = self - .buffer - .update(cx, |buffer, cx| buffer.completions(position.clone(), cx)); + let completions = project.update(cx, |project, cx| { + project.completions(&buffer, buffer_position.clone(), cx) + }); let id = post_inc(&mut self.next_completion_id); let task = cx.spawn_weak(|this, mut cx| { @@ -1904,6 +1924,7 @@ impl Editor { ) }) .collect(), + buffer, completions: completions.into(), matches: Vec::new().into(), selected_item: 0, @@ -1953,6 +1974,7 @@ impl Editor { let mat = completions_menu .matches .get(completion_ix.unwrap_or(completions_menu.selected_item))?; + let buffer_handle = completions_menu.buffer; let completion = completions_menu.completions.get(mat.candidate_id)?; let snippet; @@ -1964,11 +1986,9 @@ impl Editor { snippet = None; text = completion.new_text.clone(); }; - 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 buffer = buffer_handle.read(cx); + let old_range = completion.old_range.to_offset(&buffer); + let old_text = buffer.text_for_range(old_range.clone()).collect::(); let selections = self.local_selections::(cx); let newest_selection = selections.iter().max_by_key(|s| s.id)?; @@ -1982,7 +2002,7 @@ impl Editor { let mut ranges = Vec::new(); for selection in &selections { - if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) { + if buffer.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); @@ -2017,41 +2037,51 @@ impl Editor { } self.end_transaction(cx); - Some(self.buffer.update(cx, |buffer, cx| { - buffer.apply_additional_edits_for_completion(completion.clone(), cx) + let project = self + .workspace + .as_ref()? + .upgrade(cx)? + .read(cx) + .project() + .clone(); + let apply_edits = project.update(cx, |project, cx| { + project.apply_additional_edits_for_completion( + buffer_handle, + completion.clone(), + true, + cx, + ) + }); + Some(cx.foreground().spawn(async move { + apply_edits.await?; + Ok(()) })) } - fn show_code_actions( - workspace: &mut Workspace, - _: &ShowCodeActions, - cx: &mut ViewContext, - ) { - let active_item = workspace.active_item(cx); - let editor_handle = if let Some(editor) = active_item - .as_ref() - .and_then(|item| item.act_as::(cx)) - { - editor + fn show_code_actions(&mut self, _: &ShowCodeActions, cx: &mut ViewContext) { + let head = if let Some(selection) = self.newest_anchor_selection() { + selection.head() } else { return; }; - - let editor = editor_handle.read(cx); - let head = if let Some(selection) = editor.newest_anchor_selection() { - selection.head() + let workspace = if let Some(workspace) = self.workspace.as_ref().and_then(|w| w.upgrade(cx)) + { + workspace } else { return; }; - let (buffer, head) = editor.buffer.read(cx).text_anchor_for_position(head, cx); + + let (buffer, head) = self.buffer.read(cx).text_anchor_for_position(head, cx); let actions = workspace + .read(cx) .project() + .clone() .update(cx, |project, cx| project.code_actions(&buffer, head, cx)); - cx.spawn(|_, mut cx| async move { + cx.spawn(|this, mut cx| async move { let actions = actions.await?; if !actions.is_empty() { - editor_handle.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, cx| { if this.focused { this.show_context_menu( ContextMenu::CodeActions(CodeActionsMenu { @@ -2071,29 +2101,31 @@ impl Editor { } fn confirm_code_action( - workspace: &mut Workspace, + &mut self, ConfirmCodeAction(action_ix): &ConfirmCodeAction, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Option>> { - let active_item = workspace.active_item(cx)?; - let editor = active_item.act_as::(cx)?; - let (buffer, action) = editor.update(cx, |editor, cx| { - let actions_menu = - if let ContextMenu::CodeActions(menu) = editor.hide_context_menu(cx)? { - menu - } else { - return None; - }; - let action_ix = action_ix.unwrap_or(actions_menu.selected_item); - let action = actions_menu.actions.get(action_ix)?.clone(); - Some((actions_menu.buffer, action)) - })?; + let workspace = self.workspace.as_ref()?.upgrade(cx)?; - let apply_code_actions = workspace.project().update(cx, |project, cx| { - project.apply_code_action(buffer, action, true, cx) - }); - Some(cx.spawn(|workspace, mut cx| async move { + let actions_menu = if let ContextMenu::CodeActions(menu) = self.hide_context_menu(cx)? { + menu + } else { + return None; + }; + let action_ix = action_ix.unwrap_or(actions_menu.selected_item); + let action = actions_menu.actions.get(action_ix)?.clone(); + let buffer = actions_menu.buffer; + + let apply_code_actions = workspace + .read(cx) + .project() + .clone() + .update(cx, |project, cx| { + project.apply_code_action(buffer, action, true, cx) + }); + Some(cx.spawn(|_, mut cx| async move { let project_transaction = apply_code_actions.await?; + // TODO: replace this with opening a single tab that is a multibuffer workspace.update(&mut cx, |workspace, cx| { for (buffer, _) in project_transaction.0 { @@ -7527,6 +7559,7 @@ mod tests { three " .unindent(); + let buffer = cx.add_model(|cx| { Buffer::from_file( 0, @@ -8217,7 +8250,7 @@ mod tests { settings: EditorSettings, cx: &mut ViewContext, ) -> Editor { - Editor::for_buffer(buffer, Arc::new(move |_| settings.clone()), cx) + Editor::for_buffer(buffer, Arc::new(move |_| settings.clone()), None, cx) } } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 37b53b01413628ce24a20132048cd96f56452ef0..1f347b09a5d23f9c311bc2e02592d4bc8257bc7a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1298,6 +1298,7 @@ mod tests { let settings = settings.clone(); Arc::new(move |_| settings.clone()) }, + None, cx, ) }); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 97ce05615246cdd236e4a32cf580296bd142d119..d9c7e180f5a6e8de986ffdf16b3b6e33bef132a0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -55,6 +55,7 @@ impl ItemHandle for BufferItemHandle { let mut editor = Editor::for_buffer( buffer, crate::settings_builder(weak_buffer, workspace.settings()), + Some(workspace.weak_handle()), cx, ); editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle())); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 9a33de3a0ffaffbf98ea37f4f57a0ebd11411406..2dd84d2c998041e49e00517bbac011ac7927c2b2 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -861,41 +861,6 @@ impl MultiBuffer { }) } - pub fn completions( - &self, - position: T, - cx: &mut ModelContext, - ) -> Task>>> - where - T: ToOffset, - { - let anchor = self.read(cx).anchor_before(position); - let buffer = self.buffers.borrow()[&anchor.buffer_id].buffer.clone(); - let completions = - buffer.update(cx, |buffer, cx| buffer.completions(anchor.text_anchor, cx)); - cx.spawn(|this, cx| async move { - completions.await.map(|completions| { - let snapshot = this.read_with(&cx, |buffer, cx| buffer.snapshot(cx)); - completions - .into_iter() - .map(|completion| Completion { - old_range: snapshot.anchor_in_excerpt( - anchor.excerpt_id.clone(), - completion.old_range.start, - ) - ..snapshot.anchor_in_excerpt( - anchor.excerpt_id.clone(), - completion.old_range.end, - ), - new_text: completion.new_text, - label: completion.label, - lsp_completion: completion.lsp_completion, - }) - .collect() - }) - }) - } - pub fn is_completion_trigger(&self, position: T, text: &str, cx: &AppContext) -> bool where T: ToOffset, @@ -924,40 +889,6 @@ impl MultiBuffer { .any(|string| string == text) } - pub fn apply_additional_edits_for_completion( - &self, - completion: Completion, - cx: &mut ModelContext, - ) -> Task> { - let buffer = if let Some(buffer_state) = self - .buffers - .borrow() - .get(&completion.old_range.start.buffer_id) - { - buffer_state.buffer.clone() - } else { - return Task::ready(Ok(())); - }; - - let apply_edits = buffer.update(cx, |buffer, cx| { - buffer.apply_additional_edits_for_completion( - Completion { - old_range: completion.old_range.start.text_anchor - ..completion.old_range.end.text_anchor, - new_text: completion.new_text, - label: completion.label, - lsp_completion: completion.lsp_completion, - }, - true, - cx, - ) - }); - cx.foreground().spawn(async move { - apply_edits.await?; - Ok(()) - }) - } - pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc> { self.buffers .borrow() diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 8844f2cf907f79e7dafb149f92eb5e6376041c39..cb50c80123e6f1cb39df2fe6afb9c0afbbc40ec1 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -655,7 +655,7 @@ mod tests { ) }); let editor = cx.add_view(Default::default(), |cx| { - Editor::new(buffer.clone(), Arc::new(EditorSettings::test), cx) + Editor::new(buffer.clone(), Arc::new(EditorSettings::test), None, cx) }); let find_bar = cx.add_view(Default::default(), |cx| { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 2b5c979455d23c98695270a8f028e99c0417bfa5..7b903c67b0d73b3e555dc7755e6c2e95ca8dd5bd 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -38,7 +38,7 @@ use text::{operation_queue::OperationQueue, rope::TextDimension}; pub use text::{Buffer as TextBuffer, Operation as _, *}; use theme::SyntaxTheme; use tree_sitter::{InputEdit, QueryCursor, Tree}; -use util::{post_inc, ResultExt, TryFutureExt as _}; +use util::{post_inc, TryFutureExt as _}; #[cfg(any(test, feature = "test-support"))] pub use tree_sitter_rust; @@ -111,8 +111,8 @@ pub struct Diagnostic { } #[derive(Clone, Debug)] -pub struct Completion { - pub old_range: Range, +pub struct Completion { + pub old_range: Range, pub new_text: String, pub label: CompletionLabel, pub lsp_completion: lsp::CompletionItem, @@ -201,21 +201,6 @@ pub trait File { fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext) -> Option>>; - fn completions( - &self, - buffer_id: u64, - position: Anchor, - language: Option>, - cx: &mut MutableAppContext, - ) -> Task>>>; - - fn apply_additional_edits_for_completion( - &self, - buffer_id: u64, - completion: Completion, - cx: &mut MutableAppContext, - ) -> Task>>; - fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext); fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext); @@ -285,25 +270,6 @@ impl File for FakeFile { None } - fn completions( - &self, - _: u64, - _: Anchor, - _: Option>, - _: &mut MutableAppContext, - ) -> Task>>> { - Task::ready(Ok(Default::default())) - } - - fn apply_additional_edits_for_completion( - &self, - _: u64, - _: Completion, - _: &mut MutableAppContext, - ) -> Task>> { - Task::ready(Ok(Default::default())) - } - fn buffer_updated(&self, _: u64, _: Operation, _: &mut MutableAppContext) {} fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {} @@ -1762,157 +1728,6 @@ impl Buffer { } } - pub fn completions( - &self, - position: T, - cx: &mut ModelContext, - ) -> Task>>> - where - T: ToOffset, - { - let file = if let Some(file) = self.file.as_ref() { - file - } else { - return Task::ready(Ok(Default::default())); - }; - let language = self.language.clone(); - - if let Some(file) = file.as_local() { - let server = if let Some(language_server) = self.language_server.as_ref() { - language_server.server.clone() - } else { - return Task::ready(Ok(Default::default())); - }; - let abs_path = file.abs_path(cx); - let position = self.offset_to_point_utf16(position.to_offset(self)); - - cx.spawn(|this, cx| async move { - let completions = server - .request::(lsp::CompletionParams { - text_document_position: lsp::TextDocumentPositionParams::new( - lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path(abs_path).unwrap(), - ), - position.to_lsp_position(), - ), - context: Default::default(), - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - .await?; - - let completions = if let Some(completions) = completions { - match completions { - lsp::CompletionResponse::Array(completions) => completions, - lsp::CompletionResponse::List(list) => list.items, - } - } else { - Default::default() - }; - - this.read_with(&cx, |this, _| { - Ok(completions.into_iter().filter_map(|lsp_completion| { - let (old_range, new_text) = match lsp_completion.text_edit.as_ref()? { - lsp::CompletionTextEdit::Edit(edit) => (range_from_lsp(edit.range), edit.new_text.clone()), - lsp::CompletionTextEdit::InsertAndReplace(_) => { - log::info!("received an insert and replace completion but we don't yet support that"); - return None - }, - }; - - let clipped_start = this.clip_point_utf16(old_range.start, Bias::Left); - let clipped_end = this.clip_point_utf16(old_range.end, Bias::Left) ; - if clipped_start == old_range.start && clipped_end == old_range.end { - Some(Completion { - old_range: this.anchor_before(old_range.start)..this.anchor_after(old_range.end), - new_text, - label: language.as_ref().and_then(|l| l.label_for_completion(&lsp_completion)).unwrap_or_else(|| CompletionLabel::plain(&lsp_completion)), - lsp_completion, - }) - } else { - None - } - }).collect()) - }) - }) - } else { - file.completions( - self.remote_id(), - self.anchor_before(position), - language, - cx.as_mut(), - ) - } - } - - pub fn apply_additional_edits_for_completion( - &mut self, - completion: Completion, - push_to_history: bool, - cx: &mut ModelContext, - ) -> Task>> { - 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::(completion.lsp_completion) - .await?; - if let Some(additional_edits) = resolved_completion.additional_text_edits { - this.update(&mut cx, |this, cx| { - this.finalize_last_transaction(); - this.start_transaction(); - this.apply_lsp_edits(additional_edits, None, cx).log_err(); - let transaction = if this.end_transaction(cx).is_some() { - let transaction = this.finalize_last_transaction().unwrap().clone(); - if !push_to_history { - this.forget_transaction(transaction.id); - } - Some(transaction) - } else { - None - }; - Ok(transaction) - }) - } else { - Ok(None) - } - }) - } else { - let apply_edits = file.apply_additional_edits_for_completion( - self.remote_id(), - completion, - cx.as_mut(), - ); - cx.spawn(|this, mut cx| async move { - if let Some(transaction) = apply_edits.await? { - this.update(&mut cx, |this, _| { - this.wait_for_edits(transaction.edit_ids.iter().copied()) - }) - .await; - if push_to_history { - this.update(&mut cx, |this, _| { - this.push_transaction(transaction.clone(), Instant::now()); - }); - } - Ok(Some(transaction)) - } else { - Ok(None) - } - }) - } - } - pub fn completion_triggers(&self) -> &[String] { &self.completion_triggers } @@ -2737,7 +2552,7 @@ impl Default for Diagnostic { } } -impl Completion { +impl Completion { pub fn sort_key(&self) -> (usize, &str) { let kind_key = match self.lsp_completion.kind { Some(lsp::CompletionItemKind::VARIABLE) => 0, diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index fbcd5efa7851ece78dddc23852422475511a3dc0..44cbba47fbc198190588494cbb164803c000b719 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -394,7 +394,7 @@ pub fn lamport_timestamp_for_operation(operation: &proto::Operation) -> Option) -> proto::Completion { +pub fn serialize_completion(completion: &Completion) -> proto::Completion { proto::Completion { old_start: Some(serialize_anchor(&completion.old_range.start)), old_end: Some(serialize_anchor(&completion.old_range.end)), @@ -406,7 +406,7 @@ pub fn serialize_completion(completion: &Completion) -> proto::Completio pub fn deserialize_completion( completion: proto::Completion, language: Option<&Arc>, -) -> Result> { +) -> Result { let old_start = completion .old_start .and_then(deserialize_anchor) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 77bea77d10d91c71ad90eb779f352e551cbceb3d..2d15275f22ca13fd01d086d2c06ab0734b7f928d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -15,8 +15,9 @@ use gpui::{ use language::{ point_from_lsp, proto::{deserialize_anchor, serialize_anchor}, - range_from_lsp, Bias, Buffer, CodeAction, Diagnostic, DiagnosticEntry, File as _, Language, - LanguageRegistry, PointUtf16, ToLspPosition, ToOffset, ToPointUtf16, + range_from_lsp, Bias, Buffer, CodeAction, Completion, CompletionLabel, Diagnostic, + DiagnosticEntry, File as _, Language, LanguageRegistry, PointUtf16, ToLspPosition, + ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, LanguageServer}; use postage::{prelude::Stream, watch}; @@ -1035,7 +1036,7 @@ impl Project { Ok(()) } - pub fn definition( + pub fn definition( &self, source_buffer_handle: &ModelHandle, position: T, @@ -1052,8 +1053,9 @@ impl Project { return Task::ready(Err(anyhow!("buffer does not belong to any worktree"))); }; + let position = position.to_point_utf16(source_buffer); + if worktree.read(cx).as_local().is_some() { - let point = source_buffer.offset_to_point_utf16(position.to_offset(source_buffer)); let buffer_abs_path = buffer_abs_path.unwrap(); let lang_name; let lang_server; @@ -1078,7 +1080,7 @@ impl Project { text_document: lsp::TextDocumentIdentifier::new( lsp::Url::from_file_path(&buffer_abs_path).unwrap(), ), - position: lsp::Position::new(point.row, point.column), + position: lsp::Position::new(position.row, position.column), }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), @@ -1171,6 +1173,193 @@ impl Project { } } + pub fn completions( + &self, + source_buffer_handle: &ModelHandle, + position: T, + cx: &mut ModelContext, + ) -> Task>> { + let source_buffer_handle = source_buffer_handle.clone(); + let source_buffer = source_buffer_handle.read(cx); + let buffer_id = source_buffer.remote_id(); + let language = source_buffer.language().cloned(); + let worktree; + let buffer_abs_path; + if let Some(file) = File::from_dyn(source_buffer.file()) { + worktree = file.worktree.clone(); + buffer_abs_path = file.as_local().map(|f| f.abs_path(cx)); + } else { + return Task::ready(Err(anyhow!("buffer does not belong to any worktree"))); + }; + + let position = position.to_point_utf16(source_buffer); + let anchor = source_buffer.anchor_after(position); + + if worktree.read(cx).as_local().is_some() { + let buffer_abs_path = buffer_abs_path.unwrap(); + let lang_name; + let lang_server; + if let Some(lang) = &language { + lang_name = lang.name().to_string(); + if let Some(server) = self + .language_servers + .get(&(worktree.read(cx).id(), lang_name.clone())) + { + lang_server = server.clone(); + } else { + return Task::ready(Err(anyhow!("buffer does not have a language server"))); + }; + } else { + return Task::ready(Err(anyhow!("buffer does not have a language"))); + } + + cx.spawn(|_, cx| async move { + let completions = lang_server + .request::(lsp::CompletionParams { + text_document_position: lsp::TextDocumentPositionParams::new( + lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path(buffer_abs_path).unwrap(), + ), + position.to_lsp_position(), + ), + context: Default::default(), + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + }) + .await?; + + let completions = if let Some(completions) = completions { + match completions { + lsp::CompletionResponse::Array(completions) => completions, + lsp::CompletionResponse::List(list) => list.items, + } + } else { + Default::default() + }; + + source_buffer_handle.read_with(&cx, |this, _| { + Ok(completions.into_iter().filter_map(|lsp_completion| { + let (old_range, new_text) = match lsp_completion.text_edit.as_ref()? { + lsp::CompletionTextEdit::Edit(edit) => (range_from_lsp(edit.range), edit.new_text.clone()), + lsp::CompletionTextEdit::InsertAndReplace(_) => { + log::info!("received an insert and replace completion but we don't yet support that"); + return None + }, + }; + + let clipped_start = this.clip_point_utf16(old_range.start, Bias::Left); + let clipped_end = this.clip_point_utf16(old_range.end, Bias::Left) ; + if clipped_start == old_range.start && clipped_end == old_range.end { + Some(Completion { + old_range: this.anchor_before(old_range.start)..this.anchor_after(old_range.end), + new_text, + label: language.as_ref().and_then(|l| l.label_for_completion(&lsp_completion)).unwrap_or_else(|| CompletionLabel::plain(&lsp_completion)), + lsp_completion, + }) + } else { + None + } + }).collect()) + }) + + }) + } else if let Some(project_id) = self.remote_id() { + let rpc = self.client.clone(); + cx.foreground().spawn(async move { + let response = rpc + .request(proto::GetCompletions { + project_id, + buffer_id, + position: Some(language::proto::serialize_anchor(&anchor)), + }) + .await?; + response + .completions + .into_iter() + .map(|completion| { + language::proto::deserialize_completion(completion, language.as_ref()) + }) + .collect() + }) + } else { + Task::ready(Err(anyhow!("project does not have a remote id"))) + } + } + + pub fn apply_additional_edits_for_completion( + &self, + buffer_handle: ModelHandle, + completion: Completion, + push_to_history: bool, + cx: &mut ModelContext, + ) -> Task>> { + let buffer = buffer_handle.read(cx); + let buffer_id = buffer.remote_id(); + + if self.is_local() { + let lang_server = if let Some(language_server) = buffer.language_server() { + language_server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; + + cx.spawn(|_, mut cx| async move { + let resolved_completion = lang_server + .request::(completion.lsp_completion) + .await?; + if let Some(additional_edits) = resolved_completion.additional_text_edits { + buffer_handle.update(&mut cx, |buffer, cx| { + buffer.finalize_last_transaction(); + buffer.start_transaction(); + buffer.apply_lsp_edits(additional_edits, None, cx).log_err(); + let transaction = if buffer.end_transaction(cx).is_some() { + let transaction = buffer.finalize_last_transaction().unwrap().clone(); + if !push_to_history { + buffer.forget_transaction(transaction.id); + } + Some(transaction) + } else { + None + }; + Ok(transaction) + }) + } else { + Ok(None) + } + }) + } else if let Some(project_id) = self.remote_id() { + let client = self.client.clone(); + cx.spawn(|_, mut cx| async move { + let response = client + .request(proto::ApplyCompletionAdditionalEdits { + project_id, + buffer_id, + completion: Some(language::proto::serialize_completion(&completion)), + }) + .await?; + + if let Some(transaction) = response.transaction { + let transaction = language::proto::deserialize_transaction(transaction)?; + buffer_handle + .update(&mut cx, |buffer, _| { + buffer.wait_for_edits(transaction.edit_ids.iter().copied()) + }) + .await; + if push_to_history { + buffer_handle.update(&mut cx, |buffer, _| { + buffer.push_transaction(transaction.clone(), Instant::now()); + }); + } + Ok(Some(transaction)) + } else { + Ok(None) + } + }) + } else { + Task::ready(Err(anyhow!("project does not have a remote id"))) + } + } + pub fn code_actions( &self, source_buffer_handle: &ModelHandle, @@ -2023,9 +2212,9 @@ impl Project { .position .and_then(language::proto::deserialize_anchor) .ok_or_else(|| anyhow!("invalid position"))?; - cx.spawn(|_, mut cx| async move { - match buffer - .update(&mut cx, |buffer, cx| buffer.completions(position, cx)) + cx.spawn(|this, mut cx| async move { + match this + .update(&mut cx, |this, cx| this.completions(&buffer, position, cx)) .await { Ok(completions) => rpc.respond( @@ -2070,10 +2259,10 @@ impl Project { .ok_or_else(|| anyhow!("invalid completion"))?, language, )?; - cx.spawn(|_, mut cx| async move { - match buffer - .update(&mut cx, |buffer, cx| { - buffer.apply_additional_edits_for_completion(completion, false, cx) + cx.spawn(|this, mut cx| async move { + match this + .update(&mut cx, |this, cx| { + this.apply_additional_edits_for_completion(buffer, completion, false, cx) }) .await { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 049ebc4e168977666bf1e1d284da6e99e1232e22..f15423a7b7d21aee38cf7063b1c4de9dffb9a407 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -14,9 +14,7 @@ use gpui::{ executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, }; -use language::{ - Anchor, Buffer, Completion, DiagnosticEntry, Language, Operation, PointUtf16, Rope, Transaction, -}; +use language::{Buffer, DiagnosticEntry, Operation, PointUtf16, Rope}; use lazy_static::lazy_static; use parking_lot::Mutex; use postage::{ @@ -1406,74 +1404,6 @@ impl language::File for File { })) } - fn completions( - &self, - buffer_id: u64, - position: Anchor, - language: Option>, - cx: &mut MutableAppContext, - ) -> Task>>> { - let worktree = self.worktree.read(cx); - let worktree = if let Some(worktree) = worktree.as_remote() { - worktree - } else { - return Task::ready(Err(anyhow!( - "remote completions requested on a local worktree" - ))); - }; - let rpc = worktree.client.clone(); - let project_id = worktree.project_id; - cx.foreground().spawn(async move { - let response = rpc - .request(proto::GetCompletions { - project_id, - buffer_id, - position: Some(language::proto::serialize_anchor(&position)), - }) - .await?; - response - .completions - .into_iter() - .map(|completion| { - language::proto::deserialize_completion(completion, language.as_ref()) - }) - .collect() - }) - } - - fn apply_additional_edits_for_completion( - &self, - buffer_id: u64, - completion: Completion, - cx: &mut MutableAppContext, - ) -> Task>> { - let worktree = self.worktree.read(cx); - let worktree = if let Some(worktree) = worktree.as_remote() { - worktree - } else { - return Task::ready(Err(anyhow!( - "remote additional edits application requested on a local worktree" - ))); - }; - let rpc = worktree.client.clone(); - let project_id = worktree.project_id; - cx.foreground().spawn(async move { - let response = rpc - .request(proto::ApplyCompletionAdditionalEdits { - project_id, - buffer_id, - completion: Some(language::proto::serialize_completion(&completion)), - }) - .await?; - - if let Some(transaction) = response.transaction { - Ok(Some(language::proto::deserialize_transaction(transaction)?)) - } else { - Ok(None) - } - }) - } - fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) { self.worktree.update(cx, |worktree, cx| { worktree.send_buffer_update(buffer_id, operation, cx); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 8314bec3db5080dabaca9f46ea0025fffb584960..76b50139fc42a95dad4ef3d80dbb90c242847b01 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1349,7 +1349,7 @@ mod tests { .unwrap(); let editor_b = cx_b.add_view(window_b, |cx| { - Editor::for_buffer(buffer_b, Arc::new(|cx| EditorSettings::test(cx)), cx) + Editor::for_buffer(buffer_b, Arc::new(|cx| EditorSettings::test(cx)), None, cx) }); // TODO @@ -2401,6 +2401,7 @@ mod tests { Editor::for_buffer( cx.add_model(|cx| MultiBuffer::singleton(buffer_b.clone(), cx)), Arc::new(|cx| EditorSettings::test(cx)), + None, cx, ) }); diff --git a/script/drop-test-dbs b/script/drop-test-dbs new file mode 100755 index 0000000000000000000000000000000000000000..72d6ff5f79a0d7375f38dfd84f400efba9253805 --- /dev/null +++ b/script/drop-test-dbs @@ -0,0 +1,16 @@ +#!/bin/bash + +databases=$(psql --tuples-only --command " + SELECT + datname + FROM + pg_database + WHERE + datistemplate = false + AND datname like 'zed-test-%' +") + +for database in $databases; do + echo $database + dropdb $database +done \ No newline at end of file