diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a33988dc5d..c0755cf1fe 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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::(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::(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, ) -> Option>> { + 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(project.update(cx, |project, cx| { - project.on_type_format(buffer.clone(), 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?; + + editor.update(&mut cx, |editor, cx| { + editor.refresh_document_highlights(cx); + })?; + Ok(()) })) } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 992973182f..72dc06e14f 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -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, String)>; + type Response = ProjectTransaction; type LspRequest = lsp::request::OnTypeFormatting; type ProtoRequest = proto::OnTypeFormatting; @@ -1667,14 +1668,23 @@ impl LspCommand for OnTypeFormatting { buffer: ModelHandle, server_id: LanguageServerId, mut cx: AsyncAppContext, - ) -> Result, 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 { + 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, 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, - buffer: ModelHandle, + project: ModelHandle, + _: ModelHandle, mut cx: AsyncAppContext, - ) -> Result, String)>> { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) + ) -> Result { + 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?; - 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)) - }) - .collect() + .await } fn buffer_id_from_proto(message: &proto::OnTypeFormatting) -> u64 { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 36b3290121..5df4a94dc0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -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::); client.add_model_request_handler(Self::handle_lsp_command::); client.add_model_request_handler(Self::handle_lsp_command::); - client.add_model_request_handler(Self::handle_lsp_command::); 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, + position: Anchor, + trigger: String, + push_to_history: bool, + cx: &mut ModelContext, + ) -> Task> { + 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, + buffer_to_edit: ModelHandle, + edits: Vec, + push_to_history: bool, + _: Arc, + language_server: Arc, + cx: &mut AsyncAppContext, + ) -> Result { + 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, edit: lsp::WorkspaceEdit, @@ -4204,39 +4316,24 @@ impl Project { &self, buffer: ModelHandle, position: T, - input: char, + trigger: String, cx: &mut ModelContext, - ) -> Task> { + ) -> Task> { 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, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + 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( this: ModelHandle, envelope: TypedEnvelope, @@ -6379,7 +6512,7 @@ impl Project { } #[allow(clippy::type_complexity)] - pub fn edits_from_lsp( + fn edits_from_lsp( &mut self, buffer: &ModelHandle, lsp_edits: impl 'static + Send + IntoIterator, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index fe0d18f422..9e0d334944 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -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 {