Add support for `insert_text_mode` of a completion (#28171)

Conrad Irwin created

I wanted this for CONL (https://conl.dev )'s nascent langauge server,
and it seems like most of the support was already wired up on the LSP
side, so this surfaces it into the editor.

Release Notes:

- Added support for the `insert_text_mode` field of completions from the
language server protocol.

Change summary

crates/agent/src/context_picker/completion_provider.rs |  5 +
crates/assistant_context_editor/src/slash_command.rs   |  2 
crates/collab_ui/src/chat_panel/message_editor.rs      |  1 
crates/debugger_ui/src/session/running/console.rs      |  2 
crates/editor/src/code_context_menus.rs                |  1 
crates/editor/src/editor.rs                            | 12 ++
crates/editor/src/editor_tests.rs                      | 56 ++++++++++++
crates/lsp/src/lsp.rs                                  |  7 +
crates/project/src/lsp_store.rs                        |  3 
crates/project/src/project.rs                          |  6 
10 files changed, 91 insertions(+), 4 deletions(-)

Detailed changes

crates/agent/src/context_picker/completion_provider.rs 🔗

@@ -112,6 +112,7 @@ impl ContextPickerCompletionProvider {
                         icon_path: Some(mode.icon().path().into()),
                         documentation: None,
                         source: project::CompletionSource::Custom,
+                        insert_text_mode: None,
                         // This ensures that when a user accepts this completion, the
                         // completion menu will still be shown after "@category " is
                         // inserted
@@ -163,6 +164,7 @@ impl ContextPickerCompletionProvider {
             new_text,
             label: CodeLabel::plain(thread_entry.summary.to_string(), None),
             documentation: None,
+            insert_text_mode: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(icon_for_completion.path().into()),
             confirm: Some(confirm_completion_callback(
@@ -209,6 +211,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(IconName::Globe.path().into()),
+            insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 IconName::Globe.path().into(),
                 url_to_fetch.clone(),
@@ -290,6 +293,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(completion_icon_path),
+            insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 crease_icon_path,
                 file_name,
@@ -352,6 +356,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(IconName::Code.path().into()),
+            insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 IconName::Code.path().into(),
                 symbol.name.clone().into(),

crates/assistant_context_editor/src/slash_command.rs 🔗

@@ -127,6 +127,7 @@ impl SlashCommandCompletionProvider {
                                 new_text,
                                 label: command.label(cx),
                                 icon_path: None,
+                                insert_text_mode: None,
                                 confirm,
                                 source: CompletionSource::Custom,
                             })
@@ -228,6 +229,7 @@ impl SlashCommandCompletionProvider {
                                 new_text,
                                 documentation: None,
                                 confirm,
+                                insert_text_mode: None,
                                 source: CompletionSource::Custom,
                             }
                         })

crates/debugger_ui/src/session/running/console.rs 🔗

@@ -367,6 +367,7 @@ impl ConsoleQueryBarCompletionProvider {
                             documentation: None,
                             confirm: None,
                             source: project::CompletionSource::Custom,
+                            insert_text_mode: None,
                         })
                     })
                     .collect(),
@@ -409,6 +410,7 @@ impl ConsoleQueryBarCompletionProvider {
                         documentation: None,
                         confirm: None,
                         source: project::CompletionSource::Custom,
+                        insert_text_mode: None,
                     })
                     .collect(),
             ))

crates/editor/src/code_context_menus.rs 🔗

@@ -240,6 +240,7 @@ impl CompletionsMenu {
                 icon_path: None,
                 documentation: None,
                 confirm: None,
+                insert_text_mode: None,
                 source: CompletionSource::Custom,
             })
             .collect();

crates/editor/src/editor.rs 🔗

@@ -136,7 +136,7 @@ use task::{ResolvedTask, TaskTemplate, TaskVariables};
 pub use lsp::CompletionContext;
 use lsp::{
     CodeActionKind, CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity,
-    InsertTextFormat, LanguageServerId, LanguageServerName,
+    InsertTextFormat, InsertTextMode, LanguageServerId, LanguageServerName,
 };
 
 use language::BufferSnapshot;
@@ -4442,6 +4442,7 @@ impl Editor {
                         word_range,
                         resolved: false,
                     },
+                    insert_text_mode: Some(InsertTextMode::AS_IS),
                     confirm: None,
                 }));
 
