mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-24 11:01:54 +00:00
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:
commit
ae3bdd755e
10 changed files with 664 additions and 14 deletions
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue