Support OnTypeFormatting LSP request (#2517)

Supports
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_onTypeFormatting

rust-analyzer uses this feature to add matching brackets semantically,
e.g.
before:
![Screenshot 2023-05-23 at 17 46
42](https://github.com/zed-industries/zed/assets/2690773/020e8448-23e6-4a38-8dbb-c9edf18062f7)

after:
![Screenshot 2023-05-23 at 17 46
49](https://github.com/zed-industries/zed/assets/2690773/4d140af3-aca6-451d-ac61-e2a9bb31caea)

`use_on_type_format` settings entry was added, enabled by default, to
disable the new feature.

Release Notes:

* Support `OnTypeFormatting` LSP protocol feature, allowing
rust-analyzer to add matching brackets
This commit is contained in:
Kirill Bulatov 2023-05-25 18:35:47 +03:00 committed by GitHub
commit ae3bdd755e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 664 additions and 14 deletions

View file

@ -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,

View file

@ -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)

View 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>,

View file

@ -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;

View file

@ -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>,
}

View file

@ -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
}
}

View file

@ -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>,

View file

@ -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;
}

View file

@ -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,

View file

@ -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;