@@ -4687,7 +4688,13 @@ impl Editor {
             } else {
                 this.buffer.update(cx, |buffer, cx| {
                     let edits = ranges.iter().map(|range| (range.clone(), text));
-                    buffer.edit(edits, this.autoindent_mode.clone(), cx);
+                    let auto_indent = if completion.insert_text_mode == Some(InsertTextMode::AS_IS)
+                    {
+                        None
+                    } else {
+                        this.autoindent_mode.clone()
+                    };
+                    buffer.edit(edits, auto_indent, cx);
                 });
             }
             for (buffer, edits) in linked_edits {
@@ -18637,6 +18644,7 @@ fn snippet_completions(
                         .description
                         .clone()
                         .map(|description| CompletionDocumentation::SingleLine(description.into())),
+                    insert_text_mode: None,
                     confirm: None,
                 })
             })

crates/editor/src/editor_tests.rs 🔗

@@ -10235,6 +10235,62 @@ async fn test_completion_sort(cx: &mut TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_as_is_completions(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+    cx.lsp
+        .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
+            Ok(Some(lsp::CompletionResponse::Array(vec![
+                lsp::CompletionItem {
+                    label: "unsafe".into(),
+                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                        range: lsp::Range {
+                            start: lsp::Position {
+                                line: 1,
+                                character: 2,
+                            },
+                            end: lsp::Position {
+                                line: 1,
+                                character: 3,
+                            },
+                        },
+                        new_text: "unsafe".to_string(),
+                    })),
+                    insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
+                    ..Default::default()
+                },
+            ])))
+        });
+    cx.set_state("fn a() {}\n  nˇ");
+    cx.executor().run_until_parked();
+    cx.update_editor(|editor, window, cx| {
+        editor.show_completions(
+            &ShowCompletions {
+                trigger: Some("\n".into()),
+            },
+            window,
+            cx,
+        );
+    });
+    cx.executor().run_until_parked();
+
+    cx.update_editor(|editor, window, cx| {
+        editor.confirm_completion(&Default::default(), window, cx)
+    });
+    cx.executor().run_until_parked();
+    cx.assert_editor_state("fn a() {}\n  unsafeˇ");
+}
+
 #[gpui::test]
 async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
     init_test(cx, |_| {});

crates/lsp/src/lsp.rs 🔗

@@ -704,8 +704,15 @@ impl LanguageServer {
                             }),
                             insert_replace_support: Some(true),
                             label_details_support: Some(true),
+                            insert_text_mode_support: Some(InsertTextModeSupport {
+                                value_set: vec![
+                                    InsertTextMode::AS_IS,
+                                    InsertTextMode::ADJUST_INDENTATION,
+                                ],
+                            }),
                             ..Default::default()
                         }),
+                        insert_text_mode: Some(InsertTextMode::ADJUST_INDENTATION),
                         completion_list: Some(CompletionListCapability {
                             item_defaults: Some(vec![
                                 "commitCharacters".to_owned(),

crates/project/src/lsp_store.rs 🔗

@@ -8053,6 +8053,7 @@ impl LspStore {
                         runs: Default::default(),
                         filter_range: Default::default(),
                     },
+                    insert_text_mode: None,
                     icon_path: None,
                     confirm: None,
                 }]))),
@@ -9342,6 +9343,7 @@ async fn populate_labels_for_completions(
                     documentation,
                     old_range: completion.old_range,
                     new_text: completion.new_text,
+                    insert_text_mode: lsp_completion.insert_text_mode,
                     source: completion.source,
                     icon_path: None,
                     confirm: None,
@@ -9356,6 +9358,7 @@ async fn populate_labels_for_completions(
                     old_range: completion.old_range,
                     new_text: completion.new_text,
                     source: completion.source,
+                    insert_text_mode: None,
                     icon_path: None,
                     confirm: None,
                 });

crates/project/src/project.rs 🔗

@@ -68,8 +68,8 @@ use language::{
     language_settings::InlayHintKind, proto::split_operations,
 };
 use lsp::{
-    CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServerId,
-    LanguageServerName, MessageActionItem,
+    CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, InsertTextMode,
+    LanguageServerId, LanguageServerName, MessageActionItem,
 };
 use lsp_command::*;
 use lsp_store::{CompletionDocumentation, LspFormatTarget, OpenLspBufferHandle};
@@ -392,6 +392,8 @@ pub struct Completion {
     pub source: CompletionSource,
     /// A path to an icon for this completion that is shown in the menu.
     pub icon_path: Option<SharedString>,
+    /// Whether to adjust indentation (the default) or not.
+    pub insert_text_mode: Option<InsertTextMode>,
     /// An optional callback to invoke when this completion is confirmed.
     /// Returns, whether new completions should be retriggered after the current one.
     /// If `true` is returned, the editor will show a new completion menu after this completion is confirmed.