Merge pull request #455 from zed-industries/rename

Introduce rename support via `F2`
This commit is contained in:
Antonio Scandurra 2022-02-19 11:07:39 +01:00 committed by GitHub
commit 8913ec6cfd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1604 additions and 490 deletions

View file

@ -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,
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<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {

View file

@ -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,
),
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,
);
}
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::<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);

View file

@ -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)>,
}

View file

@ -58,6 +58,7 @@ pub enum CharKind {
Word,
}
#[derive(Clone)]
struct Transaction {
id: TransactionId,
buffer_transactions: HashMap<usize, text::TransactionId>,

View file

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

View file

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

View file

@ -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(),
})
}));
}
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<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 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::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;"
);
}
}

View file

@ -2482,7 +2482,7 @@ mod tests {
client,
Arc::from(Path::new("/root")),
false,
Arc::new(fs),
fs,
&mut cx.to_async(),
)
.await

View file

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

View file

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

View file

@ -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(&params, 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!(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(&params, 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(&params, 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(&params, cx));