Detailed changes
@@ -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,
@@ -223,6 +223,7 @@ impl Server {
.add_request_handler(forward_project_request::<proto::RenameProjectEntry>)
.add_request_handler(forward_project_request::<proto::CopyProjectEntry>)
.add_request_handler(forward_project_request::<proto::DeleteProjectEntry>)
+ .add_request_handler(forward_project_request::<proto::OnTypeFormatting>)
.add_message_handler(create_buffer_for_peer)
.add_request_handler(update_buffer)
.add_message_handler(update_buffer_file)
@@ -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<Deterministic>,
+ 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::<lsp::request::OnTypeFormatting, _, _>(
+ |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<Deterministic>,
+ 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::<lsp::request::OnTypeFormatting, _, _>(|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<String>,
@@ -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::<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);
+ }
+ }
+
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<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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<Self>) {
if self.pending_rename.is_some() {
return;
@@ -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<bool>,
pub hover_popover_enabled: Option<bool>,
pub show_completions_on_input: Option<bool>,
+ pub use_on_type_format: Option<bool>,
pub scrollbar: Option<ScrollbarContent>,
}
@@ -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<Anchor>,
}
+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<lsp::FormattingOptions> for FormattingOptions {
+ fn from(value: lsp::FormattingOptions) -> Self {
+ Self {
+ tab_size: value.tab_size,
+ }
+ }
+}
+
#[async_trait(?Send)]
impl LspCommand for PrepareRename {
type Response = Option<Range<Anchor>>;
@@ -1596,3 +1625,134 @@ impl LspCommand for GetCodeActions {
message.buffer_id
}
}
+
+#[async_trait(?Send)]
+impl LspCommand for OnTypeFormatting {
+ type Response = Option<Transaction>;
+ 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<LanguageServer>,
+ _: &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<Vec<lsp::TextEdit>>,
+ project: ModelHandle<Project>,
+ buffer: ModelHandle<Buffer>,
+ server_id: LanguageServerId,
+ mut cx: AsyncAppContext,
+ ) -> Result<Option<Transaction>> {
+ 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<Project>,
+ buffer: ModelHandle<Buffer>,
+ mut cx: AsyncAppContext,
+ ) -> Result<Self> {
+ 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<Transaction>,
+ _: &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<Project>,
+ _: ModelHandle<Buffer>,
+ _: AsyncAppContext,
+ ) -> Result<Option<Transaction>> {
+ 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
+ }
+}
@@ -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::request::Formatting>(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::request::RangeFormatting>(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<Buffer>,
+ position: Anchor,
+ trigger: String,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<Option<Transaction>>> {
+ 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<Self>,
+ buffer_to_edit: ModelHandle<Buffer>,
+ edits: Vec<lsp::TextEdit>,
+ push_to_history: bool,
+ _: Arc<CachedLspAdapter>,
+ language_server: Arc<LanguageServer>,
+ cx: &mut AsyncAppContext,
+ ) -> Result<Option<Transaction>> {
+ 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<Self>,
edit: lsp::WorkspaceEdit,
@@ -4209,6 +4303,31 @@ impl Project {
)
}
+ pub fn on_type_format<T: ToPointUtf16>(
+ &self,
+ buffer: ModelHandle<Buffer>,
+ position: T,
+ trigger: String,
+ push_to_history: bool,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<Option<Transaction>>> {
+ 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<Self>,
+ envelope: TypedEnvelope<proto::OnTypeFormatting>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::OnTypeFormattingResponse> {
+ 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<T: LspCommand>(
this: ModelHandle<Self>,
envelope: TypedEnvelope<T::ProtoRequest>,
@@ -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;
}
@@ -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,
@@ -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;