Send and handle OnTypeFormatting LSP request

Kirill Bulatov created

Change summary

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

Detailed changes

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<Self>,
+    ) -> Option<Task<Result<()>>> {
+        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<Self>) {
         if self.pending_rename.is_some() {
             return;

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<Anchor>,
 }
 
+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<Range<Anchor>>;
@@ -1596,3 +1602,98 @@ impl LspCommand for GetCodeActions {
         message.buffer_id
     }
 }
+
+#[async_trait(?Send)]
+impl LspCommand for OnTypeFormatting {
+    type Response = Vec<(Range<Anchor>, 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<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.new_char.to_string(),
+            // TODO kb pass current editor ones
+            options: lsp::FormattingOptions::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<Vec<lsp::TextEdit>>,
+        project: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        server_id: LanguageServerId,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<(Range<Anchor>, 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<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        todo!("TODO kb")
+    }
+
+    fn response_to_proto(
+        response: Vec<(Range<Anchor>, 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<Project>,
+        _: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<(Range<Anchor>, String)>> {
+        todo!("TODO kb")
+    }
+
+    fn buffer_id_from_proto(message: &proto::PerformRename) -> u64 {
+        message.buffer_id
+    }
+}

crates/project/src/project.rs 🔗

@@ -4209,6 +4209,40 @@ impl Project {
         )
     }
 
+    pub fn on_type_format<T: ToPointUtf16>(
+        &self,
+        buffer: ModelHandle<Buffer>,
+        position: T,
+        input: char,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        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<Buffer>,
         lsp_edits: impl 'static + Send + IntoIterator<Item = lsp::TextEdit>,