mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-09 21:26:14 +03:00
Start work on renames
This commit is contained in:
parent
6d8db5f6bb
commit
54d7642712
@ -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 {
|
||||
@ -2166,20 +2173,31 @@ 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
|
||||
}))
|
||||
}
|
||||
|
||||
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 = project_transaction.0.iter();
|
||||
let mut entries = transaction.0.iter();
|
||||
if let Some((buffer, transaction)) = entries.next() {
|
||||
if entries.next().is_none() {
|
||||
let excerpt = editor.read_with(&cx, |editor, cx| {
|
||||
let excerpt = this.read_with(&cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
@ -2192,8 +2210,7 @@ impl Editor {
|
||||
if snapshot
|
||||
.edited_ranges_for_transaction(transaction)
|
||||
.all(|range| {
|
||||
excerpt_range.start <= range.start
|
||||
&& excerpt_range.end >= range.end
|
||||
excerpt_range.start <= range.start && excerpt_range.end >= range.end
|
||||
})
|
||||
{
|
||||
return Ok(());
|
||||
@ -2206,7 +2223,7 @@ impl Editor {
|
||||
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 {
|
||||
for (buffer, transaction) in &transaction.0 {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
ranges_to_highlight.extend(
|
||||
multibuffer.push_excerpts_with_context_lines(
|
||||
@ -2219,7 +2236,7 @@ impl Editor {
|
||||
),
|
||||
);
|
||||
}
|
||||
multibuffer.push_transaction(&project_transaction.0);
|
||||
multibuffer.push_transaction(&transaction.0);
|
||||
multibuffer
|
||||
});
|
||||
|
||||
@ -2238,7 +2255,6 @@ impl Editor {
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
|
||||
@ -4072,6 +4088,105 @@ impl Editor {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
|
||||
use language::ToOffset as _;
|
||||
|
||||
let project = self.project.clone()?;
|
||||
let position = self.newest_anchor_selection().head();
|
||||
let (buffer, buffer_position) = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.text_anchor_for_position(position.clone(), cx)?;
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let prepare_rename = project.update(cx, |project, cx| {
|
||||
project.prepare_rename(buffer.clone(), buffer_position.to_offset(&snapshot), cx)
|
||||
});
|
||||
|
||||
Some(cx.spawn(|this, mut cx| async move {
|
||||
if let Some(range) = prepare_rename.await? {
|
||||
let buffer_offset_range = range.to_offset(&snapshot);
|
||||
let buffer_offset = buffer_position.to_offset(&snapshot);
|
||||
let lookbehind = buffer_offset.saturating_sub(buffer_offset_range.start);
|
||||
let lookahead = buffer_offset_range.end.saturating_sub(buffer_offset);
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let buffer = this.buffer.read(cx).read(cx);
|
||||
let offset = position.to_offset(&buffer);
|
||||
let start = offset - lookbehind;
|
||||
let end = offset + lookahead;
|
||||
let highlight_range = buffer.anchor_before(start)..buffer.anchor_after(end);
|
||||
drop(buffer);
|
||||
|
||||
this.select_ranges([start..end], None, cx);
|
||||
this.highlight_ranges::<Rename>(vec![highlight_range], Color::red(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
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, position, new_name) = editor.update(cx, |editor, cx| {
|
||||
let range = editor.take_rename_range(cx)?;
|
||||
let multibuffer = editor.buffer.read(cx);
|
||||
let (buffer, position) =
|
||||
multibuffer.text_anchor_for_position(range.start.clone(), cx)?;
|
||||
let snapshot = multibuffer.read(cx);
|
||||
let new_name = snapshot.text_for_range(range.clone()).collect::<String>();
|
||||
Some((buffer, position, new_name))
|
||||
})?;
|
||||
|
||||
let rename = workspace.project().clone().update(cx, |project, cx| {
|
||||
project.perform_rename(buffer, position, new_name.clone(), cx)
|
||||
});
|
||||
|
||||
Some(cx.spawn(|workspace, cx| async move {
|
||||
let project_transaction = rename.await?;
|
||||
Self::open_project_transaction(
|
||||
editor,
|
||||
workspace,
|
||||
project_transaction,
|
||||
format!("Rename: {}", new_name),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
}
|
||||
|
||||
fn rename_range(&self) -> Option<&Range<Anchor>> {
|
||||
self.highlighted_ranges_for_type::<Rename>()
|
||||
.and_then(|(_, range)| range.last())
|
||||
}
|
||||
|
||||
fn take_rename_range(&mut self, cx: &mut ViewContext<Self>) -> Option<Range<Anchor>> {
|
||||
self.clear_highlighted_ranges::<Rename>(cx)
|
||||
.and_then(|(_, mut ranges)| ranges.pop())
|
||||
}
|
||||
|
||||
fn invalidate_rename_range(
|
||||
&mut self,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(range) = &self.rename_range() {
|
||||
if self.selections.len() == 1 {
|
||||
let head = self.selections[0].head().to_offset(&buffer);
|
||||
if range.start.to_offset(&buffer) <= head && range.end.to_offset(&buffer) >= head {
|
||||
return;
|
||||
}
|
||||
}
|
||||
eprintln!("clearing highlight range");
|
||||
self.clear_highlighted_ranges::<Rename>(cx);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@ -4484,6 +4599,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();
|
||||
|
||||
@ -4759,9 +4875,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")]
|
||||
@ -5091,6 +5210,9 @@ impl View for Editor {
|
||||
EditorMode::Full => "full",
|
||||
};
|
||||
cx.map.insert("mode".into(), mode.into());
|
||||
if self.rename_range().is_some() {
|
||||
cx.set.insert("renaming".into());
|
||||
}
|
||||
match self.context_menu.as_ref() {
|
||||
Some(ContextMenu::Completions(_)) => {
|
||||
cx.set.insert("showing_completions".into());
|
||||
|
192
crates/project/src/lsp_command.rs
Normal file
192
crates/project/src/lsp_command.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -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,12 +1657,58 @@ impl Project {
|
||||
.lsp_action;
|
||||
}
|
||||
|
||||
let mut operations = Vec::new();
|
||||
if let Some(edit) = action.lsp_action.edit {
|
||||
Self::deserialize_workspace_edit(
|
||||
this,
|
||||
edit,
|
||||
push_to_history,
|
||||
lang_name,
|
||||
lang_server,
|
||||
&mut cx,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Ok(ProjectTransaction::default())
|
||||
}
|
||||
})
|
||||
} else if let Some(project_id) = self.remote_id() {
|
||||
let client = self.client.clone();
|
||||
let request = proto::ApplyCodeAction {
|
||||
project_id,
|
||||
buffer_id: buffer_handle.read(cx).remote_id(),
|
||||
action: Some(language::proto::serialize_code_action(&action)),
|
||||
};
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let response = client
|
||||
.request(request)
|
||||
.await?
|
||||
.transaction
|
||||
.ok_or_else(|| anyhow!("missing transaction"))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.deserialize_project_transaction(response, push_to_history, cx)
|
||||
})
|
||||
.await
|
||||
})
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("project does not have a remote id")))
|
||||
}
|
||||
}
|
||||
|
||||
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::Edits(edits) => {
|
||||
operations.extend(edits.into_iter().map(lsp::DocumentChangeOperation::Edit))
|
||||
}
|
||||
lsp::DocumentChanges::Operations(ops) => operations = ops,
|
||||
}
|
||||
} else if let Some(changes) = edit.changes {
|
||||
@ -1675,7 +1722,6 @@ impl Project {
|
||||
})
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let mut project_transaction = ProjectTransaction::default();
|
||||
for operation in operations {
|
||||
@ -1692,10 +1738,7 @@ impl Project {
|
||||
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(),
|
||||
)
|
||||
fs.create_file(&abs_path, op.options.map(Into::into).unwrap_or_default())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
@ -1729,18 +1772,18 @@ impl Project {
|
||||
}
|
||||
lsp::DocumentChangeOperation::Edit(op) => {
|
||||
let buffer_to_edit = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
.update(cx, |this, cx| {
|
||||
this.open_local_buffer_from_lsp_path(
|
||||
op.text_document.uri,
|
||||
lang_name.clone(),
|
||||
lang_server.clone(),
|
||||
language_name.clone(),
|
||||
language_server.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let edits = buffer_to_edit
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
.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,
|
||||
@ -1749,15 +1792,14 @@ impl Project {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let transaction = buffer_to_edit.update(&mut cx, |buffer, cx| {
|
||||
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();
|
||||
let transaction = buffer.finalize_last_transaction().unwrap().clone();
|
||||
if !push_to_history {
|
||||
buffer.forget_transaction(transaction.id);
|
||||
}
|
||||
@ -1776,28 +1818,68 @@ impl Project {
|
||||
}
|
||||
|
||||
Ok(project_transaction)
|
||||
})
|
||||
} else if let Some(project_id) = self.remote_id() {
|
||||
let client = self.client.clone();
|
||||
let request = proto::ApplyCodeAction {
|
||||
project_id,
|
||||
buffer_id: buffer_handle.read(cx).remote_id(),
|
||||
action: Some(language::proto::serialize_code_action(&action)),
|
||||
};
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let response = client
|
||||
.request(request)
|
||||
.await?
|
||||
.transaction
|
||||
.ok_or_else(|| anyhow!("missing transaction"))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.deserialize_project_transaction(response, push_to_history, cx)
|
||||
})
|
||||
.await
|
||||
})
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("project does not have a remote id")))
|
||||
}
|
||||
|
||||
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(
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,10 @@ message Envelope {
|
||||
GetCodeActionsResponse get_code_actions_response = 42;
|
||||
ApplyCodeAction apply_code_action = 43;
|
||||
ApplyCodeActionResponse apply_code_action_response = 44;
|
||||
PrepareRename prepare_rename = 58;
|
||||
PrepareRenameResponse prepare_rename_response = 59;
|
||||
PerformRename perform_rename = 60;
|
||||
PerformRenameResponse perform_rename_response = 61;
|
||||
|
||||
GetChannels get_channels = 45;
|
||||
GetChannelsResponse get_channels_response = 46;
|
||||
@ -274,6 +278,30 @@ message ApplyCodeActionResponse {
|
||||
ProjectTransaction transaction = 1;
|
||||
}
|
||||
|
||||
message PrepareRename {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
Anchor position = 3;
|
||||
}
|
||||
|
||||
message PrepareRenameResponse {
|
||||
bool can_rename = 1;
|
||||
Anchor start = 2;
|
||||
Anchor end = 3;
|
||||
repeated VectorClockEntry version = 4;
|
||||
}
|
||||
|
||||
message PerformRename {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
Anchor position = 3;
|
||||
string new_name = 4;
|
||||
}
|
||||
|
||||
message PerformRenameResponse {
|
||||
ProjectTransaction transaction = 2;
|
||||
}
|
||||
|
||||
message CodeAction {
|
||||
Anchor start = 1;
|
||||
Anchor end = 2;
|
||||
|
@ -167,6 +167,10 @@ messages!(
|
||||
(LeaveProject, Foreground),
|
||||
(OpenBuffer, Foreground),
|
||||
(OpenBufferResponse, Foreground),
|
||||
(PerformRename, Background),
|
||||
(PerformRenameResponse, Background),
|
||||
(PrepareRename, Background),
|
||||
(PrepareRenameResponse, Background),
|
||||
(RegisterProjectResponse, Foreground),
|
||||
(Ping, Foreground),
|
||||
(RegisterProject, Foreground),
|
||||
@ -205,6 +209,8 @@ request_messages!(
|
||||
(JoinProject, JoinProjectResponse),
|
||||
(OpenBuffer, OpenBufferResponse),
|
||||
(Ping, Ack),
|
||||
(PerformRename, PerformRenameResponse),
|
||||
(PrepareRename, PrepareRenameResponse),
|
||||
(RegisterProject, RegisterProjectResponse),
|
||||
(RegisterWorktree, Ack),
|
||||
(SaveBuffer, BufferSaved),
|
||||
@ -233,6 +239,7 @@ entity_messages!(
|
||||
JoinProject,
|
||||
LeaveProject,
|
||||
OpenBuffer,
|
||||
PrepareRename,
|
||||
RemoveProjectCollaborator,
|
||||
SaveBuffer,
|
||||
ShareWorktree,
|
||||
|
Loading…
Reference in New Issue
Block a user