diff --git a/assets/settings/default.json b/assets/settings/default.json index 246e28cc8e..23599c8dfb 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -39,6 +39,9 @@ // Whether to pop the completions menu while typing in an editor without // explicitly requesting it. "show_completions_on_input": true, + // Whether to use additional LSP queries to format (and amend) the code after + // every "trigger" symbol input, defined by LSP server capabilities. + "use_on_type_format": true, // Controls whether copilot provides suggestion immediately // or waits for a `copilot::Toggle` "show_copilot_suggestions": true, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index ac86f8c171..4c117b613d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -223,6 +223,7 @@ impl Server { .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) + .add_request_handler(forward_project_request::) .add_message_handler(create_buffer_for_peer) .add_request_handler(update_buffer) .add_message_handler(update_buffer_file) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 439ee0786a..d771f969d8 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -7377,6 +7377,265 @@ async fn test_peers_simultaneously_following_each_other( }); } +#[gpui::test(iterations = 10)] +async fn test_on_input_format_from_host_to_guest( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: ":".to_string(), + more_trigger_character: Some(vec![">".to_string()]), + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry.add(Arc::new(language)); + + client_a + .fs + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a }", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + // Open a file in an editor as the host. + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + let (window_a, _) = cx_a.add_window(|_| EmptyView); + let editor_a = cx_a.add_view(window_a, |cx| { + Editor::for_buffer(buffer_a, Some(project_a.clone()), cx) + }); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + cx_b.foreground().run_until_parked(); + + // Receive an OnTypeFormatting request as the host's language server. + // Return some formattings from the host's language server. + fake_language_server.handle_request::( + |params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 14), + ); + + Ok(Some(vec![lsp::TextEdit { + new_text: "~<".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), + }])) + }, + ); + + // Open the buffer on the guest and see that the formattings worked + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + + // Type a on type formatting trigger character as the guest. + editor_a.update(cx_a, |editor, cx| { + cx.focus(&editor_a); + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(">", cx); + }); + + cx_b.foreground().run_until_parked(); + + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a>~< }") + }); + + // Undo should remove LSP edits first + editor_a.update(cx_a, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a>~< }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a> }"); + }); + cx_b.foreground().run_until_parked(); + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a> }") + }); + + editor_a.update(cx_a, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a> }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a }"); + }); + cx_b.foreground().run_until_parked(); + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a }") + }); +} + +#[gpui::test(iterations = 10)] +async fn test_on_input_format_from_guest_to_host( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: ":".to_string(), + more_trigger_character: Some(vec![">".to_string()]), + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry.add(Arc::new(language)); + + client_a + .fs + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a }", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + // Open a file in an editor as the guest. + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + let (window_b, _) = cx_b.add_window(|_| EmptyView); + let editor_b = cx_b.add_view(window_b, |cx| { + Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) + }); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + cx_a.foreground().run_until_parked(); + // Type a on type formatting trigger character as the guest. + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(":", cx); + cx.focus(&editor_b); + }); + + // Receive an OnTypeFormatting request as the host's language server. + // Return some formattings from the host's language server. + cx_a.foreground().start_waiting(); + fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 14), + ); + + Ok(Some(vec![lsp::TextEdit { + new_text: "~:".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), + }])) + }) + .next() + .await + .unwrap(); + cx_a.foreground().finish_waiting(); + + // Open the buffer on the host and see that the formattings worked + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + cx_a.foreground().run_until_parked(); + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a:~: }") + }); + + // Undo should remove LSP edits first + editor_b.update(cx_b, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a:~: }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a: }"); + }); + cx_a.foreground().run_until_parked(); + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a: }") + }); + + editor_b.update(cx_b, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a: }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a }"); + }); + cx_a.foreground().run_until_parked(); + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a }") + }); +} + #[derive(Debug, Eq, PartialEq)] struct RoomParticipants { remote: Vec, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 41fd03bf7f..5a504a610c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2122,6 +2122,15 @@ impl Editor { let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx); 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 { + 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); + } + } + if had_active_copilot_suggestion { this.refresh_copilot_suggestions(true, cx); if !this.has_active_copilot_suggestion(cx) { @@ -2500,6 +2509,52 @@ impl Editor { } } + fn trigger_on_type_formatting( + &self, + input: String, + cx: &mut ViewContext, + ) -> Option>> { + if input.len() != 1 { + return None; + } + + 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)?; + + // OnTypeFormatting retuns a list of edits, no need to pass them between Zed instances, + // hence we do LSP request & edit on host side only — add formats to host's history. + let push_to_lsp_host_history = true; + // If this is not the host, append its history with new edits. + let push_to_client_history = project.read(cx).is_remote(); + + let on_type_formatting = project.update(cx, |project, cx| { + project.on_type_format( + buffer.clone(), + buffer_position, + input, + push_to_lsp_host_history, + cx, + ) + }); + Some(cx.spawn(|editor, mut cx| async move { + if let Some(transaction) = on_type_formatting.await? { + if push_to_client_history { + buffer.update(&mut cx, |buffer, _| { + buffer.push_transaction(transaction, Instant::now()); + }); + } + editor.update(&mut cx, |editor, cx| { + editor.refresh_document_highlights(cx); + })?; + } + Ok(()) + })) + } + fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { if self.pending_rename.is_some() { return; diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 7f01834b16..387d4d2c34 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -7,6 +7,7 @@ pub struct EditorSettings { pub cursor_blink: bool, pub hover_popover_enabled: bool, pub show_completions_on_input: bool, + pub use_on_type_format: bool, pub scrollbar: Scrollbar, } @@ -30,6 +31,7 @@ pub struct EditorSettingsContent { pub cursor_blink: Option, pub hover_popover_enabled: Option, pub show_completions_on_input: Option, + pub use_on_type_format: Option, pub scrollbar: Option, } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index ddae9b59ae..5ee6443896 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -8,14 +8,24 @@ use client::proto::{self, PeerId}; use fs::LineEnding; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ + language_settings::language_settings, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction, - Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Unclipped, + Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; +pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions { + lsp::FormattingOptions { + tab_size, + insert_spaces: true, + insert_final_newline: Some(true), + ..lsp::FormattingOptions::default() + } +} + #[async_trait(?Send)] pub(crate) trait LspCommand: 'static + Sized { type Response: 'static + Default + Send; @@ -109,6 +119,25 @@ pub(crate) struct GetCodeActions { pub range: Range, } +pub(crate) struct OnTypeFormatting { + pub position: PointUtf16, + pub trigger: String, + pub options: FormattingOptions, + pub push_to_history: bool, +} + +pub(crate) struct FormattingOptions { + tab_size: u32, +} + +impl From for FormattingOptions { + fn from(value: lsp::FormattingOptions) -> Self { + Self { + tab_size: value.tab_size, + } + } +} + #[async_trait(?Send)] impl LspCommand for PrepareRename { type Response = Option>; @@ -1596,3 +1625,134 @@ impl LspCommand for GetCodeActions { message.buffer_id } } + +#[async_trait(?Send)] +impl LspCommand for OnTypeFormatting { + type Response = Option; + type LspRequest = lsp::request::OnTypeFormatting; + type ProtoRequest = proto::OnTypeFormatting; + + fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool { + let Some(on_type_formatting_options) = &server_capabilities.document_on_type_formatting_provider else { return false }; + on_type_formatting_options + .first_trigger_character + .contains(&self.trigger) + || on_type_formatting_options + .more_trigger_character + .iter() + .flatten() + .any(|chars| chars.contains(&self.trigger)) + } + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &AppContext, + ) -> lsp::DocumentOnTypeFormattingParams { + lsp::DocumentOnTypeFormattingParams { + text_document_position: lsp::TextDocumentPositionParams::new( + lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path).unwrap()), + point_to_lsp(self.position), + ), + ch: self.trigger.clone(), + options: lsp_formatting_options(self.options.tab_size), + } + } + + async fn response_from_lsp( + self, + message: Option>, + project: ModelHandle, + buffer: ModelHandle, + server_id: LanguageServerId, + mut cx: AsyncAppContext, + ) -> 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(None) + } + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::OnTypeFormatting { + proto::OnTypeFormatting { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + trigger: self.trigger.clone(), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::OnTypeFormatting, + _: ModelHandle, + buffer: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + + 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 + }); + + Ok(Self { + 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: Option, + _: &mut Project, + _: PeerId, + _: &clock::Global, + _: &mut AppContext, + ) -> proto::OnTypeFormattingResponse { + proto::OnTypeFormattingResponse { + transaction: response + .map(|transaction| language::proto::serialize_transaction(&transaction)), + } + } + + async fn response_from_proto( + self, + message: proto::OnTypeFormattingResponse, + _: ModelHandle, + _: ModelHandle, + _: AsyncAppContext, + ) -> Result> { + let Some(transaction) = message.transaction else { return Ok(None) }; + Ok(Some(language::proto::deserialize_transaction(transaction)?)) + } + + fn buffer_id_from_proto(message: &proto::OnTypeFormatting) -> u64 { + message.buffer_id + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index dd53c30d14..391a698a1b 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); @@ -3476,12 +3477,7 @@ impl Project { language_server .request::(lsp::DocumentFormattingParams { text_document, - options: lsp::FormattingOptions { - tab_size: tab_size.into(), - insert_spaces: true, - insert_final_newline: Some(true), - ..Default::default() - }, + options: lsp_command::lsp_formatting_options(tab_size.get()), work_done_progress_params: Default::default(), }) .await? @@ -3497,12 +3493,7 @@ impl Project { .request::(lsp::DocumentRangeFormattingParams { text_document, range: lsp::Range::new(buffer_start, buffer_end), - options: lsp::FormattingOptions { - tab_size: tab_size.into(), - insert_spaces: true, - insert_final_newline: Some(true), - ..Default::default() - }, + options: lsp_command::lsp_formatting_options(tab_size.get()), work_done_progress_params: Default::default(), }) .await? @@ -4044,6 +4035,109 @@ impl Project { } } + fn apply_on_type_formatting( + &self, + buffer: ModelHandle, + position: Anchor, + trigger: String, + 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, false, 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(|_, _| async move { + client + .request(request) + .await? + .transaction + .map(language::proto::deserialize_transaction) + .transpose() + }) + } 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 + } + }); + + Ok(transaction) + } + async fn deserialize_workspace_edit( this: ModelHandle, edit: lsp::WorkspaceEdit, @@ -4209,6 +4303,31 @@ impl Project { ) } + pub fn on_type_format( + &self, + buffer: ModelHandle, + position: T, + trigger: String, + push_to_history: bool, + cx: &mut ModelContext, + ) -> 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)); + self.request_lsp( + buffer.clone(), + OnTypeFormatting { + position, + trigger, + options: lsp_command::lsp_formatting_options(tab_size.get()).into(), + push_to_history, + }, + cx, + ) + } + #[allow(clippy::type_complexity)] pub fn search( &self, @@ -5779,6 +5898,38 @@ impl Project { }) } + async fn handle_on_type_formatting( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + 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(), + cx, + )) + })?; + + let transaction = on_type_formatting + .await? + .as_ref() + .map(language::proto::serialize_transaction); + Ok(proto::OnTypeFormattingResponse { transaction }) + } + async fn handle_lsp_command( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index eca5fda306..848cc1c2fa 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -129,6 +129,9 @@ message Envelope { GetPrivateUserInfo get_private_user_info = 105; GetPrivateUserInfoResponse get_private_user_info_response = 106; UpdateDiffBase update_diff_base = 107; + + OnTypeFormatting on_type_formatting = 111; + OnTypeFormattingResponse on_type_formatting_response = 112; } } @@ -670,6 +673,18 @@ message PerformRename { repeated VectorClockEntry version = 5; } +message OnTypeFormatting { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + string trigger = 4; + repeated VectorClockEntry version = 5; +} + +message OnTypeFormattingResponse { + Transaction transaction = 1; +} + message PerformRenameResponse { ProjectTransaction transaction = 2; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index cef4e6867c..07925a0486 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -195,6 +195,8 @@ messages!( (OpenBufferResponse, Background), (PerformRename, Background), (PerformRenameResponse, Background), + (OnTypeFormatting, Background), + (OnTypeFormattingResponse, Background), (Ping, Foreground), (PrepareRename, Background), (PrepareRenameResponse, Background), @@ -279,6 +281,7 @@ request_messages!( (Ping, Ack), (PerformRename, PerformRenameResponse), (PrepareRename, PrepareRenameResponse), + (OnTypeFormatting, OnTypeFormattingResponse), (ReloadBuffers, ReloadBuffersResponse), (RequestContact, Ack), (RemoveContact, Ack), @@ -323,6 +326,7 @@ entity_messages!( OpenBufferByPath, OpenBufferForSymbol, PerformRename, + OnTypeFormatting, PrepareRename, ReloadBuffers, RemoveProjectCollaborator, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 64fbf19462..b929de9596 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 55; +pub const PROTOCOL_VERSION: u32 = 56;