diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 157ecf3af6..93ab2c6ad6 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -817,19 +817,28 @@ impl Client { self.peer.send(self.connection_id()?, message) } - pub async fn request(&self, request: T) -> Result { + pub fn request( + &self, + request: T, + ) -> impl Future> { + let client_id = self.id; log::debug!( "rpc request start. client_id: {}. name:{}", - self.id, + client_id, T::NAME ); - let response = self.peer.request(self.connection_id()?, request).await; - log::debug!( - "rpc request finish. client_id: {}. name:{}", - self.id, - T::NAME - ); - response + let response = self + .connection_id() + .map(|conn_id| self.peer.request(conn_id, request)); + async move { + let response = response?.await; + log::debug!( + "rpc request finish. client_id: {}. name:{}", + client_id, + T::NAME + ); + response + } } fn respond(&self, receipt: Receipt, response: T::Response) -> Result<()> { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d7db6d747e..ebe24da4e2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -24,8 +24,9 @@ use gpui::{ geometry::vector::{vec2f, Vector2F}, keymap::Binding, platform::CursorStyle, - text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, - MutableAppContext, RenderContext, Task, View, ViewContext, WeakModelHandle, WeakViewHandle, + text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, + ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, + WeakModelHandle, WeakViewHandle, }; use items::{BufferItemHandle, MultiBufferItemHandle}; use itertools::Itertools as _; @@ -40,7 +41,7 @@ pub use multi_buffer::{ }; use ordered_float::OrderedFloat; use postage::watch; -use project::Project; +use project::{Project, ProjectTransaction}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use smol::Timer; @@ -117,6 +118,8 @@ action!(SelectSmallerSyntaxNode); action!(MoveToEnclosingBracket); action!(ShowNextDiagnostic); action!(GoToDefinition); +action!(Rename); +action!(ConfirmRename); action!(PageUp); action!(PageDown); action!(Fold); @@ -153,6 +156,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec, Arc<[CodeAction]>)>, code_actions_task: Option>, + pending_rename: Option, } pub struct EditorSnapshot { @@ -470,6 +478,13 @@ struct SnippetState { active_index: usize, } +pub struct RenameState { + pub range: Range, + pub old_name: String, + pub editor: ViewHandle, + block_id: BlockId, +} + struct InvalidationStack(Vec); enum ContextMenu { @@ -885,6 +900,7 @@ impl Editor { next_completion_id: 0, available_code_actions: Default::default(), code_actions_task: Default::default(), + pending_rename: Default::default(), }; this.end_selection(cx); this @@ -1438,6 +1454,10 @@ impl Editor { } pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + if self.take_rename(cx).is_some() { + return; + } + if self.hide_context_menu(cx).is_some() { return; } @@ -1906,6 +1926,10 @@ impl Editor { } fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { + if self.pending_rename.is_some() { + return; + } + let project = if let Some(project) = self.project.clone() { project } else { @@ -2153,79 +2177,88 @@ impl Editor { let action = actions_menu.actions.get(action_ix)?.clone(); let title = action.lsp_action.title.clone(); let buffer = actions_menu.buffer; - let replica_id = editor.read(cx).replica_id(cx); let apply_code_actions = workspace.project().clone().update(cx, |project, cx| { project.apply_code_action(buffer, action, true, cx) }); - Some(cx.spawn(|workspace, mut cx| async move { + Some(cx.spawn(|workspace, cx| async move { let project_transaction = apply_code_actions.await?; + Self::open_project_transaction(editor, workspace, project_transaction, title, cx).await + })) + } - // If the code action's edits are all contained within this editor, then - // avoid opening a new editor to display them. - let mut entries = project_transaction.0.iter(); - if let Some((buffer, transaction)) = entries.next() { - if entries.next().is_none() { - let excerpt = editor.read_with(&cx, |editor, cx| { - editor - .buffer() - .read(cx) - .excerpt_containing(editor.newest_anchor_selection().head(), cx) - }); - if let Some((excerpted_buffer, excerpt_range)) = excerpt { - if excerpted_buffer == *buffer { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - let excerpt_range = excerpt_range.to_offset(&snapshot); - if snapshot - .edited_ranges_for_transaction(transaction) - .all(|range| { - excerpt_range.start <= range.start - && excerpt_range.end >= range.end - }) - { - return Ok(()); - } + async fn open_project_transaction( + this: ViewHandle, + workspace: ViewHandle, + transaction: ProjectTransaction, + title: String, + mut cx: AsyncAppContext, + ) -> Result<()> { + let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx)); + + // If the code action's edits are all contained within this editor, then + // avoid opening a new editor to display them. + let mut entries = transaction.0.iter(); + if let Some((buffer, transaction)) = entries.next() { + if entries.next().is_none() { + let excerpt = this.read_with(&cx, |editor, cx| { + editor + .buffer() + .read(cx) + .excerpt_containing(editor.newest_anchor_selection().head(), cx) + }); + if let Some((excerpted_buffer, excerpt_range)) = excerpt { + if excerpted_buffer == *buffer { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + let excerpt_range = excerpt_range.to_offset(&snapshot); + if snapshot + .edited_ranges_for_transaction(transaction) + .all(|range| { + excerpt_range.start <= range.start && excerpt_range.end >= range.end + }) + { + return Ok(()); } } } } + } - let mut ranges_to_highlight = Vec::new(); - let excerpt_buffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); - for (buffer, transaction) in &project_transaction.0 { - let snapshot = buffer.read(cx).snapshot(); - ranges_to_highlight.extend( - multibuffer.push_excerpts_with_context_lines( - buffer.clone(), - snapshot - .edited_ranges_for_transaction::(transaction) - .collect(), - 1, - cx, - ), + let mut ranges_to_highlight = Vec::new(); + let excerpt_buffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); + for (buffer, transaction) in &transaction.0 { + let snapshot = buffer.read(cx).snapshot(); + ranges_to_highlight.extend( + multibuffer.push_excerpts_with_context_lines( + buffer.clone(), + snapshot + .edited_ranges_for_transaction::(transaction) + .collect(), + 1, + cx, + ), + ); + } + multibuffer.push_transaction(&transaction.0); + multibuffer + }); + + workspace.update(&mut cx, |workspace, cx| { + let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); + if let Some(editor) = editor.act_as::(cx) { + editor.update(cx, |editor, cx| { + let settings = (editor.build_settings)(cx); + editor.highlight_ranges::( + ranges_to_highlight, + settings.style.highlighted_line_background, + cx, ); - } - multibuffer.push_transaction(&project_transaction.0); - multibuffer - }); + }); + } + }); - workspace.update(&mut cx, |workspace, cx| { - let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - let settings = (editor.build_settings)(cx); - editor.highlight_ranges::( - ranges_to_highlight, - settings.style.highlighted_line_background, - cx, - ); - }); - } - }); - - Ok(()) - })) + Ok(()) } fn refresh_code_actions(&mut self, cx: &mut ViewContext) -> Option<()> { @@ -3130,6 +3163,10 @@ impl Editor { } pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { + if self.take_rename(cx).is_some() { + return; + } + if let Some(context_menu) = self.context_menu.as_mut() { if context_menu.select_prev(cx) { return; @@ -3174,6 +3211,8 @@ impl Editor { } pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { + self.take_rename(cx); + if let Some(context_menu) = self.context_menu.as_mut() { if context_menu.select_next(cx) { return; @@ -4059,6 +4098,219 @@ impl Editor { .detach_and_log_err(cx); } + pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { + use language::ToOffset as _; + + let project = self.project.clone()?; + let selection = self.newest_anchor_selection().clone(); + let (cursor_buffer, cursor_buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.head(), cx)?; + let (tail_buffer, tail_buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.tail(), cx)?; + if tail_buffer != cursor_buffer { + return None; + } + + let snapshot = cursor_buffer.read(cx).snapshot(); + let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); + let tail_buffer_offset = tail_buffer_position.to_offset(&snapshot); + let prepare_rename = project.update(cx, |project, cx| { + project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx) + }); + + Some(cx.spawn(|this, mut cx| async move { + if let Some(rename_range) = prepare_rename.await? { + let rename_buffer_range = rename_range.to_offset(&snapshot); + let cursor_offset_in_rename_range = + cursor_buffer_offset.saturating_sub(rename_buffer_range.start); + let tail_offset_in_rename_range = + tail_buffer_offset.saturating_sub(rename_buffer_range.start); + + this.update(&mut cx, |this, cx| { + this.take_rename(cx); + let settings = (this.build_settings)(cx); + let buffer = this.buffer.read(cx).read(cx); + let cursor_offset = selection.head().to_offset(&buffer); + let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); + let rename_end = rename_start + rename_buffer_range.len(); + let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); + let old_name = buffer + .text_for_range(rename_start..rename_end) + .collect::(); + drop(buffer); + + // Position the selection in the rename editor so that it matches the current selection. + let rename_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line(this.build_settings.clone(), cx); + editor + .buffer + .update(cx, |buffer, cx| buffer.edit([0..0], &old_name, cx)); + editor.select_ranges( + [tail_offset_in_rename_range..cursor_offset_in_rename_range], + None, + cx, + ); + editor.highlight_ranges::( + vec![Anchor::min()..Anchor::max()], + settings.style.diff_background_inserted, + cx, + ); + editor + }); + this.highlight_ranges::( + vec![range.clone()], + settings.style.diff_background_deleted, + cx, + ); + this.update_selections( + vec![Selection { + id: selection.id, + start: rename_end, + end: rename_end, + reversed: false, + goal: SelectionGoal::None, + }], + None, + cx, + ); + cx.focus(&rename_editor); + let block_id = this.insert_blocks( + [BlockProperties { + position: range.start.clone(), + height: 1, + render: Arc::new({ + let editor = rename_editor.clone(); + move |cx: &BlockContext| { + ChildView::new(editor.clone()) + .contained() + .with_padding_left(cx.anchor_x) + .boxed() + } + }), + disposition: BlockDisposition::Below, + }], + cx, + )[0]; + this.pending_rename = Some(RenameState { + range, + old_name, + editor: rename_editor, + block_id, + }); + }); + } + + Ok(()) + })) + } + + pub fn confirm_rename( + workspace: &mut Workspace, + _: &ConfirmRename, + cx: &mut ViewContext, + ) -> Option>> { + let editor = workspace.active_item(cx)?.act_as::(cx)?; + + let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| { + let rename = editor.take_rename(cx)?; + let buffer = editor.buffer.read(cx); + let (start_buffer, start) = + buffer.text_anchor_for_position(rename.range.start.clone(), cx)?; + let (end_buffer, end) = + buffer.text_anchor_for_position(rename.range.end.clone(), cx)?; + if start_buffer == end_buffer { + let new_name = rename.editor.read(cx).text(cx); + Some((start_buffer, start..end, rename.old_name, new_name)) + } else { + None + } + })?; + + let rename = workspace.project().clone().update(cx, |project, cx| { + project.perform_rename( + buffer.clone(), + range.start.clone(), + new_name.clone(), + true, + cx, + ) + }); + + Some(cx.spawn(|workspace, cx| async move { + let project_transaction = rename.await?; + Self::open_project_transaction( + editor, + workspace, + project_transaction, + format!("Rename: {} → {}", old_name, new_name), + cx, + ) + .await + })) + } + + fn take_rename(&mut self, cx: &mut ViewContext) -> Option { + let rename = self.pending_rename.take()?; + self.remove_blocks([rename.block_id].into_iter().collect(), cx); + self.clear_highlighted_ranges::(cx); + + let editor = rename.editor.read(cx); + let buffer = editor.buffer.read(cx).snapshot(cx); + let selection = editor.newest_selection::(&buffer); + + // Update the selection to match the position of the selection inside + // the rename editor. + let snapshot = self.buffer.read(cx).snapshot(cx); + let rename_range = rename.range.to_offset(&snapshot); + let start = snapshot + .clip_offset(rename_range.start + selection.start, Bias::Left) + .min(rename_range.end); + let end = snapshot + .clip_offset(rename_range.start + selection.end, Bias::Left) + .min(rename_range.end); + self.update_selections( + vec![Selection { + id: self.newest_anchor_selection().id, + start, + end, + reversed: selection.reversed, + goal: SelectionGoal::None, + }], + None, + cx, + ); + + Some(rename) + } + + fn invalidate_rename_range( + &mut self, + buffer: &MultiBufferSnapshot, + cx: &mut ViewContext, + ) { + if let Some(rename) = self.pending_rename.as_ref() { + if self.selections.len() == 1 { + let head = self.selections[0].head().to_offset(buffer); + let range = rename.range.to_offset(buffer).to_inclusive(); + if range.contains(&head) { + return; + } + } + let rename = self.pending_rename.take().unwrap(); + self.remove_blocks([rename.block_id].into_iter().collect(), cx); + self.clear_highlighted_ranges::(cx); + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn pending_rename(&self) -> Option<&RenameState> { + self.pending_rename.as_ref() + } + fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext) { if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { let buffer = self.buffer.read(cx).snapshot(cx); @@ -4471,6 +4723,7 @@ impl Editor { self.select_larger_syntax_node_stack.clear(); self.autoclose_stack.invalidate(&self.selections, &buffer); self.snippet_stack.invalidate(&self.selections, &buffer); + self.invalidate_rename_range(&buffer, cx); let new_cursor_position = self.newest_anchor_selection().head(); @@ -4746,9 +4999,12 @@ impl Editor { cx.notify(); } - pub fn clear_highlighted_ranges(&mut self, cx: &mut ViewContext) { - self.highlighted_ranges.remove(&TypeId::of::()); + pub fn clear_highlighted_ranges( + &mut self, + cx: &mut ViewContext, + ) -> Option<(Color, Vec>)> { cx.notify(); + self.highlighted_ranges.remove(&TypeId::of::()) } #[cfg(feature = "test-support")] @@ -4958,6 +5214,8 @@ impl EditorSettings { gutter_padding_factor: 2., active_line_background: Default::default(), highlighted_line_background: Default::default(), + diff_background_deleted: Default::default(), + diff_background_inserted: Default::default(), line_number: Default::default(), line_number_active: Default::default(), selection: Default::default(), @@ -5078,6 +5336,9 @@ impl View for Editor { EditorMode::Full => "full", }; cx.map.insert("mode".into(), mode.into()); + if self.pending_rename.is_some() { + cx.set.insert("renaming".into()); + } match self.context_menu.as_ref() { Some(ContextMenu::Completions(_)) => { cx.set.insert("showing_completions".into()); @@ -7747,8 +8008,8 @@ mod tests { " .unindent(); - let fs = Arc::new(FakeFs::new(cx.background().clone())); - fs.insert_file("/file", text).await.unwrap(); + let fs = FakeFs::new(cx.background().clone()); + fs.insert_file("/file", text).await; let project = Project::test(fs, &mut cx); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f72320be42..10a7656f73 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -299,7 +299,7 @@ impl EditorElement { if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() { let mut x = bounds.width() - layout.gutter_padding; let mut y = *row as f32 * layout.line_height - scroll_top; - x += ((layout.gutter_padding + layout.text_offset.x()) - indicator.size().x()) / 2.; + x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.; y += (layout.line_height - indicator.size().y()) / 2.; indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx); } @@ -321,7 +321,7 @@ impl EditorElement { let end_row = ((scroll_top + bounds.height()) / layout.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen let max_glyph_width = layout.em_width; let scroll_left = scroll_position.x() * max_glyph_width; - let content_origin = bounds.origin() + layout.text_offset; + let content_origin = bounds.origin() + layout.gutter_margin; cx.scene.push_layer(Some(bounds)); @@ -776,22 +776,24 @@ impl Element for EditorElement { let gutter_padding; let gutter_width; + let gutter_margin; if snapshot.mode == EditorMode::Full { gutter_padding = style.text.em_width(cx.font_cache) * style.gutter_padding_factor; gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; + gutter_margin = -style.text.descent(cx.font_cache); } else { gutter_padding = 0.0; - gutter_width = 0.0 + gutter_width = 0.0; + gutter_margin = 0.0; }; let text_width = size.x() - gutter_width; - let text_offset = vec2f(-style.text.descent(cx.font_cache), 0.); let em_width = style.text.em_width(cx.font_cache); let em_advance = style.text.em_advance(cx.font_cache); let overscroll = vec2f(em_width, 0.); let wrap_width = match self.settings.soft_wrap { SoftWrap::None => None, - SoftWrap::EditorWidth => Some(text_width - text_offset.x() - overscroll.x() - em_width), + SoftWrap::EditorWidth => Some(text_width - gutter_margin - overscroll.x() - em_width), SoftWrap::Column(column) => Some(column as f32 * em_advance), }; let snapshot = self.update_view(cx.app, |view, cx| { @@ -991,7 +993,7 @@ impl Element for EditorElement { gutter_padding, gutter_width, em_width, - gutter_width + text_offset.x(), + gutter_width + gutter_margin, line_height, &style, &line_layouts, @@ -1006,7 +1008,7 @@ impl Element for EditorElement { gutter_size, gutter_padding, text_size, - text_offset, + gutter_margin, snapshot, active_rows, highlighted_rows, @@ -1080,6 +1082,12 @@ impl Element for EditorElement { } } + for (_, block) in &mut layout.blocks { + if block.dispatch_event(event, cx) { + return true; + } + } + match event { Event::LeftMouseDown { position, @@ -1123,6 +1131,7 @@ pub struct LayoutState { scroll_max: Vector2F, gutter_size: Vector2F, gutter_padding: f32, + gutter_margin: f32, text_size: Vector2F, snapshot: EditorSnapshot, active_rows: BTreeMap, @@ -1135,7 +1144,6 @@ pub struct LayoutState { em_advance: f32, highlighted_ranges: Vec<(Range, Color)>, selections: HashMap>>, - text_offset: Vector2F, context_menu: Option<(DisplayPoint, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>, } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 8180acacc5..4fc4488af9 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -58,6 +58,7 @@ pub enum CharKind { Word, } +#[derive(Clone)] struct Transaction { id: TransactionId, buffer_transactions: HashMap, diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 7ba2076fcb..8fbdb3220e 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -268,7 +268,7 @@ pub struct FakeFs { #[cfg(any(test, feature = "test-support"))] impl FakeFs { - pub fn new(executor: std::sync::Arc) -> Self { + pub fn new(executor: std::sync::Arc) -> std::sync::Arc { let (events_tx, _) = postage::broadcast::channel(2048); let mut entries = std::collections::BTreeMap::new(); entries.insert( @@ -283,20 +283,20 @@ impl FakeFs { content: None, }, ); - Self { + std::sync::Arc::new(Self { executor, state: futures::lock::Mutex::new(FakeFsState { entries, next_inode: 1, events_tx, }), - } + }) } - pub async fn insert_dir(&self, path: impl AsRef) -> Result<()> { + pub async fn insert_dir(&self, path: impl AsRef) { let mut state = self.state.lock().await; let path = path.as_ref(); - state.validate_path(path)?; + state.validate_path(path).unwrap(); let inode = state.next_inode; state.next_inode += 1; @@ -313,13 +313,12 @@ impl FakeFs { }, ); state.emit_event(&[path]).await; - Ok(()) } - pub async fn insert_file(&self, path: impl AsRef, content: String) -> Result<()> { + pub async fn insert_file(&self, path: impl AsRef, content: String) { let mut state = self.state.lock().await; let path = path.as_ref(); - state.validate_path(path)?; + state.validate_path(path).unwrap(); let inode = state.next_inode; state.next_inode += 1; @@ -336,7 +335,6 @@ impl FakeFs { }, ); state.emit_event(&[path]).await; - Ok(()) } #[must_use] @@ -353,7 +351,7 @@ impl FakeFs { match tree { Object(map) => { - self.insert_dir(path).await.unwrap(); + self.insert_dir(path).await; for (name, contents) in map { let mut path = PathBuf::from(path); path.push(name); @@ -361,10 +359,10 @@ impl FakeFs { } } Null => { - self.insert_dir(&path).await.unwrap(); + self.insert_dir(&path).await; } String(contents) => { - self.insert_file(&path, contents).await.unwrap(); + self.insert_file(&path, contents).await; } _ => { panic!("JSON object must contain only objects, strings, or null"); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs new file mode 100644 index 0000000000..432963a57f --- /dev/null +++ b/crates/project/src/lsp_command.rs @@ -0,0 +1,449 @@ +use crate::{Definition, Project, ProjectTransaction}; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use client::{proto, PeerId}; +use gpui::{AppContext, AsyncAppContext, ModelHandle}; +use language::{ + point_from_lsp, + proto::{deserialize_anchor, serialize_anchor}, + range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition, ToPointUtf16, +}; +use std::{ops::Range, path::Path}; + +#[async_trait(?Send)] +pub(crate) trait LspCommand: 'static + Sized { + type Response: 'static + Default + Send; + type LspRequest: 'static + Send + lsp::request::Request; + type ProtoRequest: 'static + Send + proto::RequestMessage; + + fn to_lsp( + &self, + path: &Path, + cx: &AppContext, + ) -> ::Params; + async fn response_from_lsp( + self, + message: ::Result, + project: ModelHandle, + buffer: ModelHandle, + cx: AsyncAppContext, + ) -> Result; + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest; + fn from_proto( + message: Self::ProtoRequest, + project: &mut Project, + buffer: &Buffer, + ) -> Result; + fn response_to_proto( + response: Self::Response, + project: &mut Project, + peer_id: PeerId, + buffer_version: &clock::Global, + cx: &AppContext, + ) -> ::Response; + async fn response_from_proto( + self, + message: ::Response, + project: ModelHandle, + buffer: ModelHandle, + cx: AsyncAppContext, + ) -> Result; + fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64; +} + +pub(crate) struct PrepareRename { + pub position: PointUtf16, +} + +pub(crate) struct PerformRename { + pub position: PointUtf16, + pub new_name: String, + pub push_to_history: bool, +} + +pub(crate) struct GetDefinition { + pub position: PointUtf16, +} + +#[async_trait(?Send)] +impl LspCommand for PrepareRename { + type Response = Option>; + type LspRequest = lsp::request::PrepareRenameRequest; + type ProtoRequest = proto::PrepareRename; + + fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::TextDocumentPositionParams { + lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }, + position: self.position.to_lsp_position(), + } + } + + async fn response_from_lsp( + self, + message: Option, + _: ModelHandle, + buffer: ModelHandle, + cx: AsyncAppContext, + ) -> Result>> { + buffer.read_with(&cx, |buffer, _| { + if let Some( + lsp::PrepareRenameResponse::Range(range) + | lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. }, + ) = message + { + let Range { start, end } = range_from_lsp(range); + if buffer.clip_point_utf16(start, Bias::Left) == start + && buffer.clip_point_utf16(end, Bias::Left) == end + { + return Ok(Some(buffer.anchor_after(start)..buffer.anchor_before(end))); + } + } + Ok(None) + }) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PrepareRename { + proto::PrepareRename { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + } + } + + fn from_proto(message: proto::PrepareRename, _: &mut Project, buffer: &Buffer) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + if !buffer.can_resolve(&position) { + Err(anyhow!("cannot resolve position"))?; + } + Ok(Self { + position: position.to_point_utf16(buffer), + }) + } + + fn response_to_proto( + range: Option>, + _: &mut Project, + _: PeerId, + buffer_version: &clock::Global, + _: &AppContext, + ) -> proto::PrepareRenameResponse { + proto::PrepareRenameResponse { + can_rename: range.is_some(), + start: range + .as_ref() + .map(|range| language::proto::serialize_anchor(&range.start)), + end: range + .as_ref() + .map(|range| language::proto::serialize_anchor(&range.end)), + version: buffer_version.into(), + } + } + + async fn response_from_proto( + self, + message: proto::PrepareRenameResponse, + _: ModelHandle, + buffer: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result>> { + if message.can_rename { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(message.version.into()) + }) + .await; + let start = message.start.and_then(deserialize_anchor); + let end = message.end.and_then(deserialize_anchor); + Ok(start.zip(end).map(|(start, end)| start..end)) + } else { + Ok(None) + } + } + + fn buffer_id_from_proto(message: &proto::PrepareRename) -> u64 { + message.buffer_id + } +} + +#[async_trait(?Send)] +impl LspCommand for PerformRename { + type Response = ProjectTransaction; + type LspRequest = lsp::request::Rename; + type ProtoRequest = proto::PerformRename; + + fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::RenameParams { + lsp::RenameParams { + text_document_position: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }, + position: self.position.to_lsp_position(), + }, + new_name: self.new_name.clone(), + work_done_progress_params: Default::default(), + } + } + + async fn response_from_lsp( + self, + message: Option, + project: ModelHandle, + buffer: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result { + if let Some(edit) = message { + let (language_name, language_server) = buffer.read_with(&cx, |buffer, _| { + let language = buffer + .language() + .ok_or_else(|| anyhow!("buffer's language was removed"))?; + let language_server = buffer + .language_server() + .cloned() + .ok_or_else(|| anyhow!("buffer's language server was removed"))?; + Ok::<_, anyhow::Error>((language.name().to_string(), language_server)) + })?; + Project::deserialize_workspace_edit( + project, + edit, + self.push_to_history, + language_name, + language_server, + &mut cx, + ) + .await + } else { + Ok(ProjectTransaction::default()) + } + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PerformRename { + proto::PerformRename { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + new_name: self.new_name.clone(), + } + } + + fn from_proto(message: proto::PerformRename, _: &mut Project, buffer: &Buffer) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + if !buffer.can_resolve(&position) { + Err(anyhow!("cannot resolve position"))?; + } + Ok(Self { + position: position.to_point_utf16(buffer), + new_name: message.new_name, + push_to_history: false, + }) + } + + fn response_to_proto( + response: ProjectTransaction, + project: &mut Project, + peer_id: PeerId, + _: &clock::Global, + cx: &AppContext, + ) -> proto::PerformRenameResponse { + let transaction = project.serialize_project_transaction_for_peer(response, peer_id, cx); + proto::PerformRenameResponse { + transaction: Some(transaction), + } + } + + async fn response_from_proto( + self, + message: proto::PerformRenameResponse, + project: ModelHandle, + _: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result { + let message = message + .transaction + .ok_or_else(|| anyhow!("missing transaction"))?; + project + .update(&mut cx, |project, cx| { + project.deserialize_project_transaction(message, self.push_to_history, cx) + }) + .await + } + + fn buffer_id_from_proto(message: &proto::PerformRename) -> u64 { + message.buffer_id + } +} + +#[async_trait(?Send)] +impl LspCommand for GetDefinition { + type Response = Vec; + type LspRequest = lsp::request::GotoDefinition; + type ProtoRequest = proto::GetDefinition; + + fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::GotoDefinitionParams { + lsp::GotoDefinitionParams { + text_document_position_params: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }, + position: self.position.to_lsp_position(), + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + } + } + + async fn response_from_lsp( + self, + message: Option, + project: ModelHandle, + buffer: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result> { + let mut definitions = Vec::new(); + let (language, language_server) = buffer + .read_with(&cx, |buffer, _| { + buffer + .language() + .cloned() + .zip(buffer.language_server().cloned()) + }) + .ok_or_else(|| anyhow!("buffer no longer has language server"))?; + + if let Some(message) = message { + let mut unresolved_locations = Vec::new(); + match message { + lsp::GotoDefinitionResponse::Scalar(loc) => { + unresolved_locations.push((loc.uri, loc.range)); + } + lsp::GotoDefinitionResponse::Array(locs) => { + unresolved_locations.extend(locs.into_iter().map(|l| (l.uri, l.range))); + } + lsp::GotoDefinitionResponse::Link(links) => { + unresolved_locations.extend( + links + .into_iter() + .map(|l| (l.target_uri, l.target_selection_range)), + ); + } + } + + for (target_uri, target_range) in unresolved_locations { + let target_buffer_handle = project + .update(&mut cx, |this, cx| { + this.open_local_buffer_from_lsp_path( + target_uri, + language.name().to_string(), + language_server.clone(), + cx, + ) + }) + .await?; + + cx.read(|cx| { + let target_buffer = target_buffer_handle.read(cx); + let target_start = target_buffer + .clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); + let target_end = target_buffer + .clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); + definitions.push(Definition { + target_buffer: target_buffer_handle, + target_range: target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end), + }); + }); + } + } + + Ok(definitions) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition { + proto::GetDefinition { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + } + } + + fn from_proto(message: proto::GetDefinition, _: &mut Project, buffer: &Buffer) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + if !buffer.can_resolve(&position) { + Err(anyhow!("cannot resolve position"))?; + } + Ok(Self { + position: position.to_point_utf16(buffer), + }) + } + + fn response_to_proto( + response: Vec, + project: &mut Project, + peer_id: PeerId, + _: &clock::Global, + cx: &AppContext, + ) -> proto::GetDefinitionResponse { + let definitions = response + .into_iter() + .map(|definition| { + let buffer = + project.serialize_buffer_for_peer(&definition.target_buffer, peer_id, cx); + proto::Definition { + target_start: Some(serialize_anchor(&definition.target_range.start)), + target_end: Some(serialize_anchor(&definition.target_range.end)), + buffer: Some(buffer), + } + }) + .collect(); + proto::GetDefinitionResponse { definitions } + } + + async fn response_from_proto( + self, + message: proto::GetDefinitionResponse, + project: ModelHandle, + _: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result> { + let mut definitions = Vec::new(); + for definition in message.definitions { + let buffer = definition.buffer.ok_or_else(|| anyhow!("missing buffer"))?; + let target_buffer = project + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await?; + let target_start = definition + .target_start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target start"))?; + let target_end = definition + .target_end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target end"))?; + definitions.push(Definition { + target_buffer, + target_range: target_start..target_end, + }) + } + Ok(definitions) + } + + fn buffer_id_from_proto(message: &proto::GetDefinition) -> u64 { + message.buffer_id + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 208604bd09..9a231b707b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,5 +1,6 @@ pub mod fs; mod ignore; +mod lsp_command; pub mod worktree; use anyhow::{anyhow, Context, Result}; @@ -13,13 +14,12 @@ use gpui::{ UpgradeModelHandle, WeakModelHandle, }; use language::{ - point_from_lsp, - proto::{deserialize_anchor, serialize_anchor}, - range_from_lsp, AnchorRangeExt, Bias, Buffer, CodeAction, Completion, CompletionLabel, + range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, Completion, CompletionLabel, Diagnostic, DiagnosticEntry, File as _, Language, LanguageRegistry, Operation, PointUtf16, ToLspPosition, ToOffset, ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, LanguageServer}; +use lsp_command::*; use postage::{broadcast, prelude::Stream, sink::Sink, watch}; use smol::block_on; use std::{ @@ -181,7 +181,9 @@ impl Project { client.add_entity_request_handler(Self::handle_format_buffers); client.add_entity_request_handler(Self::handle_get_code_actions); client.add_entity_request_handler(Self::handle_get_completions); - client.add_entity_request_handler(Self::handle_get_definition); + client.add_entity_request_handler(Self::handle_lsp_command::); + client.add_entity_request_handler(Self::handle_lsp_command::); + client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_open_buffer); client.add_entity_request_handler(Self::handle_save_buffer); } @@ -1171,137 +1173,12 @@ impl Project { pub fn definition( &self, - source_buffer_handle: &ModelHandle, + buffer: &ModelHandle, position: T, cx: &mut ModelContext, ) -> Task>> { - let source_buffer_handle = source_buffer_handle.clone(); - let source_buffer = source_buffer_handle.read(cx); - 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(Ok(Default::default())); - }; - - let position = position.to_point_utf16(source_buffer); - - 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) = source_buffer.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(Ok(Default::default())); - }; - } else { - return Task::ready(Ok(Default::default())); - } - - cx.spawn(|this, mut cx| async move { - let response = lang_server - .request::(lsp::GotoDefinitionParams { - text_document_position_params: lsp::TextDocumentPositionParams { - text_document: lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path(&buffer_abs_path).unwrap(), - ), - position: lsp::Position::new(position.row, position.column), - }, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - .await?; - - let mut definitions = Vec::new(); - if let Some(response) = response { - let mut unresolved_locations = Vec::new(); - match response { - lsp::GotoDefinitionResponse::Scalar(loc) => { - unresolved_locations.push((loc.uri, loc.range)); - } - lsp::GotoDefinitionResponse::Array(locs) => { - unresolved_locations.extend(locs.into_iter().map(|l| (l.uri, l.range))); - } - lsp::GotoDefinitionResponse::Link(links) => { - unresolved_locations.extend( - links - .into_iter() - .map(|l| (l.target_uri, l.target_selection_range)), - ); - } - } - - for (target_uri, target_range) in unresolved_locations { - let target_buffer_handle = this - .update(&mut cx, |this, cx| { - this.open_local_buffer_from_lsp_path( - target_uri, - lang_name.clone(), - lang_server.clone(), - cx, - ) - }) - .await?; - - cx.read(|cx| { - let target_buffer = target_buffer_handle.read(cx); - let target_start = target_buffer - .clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); - let target_end = target_buffer - .clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); - definitions.push(Definition { - target_buffer: target_buffer_handle, - target_range: target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end), - }); - }); - } - } - - Ok(definitions) - }) - } else if let Some(project_id) = self.remote_id() { - let client = self.client.clone(); - let request = proto::GetDefinition { - project_id, - buffer_id: source_buffer.remote_id(), - position: Some(serialize_anchor(&source_buffer.anchor_before(position))), - }; - cx.spawn(|this, mut cx| async move { - let response = client.request(request).await?; - let mut definitions = Vec::new(); - for definition in response.definitions { - let buffer = definition.buffer.ok_or_else(|| anyhow!("missing buffer"))?; - let target_buffer = this - .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) - .await?; - let target_start = definition - .target_start - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing target start"))?; - let target_end = definition - .target_end - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing target end"))?; - definitions.push(Definition { - target_buffer, - target_range: target_start..target_end, - }) - } - - Ok(definitions) - }) - } else { - Task::ready(Ok(Default::default())) - } + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp(buffer.clone(), GetDefinition { position }, cx) } pub fn completions( @@ -1625,7 +1502,6 @@ impl Project { return Task::ready(Err(anyhow!("buffer does not have a language server"))); }; let range = action.range.to_point_utf16(buffer); - let fs = self.fs.clone(); cx.spawn(|this, mut cx| async move { if let Some(lsp_range) = action @@ -1656,126 +1532,19 @@ impl Project { .lsp_action; } - let mut operations = Vec::new(); if let Some(edit) = action.lsp_action.edit { - if let Some(document_changes) = edit.document_changes { - match document_changes { - lsp::DocumentChanges::Edits(edits) => operations - .extend(edits.into_iter().map(lsp::DocumentChangeOperation::Edit)), - lsp::DocumentChanges::Operations(ops) => operations = ops, - } - } else if let Some(changes) = edit.changes { - operations.extend(changes.into_iter().map(|(uri, edits)| { - lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { - text_document: lsp::OptionalVersionedTextDocumentIdentifier { - uri, - version: None, - }, - edits: edits.into_iter().map(lsp::OneOf::Left).collect(), - }) - })); - } + Self::deserialize_workspace_edit( + this, + edit, + push_to_history, + lang_name, + lang_server, + &mut cx, + ) + .await + } else { + Ok(ProjectTransaction::default()) } - - let mut project_transaction = ProjectTransaction::default(); - for operation in operations { - match operation { - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(op)) => { - let abs_path = op - .uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - - if let Some(parent_path) = abs_path.parent() { - fs.create_dir(parent_path).await?; - } - if abs_path.ends_with("/") { - fs.create_dir(&abs_path).await?; - } else { - fs.create_file( - &abs_path, - op.options.map(Into::into).unwrap_or_default(), - ) - .await?; - } - } - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Rename(op)) => { - let source_abs_path = op - .old_uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - let target_abs_path = op - .new_uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - fs.rename( - &source_abs_path, - &target_abs_path, - op.options.map(Into::into).unwrap_or_default(), - ) - .await?; - } - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Delete(op)) => { - let abs_path = op - .uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - let options = op.options.map(Into::into).unwrap_or_default(); - if abs_path.ends_with("/") { - fs.remove_dir(&abs_path, options).await?; - } else { - fs.remove_file(&abs_path, options).await?; - } - } - lsp::DocumentChangeOperation::Edit(op) => { - let buffer_to_edit = this - .update(&mut cx, |this, cx| { - this.open_local_buffer_from_lsp_path( - op.text_document.uri, - lang_name.clone(), - lang_server.clone(), - cx, - ) - }) - .await?; - - let edits = buffer_to_edit - .update(&mut cx, |buffer, cx| { - let edits = op.edits.into_iter().map(|edit| match edit { - lsp::OneOf::Left(edit) => edit, - lsp::OneOf::Right(edit) => edit.text_edit, - }); - buffer.edits_from_lsp(edits, op.text_document.version, cx) - }) - .await?; - - let transaction = buffer_to_edit.update(&mut cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.start_transaction(); - for (range, text) in edits { - buffer.edit([range], text, cx); - } - 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 - }; - - transaction - }); - if let Some(transaction) = transaction { - project_transaction.0.insert(buffer_to_edit, transaction); - } - } - } - } - - Ok(project_transaction) }) } else if let Some(project_id) = self.remote_id() { let client = self.client.clone(); @@ -1800,6 +1569,199 @@ impl Project { } } + async fn deserialize_workspace_edit( + this: ModelHandle, + edit: lsp::WorkspaceEdit, + push_to_history: bool, + language_name: String, + language_server: Arc, + cx: &mut AsyncAppContext, + ) -> Result { + let fs = this.read_with(cx, |this, _| this.fs.clone()); + let mut operations = Vec::new(); + if let Some(document_changes) = edit.document_changes { + match document_changes { + lsp::DocumentChanges::Edits(edits) => { + operations.extend(edits.into_iter().map(lsp::DocumentChangeOperation::Edit)) + } + lsp::DocumentChanges::Operations(ops) => operations = ops, + } + } else if let Some(changes) = edit.changes { + operations.extend(changes.into_iter().map(|(uri, edits)| { + lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { + text_document: lsp::OptionalVersionedTextDocumentIdentifier { + uri, + version: None, + }, + edits: edits.into_iter().map(lsp::OneOf::Left).collect(), + }) + })); + } + + let mut project_transaction = ProjectTransaction::default(); + for operation in operations { + match operation { + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(op)) => { + let abs_path = op + .uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + + if let Some(parent_path) = abs_path.parent() { + fs.create_dir(parent_path).await?; + } + if abs_path.ends_with("/") { + fs.create_dir(&abs_path).await?; + } else { + fs.create_file(&abs_path, op.options.map(Into::into).unwrap_or_default()) + .await?; + } + } + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Rename(op)) => { + let source_abs_path = op + .old_uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + let target_abs_path = op + .new_uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + fs.rename( + &source_abs_path, + &target_abs_path, + op.options.map(Into::into).unwrap_or_default(), + ) + .await?; + } + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Delete(op)) => { + let abs_path = op + .uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + let options = op.options.map(Into::into).unwrap_or_default(); + if abs_path.ends_with("/") { + fs.remove_dir(&abs_path, options).await?; + } else { + fs.remove_file(&abs_path, options).await?; + } + } + lsp::DocumentChangeOperation::Edit(op) => { + let buffer_to_edit = this + .update(cx, |this, cx| { + this.open_local_buffer_from_lsp_path( + op.text_document.uri, + language_name.clone(), + language_server.clone(), + cx, + ) + }) + .await?; + + let edits = buffer_to_edit + .update(cx, |buffer, cx| { + let edits = op.edits.into_iter().map(|edit| match edit { + lsp::OneOf::Left(edit) => edit, + lsp::OneOf::Right(edit) => edit.text_edit, + }); + buffer.edits_from_lsp(edits, op.text_document.version, cx) + }) + .await?; + + let transaction = buffer_to_edit.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(); + buffer.start_transaction(); + for (range, text) in edits { + buffer.edit([range], text, cx); + } + 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 + }; + + transaction + }); + if let Some(transaction) = transaction { + project_transaction.0.insert(buffer_to_edit, transaction); + } + } + } + } + + Ok(project_transaction) + } + + pub fn prepare_rename( + &self, + buffer: ModelHandle, + position: T, + cx: &mut ModelContext, + ) -> Task>>> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp(buffer, PrepareRename { position }, cx) + } + + pub fn perform_rename( + &self, + buffer: ModelHandle, + position: T, + new_name: String, + push_to_history: bool, + cx: &mut ModelContext, + ) -> Task> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp( + buffer, + PerformRename { + position, + new_name, + push_to_history, + }, + cx, + ) + } + + fn request_lsp( + &self, + buffer_handle: ModelHandle, + request: R, + cx: &mut ModelContext, + ) -> Task> + where + ::Result: Send, + { + let buffer = buffer_handle.read(cx); + if self.is_local() { + let file = File::from_dyn(buffer.file()).and_then(File::as_local); + if let Some((file, language_server)) = file.zip(buffer.language_server().cloned()) { + let lsp_params = request.to_lsp(&file.abs_path(cx), cx); + return cx.spawn(|this, cx| async move { + let response = language_server + .request::(lsp_params) + .await + .context("lsp request failed")?; + request + .response_from_lsp(response, this, buffer_handle, cx) + .await + }); + } + } else if let Some(project_id) = self.remote_id() { + let rpc = self.client.clone(); + let message = request.to_proto(project_id, buffer); + return cx.spawn(|this, cx| async move { + let response = rpc.request(message).await?; + request + .response_from_proto(response, this, buffer_handle, cx) + .await + }); + } + Task::ready(Ok(Default::default())) + } + pub fn find_or_create_local_worktree( &self, abs_path: impl AsRef, @@ -2489,47 +2451,37 @@ impl Project { }) } - async fn handle_get_definition( + async fn handle_lsp_command( this: ModelHandle, - envelope: TypedEnvelope, + envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, - ) -> Result { + ) -> Result<::Response> + where + ::Result: Send, + { let sender_id = envelope.original_sender_id()?; - let position = envelope - .payload - .position - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("invalid position"))?; - let definitions = this.update(&mut cx, |this, cx| { - let source_buffer = this + let (request, buffer_version) = this.update(&mut cx, |this, cx| { + let buffer_id = T::buffer_id_from_proto(&envelope.payload); + let buffer_handle = this .shared_buffers .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) - .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; - if source_buffer.read(cx).can_resolve(&position) { - Ok(this.definition(&source_buffer, position, cx)) - } else { - Err(anyhow!("cannot resolve position")) - } + .and_then(|shared_buffers| shared_buffers.get(&buffer_id).cloned()) + .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?; + let buffer = buffer_handle.read(cx); + let buffer_version = buffer.version(); + let request = T::from_proto(envelope.payload, this, buffer)?; + Ok::<_, anyhow::Error>((this.request_lsp(buffer_handle, request, cx), buffer_version)) })?; - - let definitions = definitions.await?; - + let response = request.await?; this.update(&mut cx, |this, cx| { - let mut response = proto::GetDefinitionResponse { - definitions: Default::default(), - }; - for definition in definitions { - let buffer = - this.serialize_buffer_for_peer(&definition.target_buffer, sender_id, cx); - response.definitions.push(proto::Definition { - target_start: Some(serialize_anchor(&definition.target_range.start)), - target_end: Some(serialize_anchor(&definition.target_range.end)), - buffer: Some(buffer), - }); - } - Ok(response) + Ok(T::response_to_proto( + response, + this, + sender_id, + &buffer_version, + cx, + )) }) } @@ -2980,13 +2932,11 @@ impl From for fs::RemoveOptions { #[cfg(test)] mod tests { use super::{Event, *}; - use client::test::FakeHttpClient; use fs::RealFs; use futures::StreamExt; use gpui::test::subscribe; use language::{ - tree_sitter_rust, AnchorRangeExt, Diagnostic, LanguageConfig, LanguageRegistry, - LanguageServerConfig, Point, + tree_sitter_rust, AnchorRangeExt, Diagnostic, LanguageConfig, LanguageServerConfig, Point, }; use lsp::Url; use serde_json::json; @@ -3066,8 +3016,7 @@ mod tests { .clone() .unwrap(); - let mut languages = LanguageRegistry::new(); - languages.add(Arc::new(Language::new( + let language = Arc::new(Language::new( LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], @@ -3075,30 +3024,26 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )); - let dir = temp_tree(json!({ - "a.rs": "fn a() { A }", - "b.rs": "const y: i32 = 1", - })); + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": "fn a() { A }", + "b.rs": "const y: i32 = 1", + }), + ) + .await; - let http_client = FakeHttpClient::with_404_response(); - let client = Client::new(http_client.clone()); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - - let project = cx.update(|cx| { - Project::local( - client, - user_store, - Arc::new(languages), - Arc::new(RealFs), - cx, - ) + let project = Project::test(fs, &mut cx); + project.update(&mut cx, |project, _| { + Arc::get_mut(&mut project.languages).unwrap().add(language); }); let (tree, _) = project .update(&mut cx, |project, cx| { - project.find_or_create_local_worktree(dir.path(), false, cx) + project.find_or_create_local_worktree("/dir", false, cx) }) .await .unwrap(); @@ -3110,13 +3055,7 @@ mod tests { // Cause worktree to start the fake language server let _buffer = project .update(&mut cx, |project, cx| { - project.open_buffer( - ProjectPath { - worktree_id, - path: Path::new("b.rs").into(), - }, - cx, - ) + project.open_buffer((worktree_id, Path::new("b.rs")), cx) }) .await .unwrap(); @@ -3136,7 +3075,7 @@ mod tests { fake_server .notify::(lsp::PublishDiagnosticsParams { - uri: Url::from_file_path(dir.path().join("a.rs")).unwrap(), + uri: Url::from_file_path("/dir/a.rs").unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), @@ -3148,10 +3087,7 @@ mod tests { .await; assert_eq!( events.next().await.unwrap(), - Event::DiagnosticsUpdated(ProjectPath { - worktree_id, - path: Arc::from(Path::new("a.rs")) - }) + Event::DiagnosticsUpdated((worktree_id, Path::new("a.rs")).into()) ); fake_server.end_progress(&progress_token).await; @@ -3226,9 +3162,7 @@ mod tests { #[gpui::test] async fn test_definition(mut cx: gpui::TestAppContext) { let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - - let mut languages = LanguageRegistry::new(); - languages.add(Arc::new(Language::new( + let language = Arc::new(Language::new( LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], @@ -3236,30 +3170,26 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )); - let dir = temp_tree(json!({ - "a.rs": "const fn a() { A }", - "b.rs": "const y: i32 = crate::a()", - })); - let dir_path = dir.path().to_path_buf(); + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": "const fn a() { A }", + "b.rs": "const y: i32 = crate::a()", + }), + ) + .await; - let http_client = FakeHttpClient::with_404_response(); - let client = Client::new(http_client.clone()); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project = cx.update(|cx| { - Project::local( - client, - user_store, - Arc::new(languages), - Arc::new(RealFs), - cx, - ) + let project = Project::test(fs, &mut cx); + project.update(&mut cx, |project, _| { + Arc::get_mut(&mut project.languages).unwrap().add(language); }); let (tree, _) = project .update(&mut cx, |project, cx| { - project.find_or_create_local_worktree(dir.path().join("b.rs"), false, cx) + project.find_or_create_local_worktree("/dir/b.rs", false, cx) }) .await .unwrap(); @@ -3285,12 +3215,12 @@ mod tests { let params = params.text_document_position_params; assert_eq!( params.text_document.uri.to_file_path().unwrap(), - dir_path.join("b.rs") + Path::new("/dir/b.rs"), ); assert_eq!(params.position, lsp::Position::new(0, 22)); Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path(dir_path.join("a.rs")).unwrap(), + lsp::Url::from_file_path("/dir/a.rs").unwrap(), lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), ))) }); @@ -3311,15 +3241,12 @@ mod tests { .as_local() .unwrap() .abs_path(cx), - dir.path().join("a.rs") + Path::new("/dir/a.rs"), ); assert_eq!(definition.target_range.to_offset(target_buffer), 9..10); assert_eq!( list_worktrees(&project, cx), - [ - (dir.path().join("b.rs"), false), - (dir.path().join("a.rs"), true) - ] + [("/dir/b.rs".as_ref(), false), ("/dir/a.rs".as_ref(), true)] ); drop(definition); @@ -3327,18 +3254,21 @@ mod tests { cx.read(|cx| { assert_eq!( list_worktrees(&project, cx), - [(dir.path().join("b.rs"), false)] + [("/dir/b.rs".as_ref(), false)] ); }); - fn list_worktrees(project: &ModelHandle, cx: &AppContext) -> Vec<(PathBuf, bool)> { + fn list_worktrees<'a>( + project: &'a ModelHandle, + cx: &'a AppContext, + ) -> Vec<(&'a Path, bool)> { project .read(cx) .worktrees(cx) .map(|worktree| { let worktree = worktree.read(cx); ( - worktree.as_local().unwrap().abs_path().to_path_buf(), + worktree.as_local().unwrap().abs_path().as_ref(), worktree.is_weak(), ) }) @@ -3348,7 +3278,7 @@ mod tests { #[gpui::test] async fn test_save_file(mut cx: gpui::TestAppContext) { - let fs = Arc::new(FakeFs::new(cx.background())); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", json!({ @@ -3386,7 +3316,7 @@ mod tests { #[gpui::test] async fn test_save_in_single_file_worktree(mut cx: gpui::TestAppContext) { - let fs = Arc::new(FakeFs::new(cx.background())); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", json!({ @@ -3576,7 +3506,7 @@ mod tests { #[gpui::test] async fn test_buffer_deduping(mut cx: gpui::TestAppContext) { - let fs = Arc::new(FakeFs::new(cx.background())); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/the-dir", json!({ @@ -3865,7 +3795,7 @@ mod tests { #[gpui::test] async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) { - let fs = Arc::new(FakeFs::new(cx.background())); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/the-dir", json!({ @@ -4121,4 +4051,146 @@ mod tests { ] ); } + + #[gpui::test] + async fn test_rename(mut cx: gpui::TestAppContext) { + let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); + let language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;" + }), + ) + .await; + + let project = Project::test(fs.clone(), &mut cx); + project.update(&mut cx, |project, _| { + Arc::get_mut(&mut project.languages).unwrap().add(language); + }); + + let (tree, _) = project + .update(&mut cx, |project, cx| { + project.find_or_create_local_worktree("/dir", false, cx) + }) + .await + .unwrap(); + let worktree_id = tree.read_with(&cx, |tree, _| tree.id()); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + let buffer = project + .update(&mut cx, |project, cx| { + project.open_buffer((worktree_id, Path::new("one.rs")), cx) + }) + .await + .unwrap(); + + let mut fake_server = fake_servers.next().await.unwrap(); + + let response = project.update(&mut cx, |project, cx| { + project.prepare_rename(buffer.clone(), 7, cx) + }); + fake_server + .handle_request::(|params| { + assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); + assert_eq!(params.position, lsp::Position::new(0, 7)); + Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + lsp::Position::new(0, 6), + lsp::Position::new(0, 9), + ))) + }) + .next() + .await + .unwrap(); + let range = response.await.unwrap().unwrap(); + let range = buffer.read_with(&cx, |buffer, _| range.to_offset(buffer)); + assert_eq!(range, 6..9); + + let response = project.update(&mut cx, |project, cx| { + project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx) + }); + fake_server + .handle_request::(|params| { + assert_eq!( + params.text_document_position.text_document.uri.as_str(), + "file:///dir/one.rs" + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 7) + ); + assert_eq!(params.new_name, "THREE"); + Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/dir/one.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 6), + lsp::Position::new(0, 9), + ), + "THREE".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/dir/two.rs").unwrap(), + vec![ + lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 24), + lsp::Position::new(0, 27), + ), + "THREE".to_string(), + ), + lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 35), + lsp::Position::new(0, 38), + ), + "THREE".to_string(), + ), + ], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + }) + }) + .next() + .await + .unwrap(); + let mut transaction = response.await.unwrap().0; + assert_eq!(transaction.len(), 2); + assert_eq!( + transaction + .remove_entry(&buffer) + .unwrap() + .0 + .read_with(&cx, |buffer, _| buffer.text()), + "const THREE: usize = 1;" + ); + assert_eq!( + transaction + .into_keys() + .next() + .unwrap() + .read_with(&cx, |buffer, _| buffer.text()), + "const TWO: usize = one::THREE + one::THREE;" + ); + } } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 074781449d..89b93a0996 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2482,7 +2482,7 @@ mod tests { client, Arc::from(Path::new("/root")), false, - Arc::new(fs), + fs, &mut cx.to_async(), ) .await diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 9d7baa8992..65622e70d4 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -50,6 +50,10 @@ message Envelope { GetCodeActionsResponse get_code_actions_response = 42; ApplyCodeAction apply_code_action = 43; ApplyCodeActionResponse apply_code_action_response = 44; + PrepareRename prepare_rename = 58; + PrepareRenameResponse prepare_rename_response = 59; + PerformRename perform_rename = 60; + PerformRenameResponse perform_rename_response = 61; GetChannels get_channels = 45; GetChannelsResponse get_channels_response = 46; @@ -274,6 +278,30 @@ message ApplyCodeActionResponse { ProjectTransaction transaction = 1; } +message PrepareRename { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; +} + +message PrepareRenameResponse { + bool can_rename = 1; + Anchor start = 2; + Anchor end = 3; + repeated VectorClockEntry version = 4; +} + +message PerformRename { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + string new_name = 4; +} + +message PerformRenameResponse { + ProjectTransaction transaction = 2; +} + message CodeAction { Anchor start = 1; Anchor end = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 8093f2551f..fa8b2f692c 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -167,6 +167,10 @@ messages!( (LeaveProject, Foreground), (OpenBuffer, Foreground), (OpenBufferResponse, Foreground), + (PerformRename, Background), + (PerformRenameResponse, Background), + (PrepareRename, Background), + (PrepareRenameResponse, Background), (RegisterProjectResponse, Foreground), (Ping, Foreground), (RegisterProject, Foreground), @@ -205,6 +209,8 @@ request_messages!( (JoinProject, JoinProjectResponse), (OpenBuffer, OpenBufferResponse), (Ping, Ack), + (PerformRename, PerformRenameResponse), + (PrepareRename, PrepareRenameResponse), (RegisterProject, RegisterProjectResponse), (RegisterWorktree, Ack), (SaveBuffer, BufferSaved), @@ -233,6 +239,8 @@ entity_messages!( JoinProject, LeaveProject, OpenBuffer, + PerformRename, + PrepareRename, RemoveProjectCollaborator, SaveBuffer, ShareWorktree, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index adb0592df5..c6a0ef2be6 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -91,6 +91,8 @@ impl Server { .add_request_handler(Server::apply_additional_edits_for_completion) .add_request_handler(Server::get_code_actions) .add_request_handler(Server::apply_code_action) + .add_request_handler(Server::prepare_rename) + .add_request_handler(Server::perform_rename) .add_request_handler(Server::get_channels) .add_request_handler(Server::get_users) .add_request_handler(Server::join_channel) @@ -708,6 +710,34 @@ impl Server { .await?) } + async fn prepare_rename( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result { + let host = self + .state() + .read_project(request.payload.project_id, request.sender_id)? + .host_connection_id; + Ok(self + .peer + .forward_request(request.sender_id, host, request.payload.clone()) + .await?) + } + + async fn perform_rename( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result { + let host = self + .state() + .read_project(request.payload.project_id, request.sender_id)? + .host_connection_id; + Ok(self + .peer + .forward_request(request.sender_id, host, request.payload.clone()) + .await?) + } + async fn update_buffer( self: Arc, request: TypedEnvelope, @@ -1122,8 +1152,8 @@ mod tests { EstablishConnectionError, UserStore, }, editor::{ - self, ConfirmCodeAction, ConfirmCompletion, Editor, EditorSettings, Input, MultiBuffer, - Redo, ToggleCodeActions, Undo, + self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, EditorSettings, + Input, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions, Undo, }, fs::{FakeFs, Fs as _}, language::{ @@ -1147,7 +1177,7 @@ mod tests { async fn test_share_project(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { let (window_b, _) = cx_b.add_window(|_| EmptyView); let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); cx_a.foreground().forbid_parking(); // Connect to a server as 2 clients. @@ -1285,7 +1315,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_unshare_project(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); cx_a.foreground().forbid_parking(); // Connect to a server as 2 clients. @@ -1386,7 +1416,7 @@ mod tests { mut cx_c: TestAppContext, ) { let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); cx_a.foreground().forbid_parking(); // Connect to a server as 3 clients. @@ -1514,9 +1544,7 @@ mod tests { fs.rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default()) .await .unwrap(); - fs.insert_file(Path::new("/a/file4"), "4".into()) - .await - .unwrap(); + fs.insert_file(Path::new("/a/file4"), "4".into()).await; worktree_a .condition(&cx_a, |tree, _| { @@ -1565,7 +1593,7 @@ mod tests { async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -1653,7 +1681,7 @@ mod tests { async fn test_buffer_reloading(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -1738,7 +1766,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -1820,7 +1848,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -1895,7 +1923,7 @@ mod tests { async fn test_peer_disconnection(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -1969,7 +1997,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); @@ -2193,7 +2221,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. let (mut language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); @@ -2402,7 +2430,7 @@ mod tests { async fn test_formatting_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); @@ -2504,7 +2532,7 @@ mod tests { async fn test_definition(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/root-1", json!({ @@ -2657,7 +2685,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/root", json!({ @@ -2766,7 +2794,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); let mut path_openers_b = Vec::new(); cx_b.update(|cx| editor::init(cx, &mut path_openers_b)); @@ -3001,6 +3029,223 @@ mod tests { }); } + #[gpui::test(iterations = 10)] + async fn test_collaborating_with_renames(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + cx_a.foreground().forbid_parking(); + let mut lang_registry = Arc::new(LanguageRegistry::new()); + let fs = FakeFs::new(cx_a.background()); + let mut path_openers_b = Vec::new(); + cx_b.update(|cx| editor::init(cx, &mut path_openers_b)); + + // Set up a fake language server. + let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); + Arc::get_mut(&mut lang_registry) + .unwrap() + .add(Arc::new(Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ))); + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + + // Share a project as client A + fs.insert_tree( + "/dir", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;" + }), + ) + .await; + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_local_worktree("/dir", false, cx) + }) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let project_id = project_a.update(&mut cx_a, |p, _| p.next_remote_id()).await; + let worktree_id = worktree_a.read_with(&cx_a, |tree, _| tree.id()); + project_a + .update(&mut cx_a, |p, cx| p.share(cx)) + .await + .unwrap(); + + // Join the worktree as client B. + let project_b = Project::remote( + project_id, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + let mut params = cx_b.update(WorkspaceParams::test); + params.languages = lang_registry.clone(); + params.client = client_b.client.clone(); + params.user_store = client_b.user_store.clone(); + params.project = project_b; + params.path_openers = path_openers_b.into(); + + let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); + let editor_b = workspace_b + .update(&mut cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "one.rs").into(), cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let mut fake_language_server = fake_language_servers.next().await.unwrap(); + + // Move cursor to a location that can be renamed. + let prepare_rename = editor_b.update(&mut cx_b, |editor, cx| { + editor.select_ranges([7..7], None, cx); + editor.rename(&Rename, cx).unwrap() + }); + + fake_language_server + .handle_request::(|params| { + assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); + assert_eq!(params.position, lsp::Position::new(0, 7)); + Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + lsp::Position::new(0, 6), + lsp::Position::new(0, 9), + ))) + }) + .next() + .await + .unwrap(); + prepare_rename.await.unwrap(); + editor_b.update(&mut cx_b, |editor, cx| { + let rename = editor.pending_rename().unwrap(); + let buffer = editor.buffer().read(cx).snapshot(cx); + assert_eq!( + rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer), + 6..9 + ); + rename.editor.update(cx, |rename_editor, cx| { + rename_editor.buffer().update(cx, |rename_buffer, cx| { + rename_buffer.edit([0..3], "THREE", cx); + }); + }); + }); + + let confirm_rename = workspace_b.update(&mut cx_b, |workspace, cx| { + Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap() + }); + fake_language_server + .handle_request::(|params| { + assert_eq!( + params.text_document_position.text_document.uri.as_str(), + "file:///dir/one.rs" + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 6) + ); + assert_eq!(params.new_name, "THREE"); + Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/dir/one.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 6), + lsp::Position::new(0, 9), + ), + "THREE".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/dir/two.rs").unwrap(), + vec![ + lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 24), + lsp::Position::new(0, 27), + ), + "THREE".to_string(), + ), + lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 35), + lsp::Position::new(0, 38), + ), + "THREE".to_string(), + ), + ], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + }) + }) + .next() + .await + .unwrap(); + confirm_rename.await.unwrap(); + + let rename_editor = workspace_b.read_with(&cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + rename_editor.update(&mut cx_b, |editor, cx| { + assert_eq!( + editor.text(cx), + "const TWO: usize = one::THREE + one::THREE;\nconst THREE: usize = 1;" + ); + editor.undo(&Undo, cx); + assert_eq!( + editor.text(cx), + "const TWO: usize = one::ONE + one::ONE;\nconst ONE: usize = 1;" + ); + editor.redo(&Redo, cx); + assert_eq!( + editor.text(cx), + "const TWO: usize = one::THREE + one::THREE;\nconst THREE: usize = 1;" + ); + }); + + // Ensure temporary rename edits cannot be undone/redone. + editor_b.update(&mut cx_b, |editor, cx| { + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "const ONE: usize = 1;"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "const ONE: usize = 1;"); + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "const THREE: usize = 1;"); + }) + } + #[gpui::test(iterations = 10)] async fn test_basic_chat(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); @@ -3421,7 +3666,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Connect to a server as 3 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3591,6 +3836,13 @@ mod tests { }, )]) }); + + fake_server.handle_request::(|params| { + Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + params.position, + params.position, + ))) + }); }); Arc::get_mut(&mut host_lang_registry) @@ -3605,7 +3857,7 @@ mod tests { None, ))); - let fs = Arc::new(FakeFs::new(cx.background())); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/_collab", json!({ @@ -4223,6 +4475,26 @@ mod tests { save.await; } } + 40..=45 => { + let prepare_rename = project.update(&mut cx, |project, cx| { + log::info!( + "Guest {}: preparing rename for buffer {:?}", + guest_id, + buffer.read(cx).file().unwrap().full_path(cx) + ); + let offset = rng.borrow_mut().gen_range(0..=buffer.read(cx).len()); + project.prepare_rename(buffer, offset, cx) + }); + let prepare_rename = cx.background().spawn(async move { + prepare_rename.await.expect("prepare rename request failed"); + }); + if rng.borrow_mut().gen_bool(0.3) { + log::info!("Guest {}: detaching prepare rename request", guest_id); + prepare_rename.detach(); + } else { + prepare_rename.await; + } + } _ => { buffer.update(&mut cx, |buffer, cx| { log::info!( diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index da003b5d44..d92a36dd43 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1222,7 +1222,6 @@ impl Buffer { .iter() .map(|entry| entry.transaction.clone()) .collect::>(); - transactions .into_iter() .map(|transaction| self.undo_or_redo(transaction).unwrap()) @@ -1251,7 +1250,6 @@ impl Buffer { .iter() .map(|entry| entry.transaction.clone()) .collect::>(); - transactions .into_iter() .map(|transaction| self.undo_or_redo(transaction).unwrap()) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index db078cd862..1e63830792 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -278,6 +278,8 @@ pub struct EditorStyle { pub gutter_padding_factor: f32, pub active_line_background: Color, pub highlighted_line_background: Color, + pub diff_background_deleted: Color, + pub diff_background_inserted: Color, pub line_number: Color, pub line_number_active: Color, pub guest_selections: Vec, @@ -383,6 +385,8 @@ impl InputEditorStyle { gutter_padding_factor: Default::default(), active_line_background: Default::default(), highlighted_line_background: Default::default(), + diff_background_deleted: Default::default(), + diff_background_inserted: Default::default(), line_number: Default::default(), line_number_active: Default::default(), guest_selections: Default::default(), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ba27d0daab..ddff6f9bf7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -492,7 +492,7 @@ pub struct WorkspaceParams { impl WorkspaceParams { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut MutableAppContext) -> Self { - let fs = Arc::new(project::FakeFs::new(cx.background().clone())); + let fs = project::FakeFs::new(cx.background().clone()); let languages = Arc::new(LanguageRegistry::new()); let http_client = client::test::FakeHttpClient::new(|_| async move { Ok(client::http::ServerResponse::new(404)) diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index ed4c4c9039..a9c500b640 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -188,7 +188,7 @@ corner_radius = 6 [project_panel] extends = "$panel" -padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 +padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 [project_panel.entry] text = "$text.1" @@ -248,6 +248,8 @@ gutter_background = "$surface.1" gutter_padding_factor = 2.5 active_line_background = "$state.active_line" highlighted_line_background = "$state.highlighted_line" +diff_background_deleted = "$state.deleted_line" +diff_background_inserted = "$state.inserted_line" line_number = "$text.2.color" line_number_active = "$text.0.color" selection = "$selection.host" diff --git a/crates/zed/assets/themes/black.toml b/crates/zed/assets/themes/black.toml index bc9f6c75d6..769076645f 100644 --- a/crates/zed/assets/themes/black.toml +++ b/crates/zed/assets/themes/black.toml @@ -19,7 +19,7 @@ extends = "_base" 0 = "#00000052" [selection] -host = { selection = "#3B57BC33", cursor = "$text.0.color" } +host = { selection = "#3B57BC55", cursor = "$text.0.color" } guests = [ { selection = "#FDF35133", cursor = "#FDF351" }, { selection = "#4EACAD33", cursor = "#4EACAD" }, @@ -39,6 +39,8 @@ bad = "#b7372e" [state] active_line = "#161313" highlighted_line = "#faca5033" +deleted_line = "#dd000036" +inserted_line = "#00dd0036" hover = "#00000033" selected = "#00000088" diff --git a/crates/zed/assets/themes/dark.toml b/crates/zed/assets/themes/dark.toml index acfbf083c0..ed6deed040 100644 --- a/crates/zed/assets/themes/dark.toml +++ b/crates/zed/assets/themes/dark.toml @@ -19,7 +19,7 @@ extends = "_base" 0 = "#00000052" [selection] -host = { selection = "#3B57BC33", cursor = "$text.0.color" } +host = { selection = "#3B57BC55", cursor = "$text.0.color" } guests = [ { selection = "#FDF35133", cursor = "#FDF351" }, { selection = "#4EACAD33", cursor = "#4EACAD" }, @@ -39,6 +39,8 @@ bad = "#b7372e" [state] active_line = "#00000022" highlighted_line = "#faca5033" +deleted_line = "#dd000036" +inserted_line = "#00dd0036" hover = "#00000033" selected = "#00000088" diff --git a/crates/zed/assets/themes/light.toml b/crates/zed/assets/themes/light.toml index cf8ebe34e6..f51b3f4656 100644 --- a/crates/zed/assets/themes/light.toml +++ b/crates/zed/assets/themes/light.toml @@ -19,7 +19,7 @@ extends = "_base" 0 = "#0000000D" [selection] -host = { selection = "#3B57BC33", cursor = "$text.0.color" } +host = { selection = "#3B57BC55", cursor = "$text.0.color" } guests = [ { selection = "#D0453B33", cursor = "#D0453B" }, { selection = "#3B874B33", cursor = "#3B874B" }, @@ -39,6 +39,8 @@ bad = "#b7372e" [state] active_line = "#00000008" highlighted_line = "#faca5033" +deleted_line = "#dd000036" +inserted_line = "#00dd0036" hover = "#0000000D" selected = "#0000001c" diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index 9e12e2601c..18819b25a6 100644 --- a/crates/zed/src/test.rs +++ b/crates/zed/src/test.rs @@ -42,7 +42,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)), client, user_store, - fs: Arc::new(FakeFs::new(cx.background().clone())), + fs: FakeFs::new(cx.background().clone()), path_openers: Arc::from(path_openers), build_window_options: &build_window_options, build_workspace: &build_workspace, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 791c236ec6..4c98e0fa98 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -214,7 +214,7 @@ mod tests { }); let save_task = workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(cx)); - app_state.fs.as_fake().insert_dir("/root").await.unwrap(); + app_state.fs.as_fake().insert_dir("/root").await; cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name"))); save_task.await.unwrap(); editor.read_with(&cx, |editor, cx| { @@ -348,10 +348,10 @@ mod tests { async fn test_open_paths(mut cx: TestAppContext) { let app_state = cx.update(test_app_state); let fs = app_state.fs.as_fake(); - fs.insert_dir("/dir1").await.unwrap(); - fs.insert_dir("/dir2").await.unwrap(); - fs.insert_file("/dir1/a.txt", "".into()).await.unwrap(); - fs.insert_file("/dir2/b.txt", "".into()).await.unwrap(); + fs.insert_dir("/dir1").await; + fs.insert_dir("/dir2").await; + fs.insert_file("/dir1/a.txt", "".into()).await; + fs.insert_file("/dir2/b.txt", "".into()).await; let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); @@ -456,9 +456,7 @@ mod tests { editor.handle_input(&editor::Input("x".into()), cx) }) }); - fs.insert_file("/root/a.txt", "changed".to_string()) - .await - .unwrap(); + fs.insert_file("/root/a.txt", "changed".to_string()).await; editor .condition(&cx, |editor, cx| editor.has_conflict(cx)) .await; @@ -476,7 +474,7 @@ mod tests { #[gpui::test] async fn test_open_and_save_new_file(mut cx: TestAppContext) { let app_state = cx.update(test_app_state); - app_state.fs.as_fake().insert_dir("/root").await.unwrap(); + app_state.fs.as_fake().insert_dir("/root").await; let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); params @@ -576,7 +574,7 @@ mod tests { #[gpui::test] async fn test_setting_language_when_saving_as_single_file_worktree(mut cx: TestAppContext) { let app_state = cx.update(test_app_state); - app_state.fs.as_fake().insert_dir("/root").await.unwrap(); + app_state.fs.as_fake().insert_dir("/root").await; let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));