Detailed changes
@@ -817,19 +817,28 @@ impl Client {
self.peer.send(self.connection_id()?, message)
}
- pub async fn request<T: RequestMessage>(&self, request: T) -> Result<T::Response> {
+ pub fn request<T: RequestMessage>(
+ &self,
+ request: T,
+ ) -> impl Future<Output = Result<T::Response>> {
+ let client_id = self.id;
log::debug!(
"rpc request start. client_id: {}. name:{}",
- self.id,
- T::NAME
- );
- let response = self.peer.request(self.connection_id()?, request).await;
- log::debug!(
- "rpc request finish. client_id: {}. name:{}",
- self.id,
+ client_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<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {
@@ -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<Box<dyn PathOpene
ConfirmCodeAction(None),
Some("Editor && showing_code_actions"),
),
+ Binding::new("enter", ConfirmRename, Some("Editor && renaming")),
Binding::new("tab", Tab, Some("Editor")),
Binding::new(
"tab",
@@ -243,6 +247,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")),
Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")),
Binding::new("f8", ShowNextDiagnostic, Some("Editor")),
+ Binding::new("f2", Rename, Some("Editor")),
Binding::new("f12", GoToDefinition, Some("Editor")),
Binding::new("ctrl-m", MoveToEnclosingBracket, Some("Editor")),
Binding::new("pageup", PageUp, Some("Editor")),
@@ -319,6 +324,8 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
cx.add_action(Editor::toggle_code_actions);
cx.add_async_action(Editor::confirm_completion);
cx.add_async_action(Editor::confirm_code_action);
+ cx.add_async_action(Editor::rename);
+ cx.add_async_action(Editor::confirm_rename);
}
trait SelectionExt {
@@ -432,6 +439,7 @@ pub struct Editor {
next_completion_id: CompletionId,
available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
code_actions_task: Option<Task<()>>,
+ pending_rename: Option<RenameState>,
}
pub struct EditorSnapshot {
@@ -470,6 +478,13 @@ struct SnippetState {
active_index: usize,
}
+pub struct RenameState {
+ pub range: Range<Anchor>,
+ pub old_name: String,
+ pub editor: ViewHandle<Editor>,
+ block_id: BlockId,
+}
+
struct InvalidationStack<T>(Vec<T>);
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<Self>) {
+ 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<Self>) {
+ 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<Editor>,
+ workspace: ViewHandle<Workspace>,
+ 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::<usize>(transaction)
- .collect(),
- 1,
- cx,
- ),
- );
- }
- multibuffer.push_transaction(&project_transaction.0);
- multibuffer
- });
+ 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::<usize>(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::<Self>(cx) {
- editor.update(cx, |editor, cx| {
- let settings = (editor.build_settings)(cx);
- editor.highlight_ranges::<Self>(
- ranges_to_highlight,
- settings.style.highlighted_line_background,
- cx,
- );
- });
- }
- });
+ workspace.update(&mut cx, |workspace, cx| {
+ let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx);
+ if let Some(editor) = editor.act_as::<Self>(cx) {
+ editor.update(cx, |editor, cx| {
+ let settings = (editor.build_settings)(cx);
+ editor.highlight_ranges::<Self>(
+ ranges_to_highlight,
+ settings.style.highlighted_line_background,
+ cx,
+ );
+ });
+ }
+ });
- Ok(())
- }))
+ Ok(())
}
fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
@@ -3130,6 +3163,10 @@ impl Editor {
}
pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
+ 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>) {
+ 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<Self>) -> Option<Task<Result<()>>> {
+ 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::<String>();
+ 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::<Rename>(
+ vec![Anchor::min()..Anchor::max()],
+ settings.style.diff_background_inserted,
+ cx,
+ );
+ editor
+ });
+ this.highlight_ranges::<Rename>(
+ 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<Workspace>,
+ ) -> Option<Task<Result<()>>> {
+ let editor = workspace.active_item(cx)?.act_as::<Editor>(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<Self>) -> Option<RenameState> {
+ let rename = self.pending_rename.take()?;
+ self.remove_blocks([rename.block_id].into_iter().collect(), cx);
+ self.clear_highlighted_ranges::<Rename>(cx);
+
+ let editor = rename.editor.read(cx);
+ let buffer = editor.buffer.read(cx).snapshot(cx);
+ let selection = editor.newest_selection::<usize>(&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<Self>,
+ ) {
+ 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::<Rename>(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<Editor>) {
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<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
- self.highlighted_ranges.remove(&TypeId::of::<T>());
+ pub fn clear_highlighted_ranges<T: 'static>(
+ &mut self,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<(Color, Vec<Range<Anchor>>)> {
cx.notify();
+ self.highlighted_ranges.remove(&TypeId::of::<T>())
}
#[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);
@@ -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<u32, bool>,
@@ -1135,7 +1144,6 @@ pub struct LayoutState {
em_advance: f32,
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
- text_offset: Vector2F,
context_menu: Option<(DisplayPoint, ElementBox)>,
code_actions_indicator: Option<(u32, ElementBox)>,
}
@@ -58,6 +58,7 @@ pub enum CharKind {
Word,
}
+#[derive(Clone)]
struct Transaction {
id: TransactionId,
buffer_transactions: HashMap<usize, text::TransactionId>,
@@ -268,7 +268,7 @@ pub struct FakeFs {
#[cfg(any(test, feature = "test-support"))]
impl FakeFs {
- pub fn new(executor: std::sync::Arc<gpui::executor::Background>) -> Self {
+ pub fn new(executor: std::sync::Arc<gpui::executor::Background>) -> std::sync::Arc<Self> {
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<Path>) -> Result<()> {
+ pub async fn insert_dir(&self, path: impl AsRef<Path>) {
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<Path>, content: String) -> Result<()> {
+ pub async fn insert_file(&self, path: impl AsRef<Path>, 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");
@@ -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,
+ ) -> <Self::LspRequest as lsp::request::Request>::Params;
+ async fn response_from_lsp(
+ self,
+ message: <Self::LspRequest as lsp::request::Request>::Result,
+ project: ModelHandle<Project>,
+ buffer: ModelHandle<Buffer>,
+ cx: AsyncAppContext,
+ ) -> Result<Self::Response>;
+
+ fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest;
+ fn from_proto(
+ message: Self::ProtoRequest,
+ project: &mut Project,
+ buffer: &Buffer,
+ ) -> Result<Self>;
+ fn response_to_proto(
+ response: Self::Response,
+ project: &mut Project,
+ peer_id: PeerId,
+ buffer_version: &clock::Global,
+ cx: &AppContext,
+ ) -> <Self::ProtoRequest as proto::RequestMessage>::Response;
+ async fn response_from_proto(
+ self,
+ message: <Self::ProtoRequest as proto::RequestMessage>::Response,
+ project: ModelHandle<Project>,
+ buffer: ModelHandle<Buffer>,
+ cx: AsyncAppContext,
+ ) -> Result<Self::Response>;
+ 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<Range<Anchor>>;
+ 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<lsp::PrepareRenameResponse>,
+ _: ModelHandle<Project>,
+ buffer: ModelHandle<Buffer>,
+ cx: AsyncAppContext,
+ ) -> Result<Option<Range<Anchor>>> {
+ 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<Self> {
+ 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<Range<Anchor>>,
+ _: &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<Project>,
+ buffer: ModelHandle<Buffer>,
+ mut cx: AsyncAppContext,
+ ) -> Result<Option<Range<Anchor>>> {
+ 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<lsp::WorkspaceEdit>,
+ project: ModelHandle<Project>,
+ buffer: ModelHandle<Buffer>,
+ mut cx: AsyncAppContext,
+ ) -> Result<ProjectTransaction> {
+ 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<Self> {
+ 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<Project>,
+ _: ModelHandle<Buffer>,
+ mut cx: AsyncAppContext,
+ ) -> Result<ProjectTransaction> {
+ 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<Definition>;
+ 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<lsp::GotoDefinitionResponse>,
+ project: ModelHandle<Project>,
+ buffer: ModelHandle<Buffer>,
+ mut cx: AsyncAppContext,
+ ) -> Result<Vec<Definition>> {
+ 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<Self> {
+ 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<Definition>,
+ 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<Project>,
+ _: ModelHandle<Buffer>,
+ mut cx: AsyncAppContext,
+ ) -> Result<Vec<Definition>> {
+ 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
+ }
+}
@@ -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::<GetDefinition>);
+ client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
+ client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
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<T: ToPointUtf16>(
&self,
- source_buffer_handle: &ModelHandle<Buffer>,
+ buffer: &ModelHandle<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Definition>>> {
- 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::request::GotoDefinition>(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<T: ToPointUtf16>(
@@ -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(),
- })
- }));
- }
- }
-
- 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);
- }
- }
- }
+ Self::deserialize_workspace_edit(
+ this,
+ edit,
+ push_to_history,
+ lang_name,
+ lang_server,
+ &mut cx,
+ )
+ .await
+ } else {
+ Ok(ProjectTransaction::default())
}
-
- 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<Self>,
+ edit: lsp::WorkspaceEdit,
+ push_to_history: bool,
+ language_name: String,
+ language_server: Arc<LanguageServer>,
+ cx: &mut AsyncAppContext,
+ ) -> Result<ProjectTransaction> {
+ 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<T: ToPointUtf16>(
+ &self,
+ buffer: ModelHandle<Buffer>,
+ position: T,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<Option<Range<Anchor>>>> {
+ let position = position.to_point_utf16(buffer.read(cx));
+ self.request_lsp(buffer, PrepareRename { position }, cx)
+ }
+
+ pub fn perform_rename<T: ToPointUtf16>(
+ &self,
+ buffer: ModelHandle<Buffer>,
+ position: T,
+ new_name: String,
+ push_to_history: bool,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<ProjectTransaction>> {
+ let position = position.to_point_utf16(buffer.read(cx));
+ self.request_lsp(
+ buffer,
+ PerformRename {
+ position,
+ new_name,
+ push_to_history,
+ },
+ cx,
+ )
+ }
+
+ fn request_lsp<R: LspCommand>(
+ &self,
+ buffer_handle: ModelHandle<Buffer>,
+ request: R,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<R::Response>>
+ where
+ <R::LspRequest as lsp::request::Request>::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::<R::LspRequest>(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<Path>,
@@ -2489,47 +2451,37 @@ impl Project {
})
}
- async fn handle_get_definition(
+ async fn handle_lsp_command<T: LspCommand>(
this: ModelHandle<Self>,
- envelope: TypedEnvelope<proto::GetDefinition>,
+ envelope: TypedEnvelope<T::ProtoRequest>,
_: Arc<Client>,
mut cx: AsyncAppContext,
- ) -> Result<proto::GetDefinitionResponse> {
+ ) -> Result<<T::ProtoRequest as proto::RequestMessage>::Response>
+ where
+ <T::LspRequest as lsp::request::Request>::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<lsp::DeleteFileOptions> 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 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 fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ "a.rs": "fn a() { A }",
+ "b.rs": "const y: i32 = 1",
+ }),
+ )
+ .await;
- 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::notification::PublishDiagnostics>(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<Project>, cx: &AppContext) -> Vec<(PathBuf, bool)> {
+ fn list_worktrees<'a>(
+ project: &'a ModelHandle<Project>,
+ 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::<lsp::request::PrepareRenameRequest, _>(|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::<lsp::request::Rename, _>(|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;"
+ );
+ }
}
@@ -2482,7 +2482,7 @@ mod tests {
client,
Arc::from(Path::new("/root")),
false,
- Arc::new(fs),
+ fs,
&mut cx.to_async(),
)
.await
@@ -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;
@@ -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,
@@ -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<Server>,
+ request: TypedEnvelope<proto::PrepareRename>,
+ ) -> tide::Result<proto::PrepareRenameResponse> {
+ 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<Server>,
+ request: TypedEnvelope<proto::PerformRename>,
+ ) -> tide::Result<proto::PerformRenameResponse> {
+ 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<Server>,
request: TypedEnvelope<proto::UpdateBuffer>,
@@ -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::<Editor>()
+ .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::<lsp::request::PrepareRenameRequest, _>(|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::<lsp::request::Rename, _>(|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::<Editor>()
+ .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::<lsp::request::PrepareRenameRequest, _>(|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!(
@@ -1222,7 +1222,6 @@ impl Buffer {
.iter()
.map(|entry| entry.transaction.clone())
.collect::<Vec<_>>();
-
transactions
.into_iter()
.map(|transaction| self.undo_or_redo(transaction).unwrap())
@@ -1251,7 +1250,6 @@ impl Buffer {
.iter()
.map(|entry| entry.transaction.clone())
.collect::<Vec<_>>();
-
transactions
.into_iter()
.map(|transaction| self.undo_or_redo(transaction).unwrap())
@@ -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<SelectionStyle>,
@@ -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(),
@@ -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))
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -42,7 +42,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
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,
@@ -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));