Start work on renames

This commit is contained in:
Max Brunsfeld 2022-02-17 12:44:14 -08:00
parent 6d8db5f6bb
commit 54d7642712
5 changed files with 683 additions and 185 deletions

View file

@ -0,0 +1,192 @@
use crate::{Project, ProjectTransaction};
use anyhow::{anyhow, Result};
use client::proto;
use futures::{future::LocalBoxFuture, FutureExt};
use gpui::{AppContext, AsyncAppContext, ModelHandle};
use language::{
proto::deserialize_anchor, range_from_lsp, Anchor, Buffer, PointUtf16, ToLspPosition,
};
use std::{ops::Range, path::Path};
pub(crate) trait LspCommand: 'static {
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;
fn to_proto(&self, project_id: u64, cx: &AppContext) -> Self::ProtoRequest;
fn response_from_lsp(
self,
message: <Self::LspRequest as lsp::request::Request>::Result,
project: ModelHandle<Project>,
cx: AsyncAppContext,
) -> LocalBoxFuture<'static, Result<Self::Response>>;
fn response_from_proto(
self,
message: <Self::ProtoRequest as proto::RequestMessage>::Response,
project: ModelHandle<Project>,
cx: AsyncAppContext,
) -> LocalBoxFuture<'static, Result<Self::Response>>;
}
pub(crate) struct PrepareRename {
pub buffer: ModelHandle<Buffer>,
pub position: PointUtf16,
}
#[derive(Debug)]
pub(crate) struct PerformRename {
pub buffer: ModelHandle<Buffer>,
pub position: PointUtf16,
pub new_name: String,
}
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(),
}
}
fn to_proto(&self, project_id: u64, cx: &AppContext) -> proto::PrepareRename {
let buffer_id = self.buffer.read(cx).remote_id();
proto::PrepareRename {
project_id,
buffer_id,
position: None,
}
}
fn response_from_lsp(
self,
message: Option<lsp::PrepareRenameResponse>,
_: ModelHandle<Project>,
cx: AsyncAppContext,
) -> LocalBoxFuture<'static, Result<Option<Range<Anchor>>>> {
async move {
Ok(message.and_then(|result| match result {
lsp::PrepareRenameResponse::Range(range)
| lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. } => {
self.buffer.read_with(&cx, |buffer, _| {
let range = range_from_lsp(range);
Some(buffer.anchor_after(range.start)..buffer.anchor_before(range.end))
})
}
_ => None,
}))
}
.boxed_local()
}
fn response_from_proto(
self,
message: proto::PrepareRenameResponse,
_: ModelHandle<Project>,
_: AsyncAppContext,
) -> LocalBoxFuture<'static, Result<Option<Range<Anchor>>>> {
async move {
if message.can_rename {
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)
}
}
.boxed_local()
}
}
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(),
}
}
fn to_proto(&self, project_id: u64, cx: &AppContext) -> proto::PerformRename {
let buffer_id = self.buffer.read(cx).remote_id();
proto::PerformRename {
project_id,
buffer_id,
position: None,
new_name: self.new_name.clone(),
}
}
fn response_from_lsp(
self,
message: Option<lsp::WorkspaceEdit>,
project: ModelHandle<Project>,
mut cx: AsyncAppContext,
) -> LocalBoxFuture<'static, Result<ProjectTransaction>> {
async move {
if let Some(edit) = message {
let (language_name, language_server) =
self.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,
false,
language_name,
language_server,
&mut cx,
)
.await
} else {
Ok(ProjectTransaction::default())
}
}
.boxed_local()
}
fn response_from_proto(
self,
message: proto::PerformRenameResponse,
project: ModelHandle<Project>,
mut cx: AsyncAppContext,
) -> LocalBoxFuture<'static, Result<ProjectTransaction>> {
async move {
let message = message
.transaction
.ok_or_else(|| anyhow!("missing transaction"))?;
project
.update(&mut cx, |project, cx| {
project.deserialize_project_transaction(message, false, cx)
})
.await
}
.boxed_local()
}
}

View file

@ -1,5 +1,6 @@
pub mod fs;
mod ignore;
mod lsp_command;
pub mod worktree;
use anyhow::{anyhow, Context, Result};
@ -15,11 +16,12 @@ use gpui::{
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::{
@ -1625,7 +1627,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 +1657,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 +1694,194 @@ 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.clone(), PrepareRename { buffer, position }, cx)
}
pub fn perform_rename<T: ToPointUtf16>(
&self,
buffer: ModelHandle<Buffer>,
position: T,
new_name: String,
cx: &mut ModelContext<Self>,
) -> Task<Result<ProjectTransaction>> {
let position = position.to_point_utf16(buffer.read(cx));
self.request_lsp(
buffer.clone(),
PerformRename {
buffer,
position,
new_name,
},
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, cx).await
});
}
} else if let Some(project_id) = self.remote_id() {
let rpc = self.client.clone();
let message = request.to_proto(project_id, cx);
return cx.spawn(|this, cx| async move {
let response = rpc.request(message).await?;
request.response_from_proto(response, this, cx).await
});
}
Task::ready(Ok(Default::default()))
}
pub fn find_or_create_local_worktree(
&self,
abs_path: impl AsRef<Path>,
@ -4099,4 +4181,71 @@ 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);
}
}