From f6d7b3d2e8fb68077dbb575314b4bb8ee81116e1 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 19 May 2023 13:18:50 +0300 Subject: [PATCH] Send and handle OnTypeFormatting LSP request --- crates/editor/src/editor.rs | 24 +++++++ crates/project/src/lsp_command.rs | 103 +++++++++++++++++++++++++++++- crates/project/src/project.rs | 36 ++++++++++- 3 files changed, 161 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 41fd03bf7f..c6709b51fd 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2122,6 +2122,13 @@ 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)); + if text.len() == 1 { + let input_char = text.chars().next().expect("single char input"); + if let Some(on_type_format_task) = this.trigger_on_type_format(input_char, cx) { + 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 +2507,23 @@ impl Editor { } } + fn trigger_on_type_format( + &self, + input: char, + cx: &mut ViewContext, + ) -> Option>> { + 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)?; + + Some(project.update(cx, |project, cx| { + project.on_type_format(buffer.clone(), buffer_position, input, cx) + })) + } + fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { if self.pending_rename.is_some() { return; diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index ddae9b59ae..523011c76a 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2,7 +2,7 @@ use crate::{ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, Location, LocationLink, Project, ProjectTransaction, }; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::proto::{self, PeerId}; use fs::LineEnding; @@ -109,6 +109,12 @@ pub(crate) struct GetCodeActions { pub range: Range, } +pub(crate) struct OnTypeFormatting { + pub position: PointUtf16, + pub new_char: char, + // TODO kb formatting options? +} + #[async_trait(?Send)] impl LspCommand for PrepareRename { type Response = Option>; @@ -1596,3 +1602,98 @@ impl LspCommand for GetCodeActions { message.buffer_id } } + +#[async_trait(?Send)] +impl LspCommand for OnTypeFormatting { + type Response = Vec<(Range, String)>; + type LspRequest = lsp::request::OnTypeFormatting; + type ProtoRequest = proto::PerformRename; + + 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.new_char) + || on_type_formatting_options + .more_trigger_character + .iter() + .flatten() + .any(|chars| chars.contains(self.new_char)) + } + + 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.new_char.to_string(), + // TODO kb pass current editor ones + options: lsp::FormattingOptions::default(), + } + } + + async fn response_from_lsp( + self, + message: Option>, + project: ModelHandle, + buffer: ModelHandle, + server_id: LanguageServerId, + mut cx: AsyncAppContext, + ) -> Result, String)>> { + cx.update(|cx| { + project.update(cx, |project, cx| { + project.edits_from_lsp(&buffer, message.into_iter().flatten(), server_id, None, cx) + }) + }) + .await + .context("LSP edits conversion") + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PerformRename { + todo!("TODO kb") + } + + async fn from_proto( + message: proto::PerformRename, + _: ModelHandle, + buffer: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result { + todo!("TODO kb") + } + + fn response_to_proto( + response: Vec<(Range, String)>, + project: &mut Project, + peer_id: PeerId, + _: &clock::Global, + cx: &mut AppContext, + ) -> proto::PerformRenameResponse { + // let transaction = project.serialize_project_transaction_for_peer(response, peer_id, cx); + // proto::PerformRenameResponse { + // transaction: Some(transaction), + // } + todo!("TODO kb") + } + + async fn response_from_proto( + self, + message: proto::PerformRenameResponse, + project: ModelHandle, + _: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result, String)>> { + todo!("TODO kb") + } + + fn buffer_id_from_proto(message: &proto::PerformRename) -> u64 { + message.buffer_id + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index dd53c30d14..d8d29cbadd 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4209,6 +4209,40 @@ impl Project { ) } + pub fn on_type_format( + &self, + buffer: ModelHandle, + position: T, + input: char, + cx: &mut ModelContext, + ) -> Task> { + let position = position.to_point_utf16(buffer.read(cx)); + let edits_task = self.request_lsp( + buffer.clone(), + OnTypeFormatting { + position, + new_char: input, + }, + cx, + ); + + cx.spawn(|_project, mut cx| async move { + let edits = edits_task + .await + .context("requesting OnTypeFormatting edits for char '{new_char}'")?; + + if !edits.is_empty() { + cx.update(|cx| { + buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + }); + } + + Ok(()) + }) + } + #[allow(clippy::type_complexity)] pub fn search( &self, @@ -6349,7 +6383,7 @@ impl Project { } #[allow(clippy::type_complexity)] - fn edits_from_lsp( + pub fn edits_from_lsp( &mut self, buffer: &ModelHandle, lsp_edits: impl 'static + Send + IntoIterator,