Detailed changes
@@ -2123,9 +2123,10 @@ impl Editor {
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
// When buffer contents is updated and caret is moved, try triggering on type formatting.
- if settings::get::<EditorSettings>(cx).use_on_type_format && text.len() == 1 {
- let input_char = text.chars().next().expect("single char input");
- if let Some(on_type_format_task) = this.trigger_on_type_format(input_char, cx) {
+ if settings::get::<EditorSettings>(cx).use_on_type_format {
+ if let Some(on_type_format_task) =
+ this.trigger_on_type_formatting(text.to_string(), cx)
+ {
on_type_format_task.detach_and_log_err(cx);
}
}
@@ -2508,20 +2509,42 @@ impl Editor {
}
}
- fn trigger_on_type_format(
+ fn trigger_on_type_formatting(
&self,
- input: char,
+ input: String,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
+ if input.len() != 1 {
+ return None;
+ }
+
+ let transaction_title = format!("OnTypeFormatting after {input}");
+ let workspace = self.workspace(cx)?;
let project = self.project.as_ref()?;
let position = self.selections.newest_anchor().head();
let (buffer, buffer_position) = self
.buffer
.read(cx)
.text_anchor_for_position(position.clone(), cx)?;
+ let on_type_formatting = project.update(cx, |project, cx| {
+ project.on_type_format(buffer, buffer_position, input, cx)
+ });
+
+ Some(cx.spawn(|editor, mut cx| async move {
+ let project_transaction = on_type_formatting.await?;
+ Self::open_project_transaction(
+ &editor,
+ workspace.downgrade(),
+ project_transaction,
+ transaction_title,
+ cx.clone(),
+ )
+ .await?;
- Some(project.update(cx, |project, cx| {
- project.on_type_format(buffer.clone(), buffer_position, input, cx)
+ editor.update(&mut cx, |editor, cx| {
+ editor.refresh_document_highlights(cx);
+ })?;
+ Ok(())
}))
}
@@ -2,7 +2,7 @@ use crate::{
DocumentHighlight, Hover, HoverBlock, HoverBlockKind, Location, LocationLink, Project,
ProjectTransaction,
};
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Result};
use async_trait::async_trait;
use client::proto::{self, PeerId};
use fs::LineEnding;
@@ -123,6 +123,7 @@ pub(crate) struct OnTypeFormatting {
pub position: PointUtf16,
pub trigger: String,
pub options: FormattingOptions,
+ pub push_to_history: bool,
}
pub(crate) struct FormattingOptions {
@@ -1627,7 +1628,7 @@ impl LspCommand for GetCodeActions {
#[async_trait(?Send)]
impl LspCommand for OnTypeFormatting {
- type Response = Vec<(Range<Anchor>, String)>;
+ type Response = ProjectTransaction;
type LspRequest = lsp::request::OnTypeFormatting;
type ProtoRequest = proto::OnTypeFormatting;
@@ -1667,14 +1668,23 @@ impl LspCommand for OnTypeFormatting {
buffer: ModelHandle<Buffer>,
server_id: LanguageServerId,
mut cx: AsyncAppContext,
- ) -> Result<Vec<(Range<Anchor>, String)>> {
- cx.update(|cx| {
- project.update(cx, |project, cx| {
- project.edits_from_lsp(&buffer, message.into_iter().flatten(), server_id, None, cx)
- })
- })
- .await
- .context("LSP edits conversion")
+ ) -> Result<ProjectTransaction> {
+ if let Some(edits) = message {
+ let (lsp_adapter, lsp_server) =
+ language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
+ Project::deserialize_edits(
+ project,
+ buffer,
+ edits,
+ self.push_to_history,
+ lsp_adapter,
+ lsp_server,
+ &mut cx,
+ )
+ .await
+ } else {
+ Ok(ProjectTransaction::default())
+ }
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::OnTypeFormatting {
@@ -1714,58 +1724,38 @@ impl LspCommand for OnTypeFormatting {
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
trigger: message.trigger.clone(),
options: lsp_formatting_options(tab_size.get()).into(),
+ push_to_history: false,
})
}
fn response_to_proto(
- response: Vec<(Range<Anchor>, String)>,
- _: &mut Project,
- _: PeerId,
- buffer_version: &clock::Global,
- _: &mut AppContext,
+ response: ProjectTransaction,
+ project: &mut Project,
+ peer_id: PeerId,
+ _: &clock::Global,
+ cx: &mut AppContext,
) -> proto::OnTypeFormattingResponse {
+ let transaction = project.serialize_project_transaction_for_peer(response, peer_id, cx);
proto::OnTypeFormattingResponse {
- entries: response
- .into_iter()
- .map(
- |(response_range, new_text)| proto::OnTypeFormattingResponseEntry {
- start: Some(language::proto::serialize_anchor(&response_range.start)),
- end: Some(language::proto::serialize_anchor(&response_range.end)),
- new_text,
- },
- )
- .collect(),
- version: serialize_version(&buffer_version),
+ transaction: Some(transaction),
}
}
async fn response_from_proto(
self,
message: proto::OnTypeFormattingResponse,
- _: ModelHandle<Project>,
- buffer: ModelHandle<Buffer>,
+ project: ModelHandle<Project>,
+ _: ModelHandle<Buffer>,
mut cx: AsyncAppContext,
- ) -> Result<Vec<(Range<Anchor>, String)>> {
- buffer
- .update(&mut cx, |buffer, _| {
- buffer.wait_for_version(deserialize_version(&message.version))
- })
- .await?;
- message
- .entries
- .into_iter()
- .map(|entry| {
- let start = entry
- .start
- .and_then(language::proto::deserialize_anchor)
- .ok_or_else(|| anyhow!("invalid start"))?;
- let end = entry
- .end
- .and_then(language::proto::deserialize_anchor)
- .ok_or_else(|| anyhow!("invalid end"))?;
- Ok((start..end, entry.new_text))
+ ) -> 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)
})
- .collect()
+ .await
}
fn buffer_id_from_proto(message: &proto::OnTypeFormatting) -> u64 {
@@ -417,6 +417,7 @@ impl Project {
client.add_model_request_handler(Self::handle_delete_project_entry);
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
client.add_model_request_handler(Self::handle_apply_code_action);
+ client.add_model_request_handler(Self::handle_on_type_formatting);
client.add_model_request_handler(Self::handle_reload_buffers);
client.add_model_request_handler(Self::handle_synchronize_buffers);
client.add_model_request_handler(Self::handle_format_buffers);
@@ -429,7 +430,6 @@ impl Project {
client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
client.add_model_request_handler(Self::handle_lsp_command::<PrepareRename>);
client.add_model_request_handler(Self::handle_lsp_command::<PerformRename>);
- client.add_model_request_handler(Self::handle_lsp_command::<OnTypeFormatting>);
client.add_model_request_handler(Self::handle_search_project);
client.add_model_request_handler(Self::handle_get_project_symbols);
client.add_model_request_handler(Self::handle_open_buffer_for_symbol);
@@ -4035,6 +4035,118 @@ impl Project {
}
}
+ fn apply_on_type_formatting(
+ &self,
+ buffer: ModelHandle<Buffer>,
+ position: Anchor,
+ trigger: String,
+ push_to_history: bool,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<ProjectTransaction>> {
+ if self.is_local() {
+ cx.spawn(|this, mut cx| async move {
+ // Do not allow multiple concurrent formatting requests for the
+ // same buffer.
+ this.update(&mut cx, |this, cx| {
+ this.buffers_being_formatted
+ .insert(buffer.read(cx).remote_id())
+ });
+
+ let _cleanup = defer({
+ let this = this.clone();
+ let mut cx = cx.clone();
+ let closure_buffer = buffer.clone();
+ move || {
+ this.update(&mut cx, |this, cx| {
+ this.buffers_being_formatted
+ .remove(&closure_buffer.read(cx).remote_id());
+ });
+ }
+ });
+
+ buffer
+ .update(&mut cx, |buffer, _| {
+ buffer.wait_for_edits(Some(position.timestamp))
+ })
+ .await?;
+ this.update(&mut cx, |this, cx| {
+ let position = position.to_point_utf16(buffer.read(cx));
+ this.on_type_format(buffer, position, trigger, cx)
+ })
+ .await
+ })
+ } else if let Some(project_id) = self.remote_id() {
+ let client = self.client.clone();
+ let request = proto::OnTypeFormatting {
+ project_id,
+ buffer_id: buffer.read(cx).remote_id(),
+ position: Some(serialize_anchor(&position)),
+ trigger,
+ version: serialize_version(&buffer.read(cx).version()),
+ };
+ 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_edits(
+ this: ModelHandle<Self>,
+ buffer_to_edit: ModelHandle<Buffer>,
+ edits: Vec<lsp::TextEdit>,
+ push_to_history: bool,
+ _: Arc<CachedLspAdapter>,
+ language_server: Arc<LanguageServer>,
+ cx: &mut AsyncAppContext,
+ ) -> Result<ProjectTransaction> {
+ let edits = this
+ .update(cx, |this, cx| {
+ this.edits_from_lsp(
+ &buffer_to_edit,
+ edits,
+ language_server.server_id(),
+ None,
+ 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)], None, cx);
+ }
+
+ 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
+ }
+ });
+
+ let mut project_transaction = ProjectTransaction::default();
+ if let Some(transaction) = transaction {
+ project_transaction.0.insert(buffer_to_edit, transaction);
+ }
+
+ Ok(project_transaction)
+ }
+
async fn deserialize_workspace_edit(
this: ModelHandle<Self>,
edit: lsp::WorkspaceEdit,
@@ -4204,39 +4316,24 @@ impl Project {
&self,
buffer: ModelHandle<Buffer>,
position: T,
- input: char,
+ trigger: String,
cx: &mut ModelContext<Self>,
- ) -> Task<Result<()>> {
+ ) -> Task<Result<ProjectTransaction>> {
let tab_size = buffer.read_with(cx, |buffer, cx| {
let language_name = buffer.language().map(|language| language.name());
language_settings(language_name.as_deref(), cx).tab_size
});
let position = position.to_point_utf16(buffer.read(cx));
- let edits_task = self.request_lsp(
+ self.request_lsp(
buffer.clone(),
OnTypeFormatting {
position,
- trigger: input.to_string(),
+ trigger,
options: lsp_command::lsp_formatting_options(tab_size.get()).into(),
+ push_to_history: true,
},
cx,
- );
-
- cx.spawn(|_project, mut cx| async move {
- let edits = edits_task
- .await
- .context("requesting OnTypeFormatting edits for char '{new_char}'")?;
-
- if !edits.is_empty() {
- cx.update(|cx| {
- buffer.update(cx, |buffer, cx| {
- buffer.edit(edits, None, cx);
- });
- });
- }
-
- Ok(())
- })
+ )
}
#[allow(clippy::type_complexity)]
@@ -5809,6 +5906,42 @@ impl Project {
})
}
+ async fn handle_on_type_formatting(
+ this: ModelHandle<Self>,
+ envelope: TypedEnvelope<proto::OnTypeFormatting>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::OnTypeFormattingResponse> {
+ let sender_id = envelope.original_sender_id()?;
+ let on_type_formatting = this.update(&mut cx, |this, cx| {
+ let buffer = this
+ .opened_buffers
+ .get(&envelope.payload.buffer_id)
+ .and_then(|buffer| buffer.upgrade(cx))
+ .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?;
+ let position = envelope
+ .payload
+ .position
+ .and_then(deserialize_anchor)
+ .ok_or_else(|| anyhow!("invalid position"))?;
+ Ok::<_, anyhow::Error>(this.apply_on_type_formatting(
+ buffer,
+ position,
+ envelope.payload.trigger.clone(),
+ false,
+ cx,
+ ))
+ })?;
+
+ let project_transaction = on_type_formatting.await?;
+ let project_transaction = this.update(&mut cx, |this, cx| {
+ this.serialize_project_transaction_for_peer(project_transaction, sender_id, cx)
+ });
+ Ok(proto::OnTypeFormattingResponse {
+ transaction: Some(project_transaction),
+ })
+ }
+
async fn handle_lsp_command<T: LspCommand>(
this: ModelHandle<Self>,
envelope: TypedEnvelope<T::ProtoRequest>,
@@ -6379,7 +6512,7 @@ impl Project {
}
#[allow(clippy::type_complexity)]
- pub fn edits_from_lsp(
+ fn edits_from_lsp(
&mut self,
buffer: &ModelHandle<Buffer>,
lsp_edits: impl 'static + Send + IntoIterator<Item = lsp::TextEdit>,
@@ -682,14 +682,7 @@ message OnTypeFormatting {
}
message OnTypeFormattingResponse {
- repeated OnTypeFormattingResponseEntry entries = 1;
- repeated VectorClockEntry version = 2;
-}
-
-message OnTypeFormattingResponseEntry {
- Anchor start = 1;
- Anchor end = 2;
- string new_text = 3;
+ ProjectTransaction transaction = 1;
}
message PerformRenameResponse {