diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 0e1163b53e626a65b32731656f60a6c6c019b1c8..482f27410a912beb0c34a1e0811c5095817c2384 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -532,6 +532,7 @@ "context": "Editor && showing_completions", "bindings": { "enter": "editor::ConfirmCompletion", + "shift-enter": "editor::ConfirmCompletionReplace", "tab": "editor::ComposeCompletion" } }, diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 742b2a2a98b00c75db910e453ba8ea4bde189b1b..7248dab0a3be1cebf7fb824c46f610b91dda282f 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -681,6 +681,7 @@ "use_key_equivalents": true, "bindings": { "enter": "editor::ConfirmCompletion", + "shift-enter": "editor::ConfirmCompletionReplace", "tab": "editor::ComposeCompletion" } }, diff --git a/crates/agent/src/context_picker/completion_provider.rs b/crates/agent/src/context_picker/completion_provider.rs index cda7abbe5d82300085d365fc2f3cec3efb47caa4..bf8bdf155cac806d779556f7701cb4c83c6cac8d 100644 --- a/crates/agent/src/context_picker/completion_provider.rs +++ b/crates/agent/src/context_picker/completion_provider.rs @@ -106,7 +106,7 @@ impl ContextPickerCompletionProvider { .iter() .map(|mode| { Completion { - old_range: source_range.clone(), + replace_range: source_range.clone(), new_text: format!("@{} ", mode.mention_prefix()), label: CodeLabel::plain(mode.label().to_string(), None), icon_path: Some(mode.icon().path().into()), @@ -160,7 +160,7 @@ impl ContextPickerCompletionProvider { let new_text = MentionLink::for_thread(&thread_entry); let new_text_len = new_text.len(); Completion { - old_range: source_range.clone(), + replace_range: source_range.clone(), new_text, label: CodeLabel::plain(thread_entry.summary.to_string(), None), documentation: None, @@ -205,7 +205,7 @@ impl ContextPickerCompletionProvider { let new_text = MentionLink::for_fetch(&url_to_fetch); let new_text_len = new_text.len(); Completion { - old_range: source_range.clone(), + replace_range: source_range.clone(), new_text, label: CodeLabel::plain(url_to_fetch.to_string(), None), documentation: None, @@ -287,7 +287,7 @@ impl ContextPickerCompletionProvider { let new_text = MentionLink::for_file(&file_name, &full_path); let new_text_len = new_text.len(); Completion { - old_range: source_range.clone(), + replace_range: source_range.clone(), new_text, label, documentation: None, @@ -350,7 +350,7 @@ impl ContextPickerCompletionProvider { let new_text = MentionLink::for_symbol(&symbol.name, &full_path); let new_text_len = new_text.len(); Some(Completion { - old_range: source_range.clone(), + replace_range: source_range.clone(), new_text, label, documentation: None, diff --git a/crates/assistant_context_editor/src/slash_command.rs b/crates/assistant_context_editor/src/slash_command.rs index da3e53435b248a210d520002bbda342ef40b4c6a..1cbcb4a63794526b3910b4268979fbeabf2de39c 100644 --- a/crates/assistant_context_editor/src/slash_command.rs +++ b/crates/assistant_context_editor/src/slash_command.rs @@ -120,7 +120,7 @@ impl SlashCommandCompletionProvider { ) as Arc<_> }); Some(project::Completion { - old_range: name_range.clone(), + replace_range: name_range.clone(), documentation: Some(CompletionDocumentation::SingleLine( command.description().into(), )), @@ -219,7 +219,7 @@ impl SlashCommandCompletionProvider { } project::Completion { - old_range: if new_argument.replace_previous_arguments { + replace_range: if new_argument.replace_previous_arguments { argument_range.clone() } else { last_argument_range.clone() diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 31d25f0054bd79c1e959a48cff30a3a4b2c4fa80..59d4e58c3204f1a926889a08bbcc86c2d52d0d3f 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -309,7 +309,7 @@ impl MessageEditor { .map(|mat| { let (new_text, label) = completion_fn(&mat); Completion { - old_range: range.clone(), + replace_range: range.clone(), new_text, label, icon_path: None, diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index b112453d714a1e57b3df833ec3ed0854245b1e49..4973b854c3a129acffffb41b07c1d7e1af06f0ea 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -356,7 +356,7 @@ impl ConsoleQueryBarCompletionProvider { let variable_value = variables.get(&string_match.string)?; Some(project::Completion { - old_range: buffer_position..buffer_position, + replace_range: buffer_position..buffer_position, new_text: string_match.string.clone(), label: CodeLabel { filter_range: 0..string_match.string.len(), @@ -428,10 +428,10 @@ impl ConsoleQueryBarCompletionProvider { let buffer_offset = buffer_position.to_offset(&snapshot); let start = buffer_offset - word_bytes_length; let start = snapshot.anchor_before(start); - let old_range = start..buffer_position; + let replace_range = start..buffer_position; project::Completion { - old_range, + replace_range, new_text, label: CodeLabel { filter_range: 0..completion.label.len(), diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 6d744031d773ad87d3d93e640ee9dad4fe70fb4a..3ac782599360ca3929dd58242895564739ea8d06 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -3,6 +3,7 @@ use super::*; use gpui::{action_as, action_with_deprecated_aliases, actions}; use schemars::JsonSchema; use util::serde::default_true; + #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] #[serde(deny_unknown_fields)] pub struct SelectNext { @@ -262,6 +263,8 @@ actions!( Cancel, CancelLanguageServerWork, ConfirmRename, + ConfirmCompletionInsert, + ConfirmCompletionReplace, ContextMenuFirst, ContextMenuLast, ContextMenuNext, diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 6dd5029700513b00230176e52215e2d68a2dd04f..caf555bc30c9dbc85e248ea6558e0633dd488c25 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -230,7 +230,7 @@ impl CompletionsMenu { let completions = choices .iter() .map(|choice| Completion { - old_range: selection.start.text_anchor..selection.end.text_anchor, + replace_range: selection.start.text_anchor..selection.end.text_anchor, new_text: choice.to_string(), label: CodeLabel { text: choice.to_string(), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a5fcc81b405109a468e1385acd28a73d75c3a816..4e5750ab64c78ac511948cd6d5c3988ecb3cc870 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -109,8 +109,8 @@ use language::{ IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery, language_settings::{ - self, InlayHintSettings, RewrapBehavior, WordsCompletionMode, all_language_settings, - language_settings, + self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, + all_language_settings, language_settings, }, point_from_lsp, text_diff_with_options, }; @@ -4462,7 +4462,7 @@ impl Editor { words.remove(&lsp_completion.new_text); } completions.extend(words.into_iter().map(|(word, word_range)| Completion { - old_range: old_range.clone(), + replace_range: old_range.clone(), new_text: word.clone(), label: CodeLabel::plain(word, None), icon_path: None, @@ -4569,6 +4569,26 @@ impl Editor { self.do_completion(action.item_ix, CompletionIntent::Complete, window, cx) } + pub fn confirm_completion_insert( + &mut self, + _: &ConfirmCompletionInsert, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.do_completion(None, CompletionIntent::CompleteWithInsert, window, cx) + } + + pub fn confirm_completion_replace( + &mut self, + _: &ConfirmCompletionReplace, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.do_completion(None, CompletionIntent::CompleteWithReplace, window, cx) + } + pub fn compose_completion( &mut self, action: &ComposeCompletion, @@ -4588,12 +4608,10 @@ impl Editor { ) -> Option>> { use language::ToOffset as _; - let completions_menu = - if let CodeContextMenu::Completions(menu) = self.hide_context_menu(window, cx)? { - menu - } else { - return None; - }; + let CodeContextMenu::Completions(completions_menu) = self.hide_context_menu(window, cx)? + else { + return None; + }; let candidate_id = { let entries = completions_menu.entries.borrow(); @@ -4622,9 +4640,12 @@ impl Editor { new_text = completion.new_text.clone(); }; let selections = self.selections.all::(cx); + + let replace_range = choose_completion_range(&completion, intent, &buffer_handle, cx); let buffer = buffer_handle.read(cx); - let old_range = completion.old_range.to_offset(buffer); - let old_text = buffer.text_for_range(old_range.clone()).collect::(); + let old_text = buffer + .text_for_range(replace_range.clone()) + .collect::(); let newest_selection = self.selections.newest_anchor(); if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) { @@ -4635,8 +4656,8 @@ impl Editor { .start .text_anchor .to_offset(buffer) - .saturating_sub(old_range.start); - let lookahead = old_range + .saturating_sub(replace_range.start); + let lookahead = replace_range .end .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer)); let mut common_prefix_len = 0; @@ -4665,8 +4686,8 @@ impl Editor { ranges.clear(); ranges.extend(selections.iter().map(|s| { if s.id == newest_selection.id { - range_to_replace = Some(old_range.clone()); - old_range.clone() + range_to_replace = Some(replace_range.clone()); + replace_range.clone() } else { s.start..s.end } @@ -17980,6 +18001,81 @@ impl Editor { } } +// Consider user intent and default settings +fn choose_completion_range( + completion: &Completion, + intent: CompletionIntent, + buffer: &Entity, + cx: &mut Context, +) -> Range { + fn should_replace( + completion: &Completion, + insert_range: &Range, + intent: CompletionIntent, + completion_mode_setting: LspInsertMode, + buffer: &Buffer, + ) -> bool { + // specific actions take precedence over settings + match intent { + CompletionIntent::CompleteWithInsert => return false, + CompletionIntent::CompleteWithReplace => return true, + CompletionIntent::Complete | CompletionIntent::Compose => {} + } + + match completion_mode_setting { + LspInsertMode::Insert => false, + LspInsertMode::Replace => true, + LspInsertMode::ReplaceSubsequence => { + let mut text_to_replace = buffer.chars_for_range( + buffer.anchor_before(completion.replace_range.start) + ..buffer.anchor_after(completion.replace_range.end), + ); + let mut completion_text = completion.new_text.chars(); + + // is `text_to_replace` a subsequence of `completion_text` + text_to_replace + .all(|needle_ch| completion_text.any(|haystack_ch| haystack_ch == needle_ch)) + } + LspInsertMode::ReplaceSuffix => { + let range_after_cursor = insert_range.end..completion.replace_range.end; + + let text_after_cursor = buffer + .text_for_range( + buffer.anchor_before(range_after_cursor.start) + ..buffer.anchor_after(range_after_cursor.end), + ) + .collect::(); + completion.new_text.ends_with(&text_after_cursor) + } + } + } + + let buffer = buffer.read(cx); + + if let CompletionSource::Lsp { + insert_range: Some(insert_range), + .. + } = &completion.source + { + let completion_mode_setting = + language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) + .completions + .lsp_insert_mode; + + if !should_replace( + completion, + &insert_range, + intent, + completion_mode_setting, + buffer, + ) { + return insert_range.to_offset(buffer); + } + } + + completion.replace_range.to_offset(buffer) +} + fn insert_extra_newline_brackets( buffer: &MultiBufferSnapshot, range: Range, @@ -18701,9 +18797,10 @@ fn snippet_completions( end: lsp_end, }; Some(Completion { - old_range: range, + replace_range: range, new_text: snippet.body.clone(), source: CompletionSource::Lsp { + insert_range: None, server_id: LanguageServerId(usize::MAX), resolved: true, lsp_completion: Box::new(lsp::CompletionItem { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b46798e5f11bdba4a0817cfb1da0bdff288b0677..915edc237fb84ef3a26743d370e1c54391dd1f2a 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -9218,7 +9218,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { initial_state: String, buffer_marked_text: String, completion_text: &'static str, - expected_with_insertion_mode: String, + expected_with_insert_mode: String, expected_with_replace_mode: String, expected_with_replace_subsequence_mode: String, expected_with_replace_suffix_mode: String, @@ -9230,7 +9230,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { initial_state: "before ediˇ after".into(), buffer_marked_text: "before after".into(), completion_text: "editor", - expected_with_insertion_mode: "before editorˇ after".into(), + expected_with_insert_mode: "before editorˇ after".into(), expected_with_replace_mode: "before editorˇ after".into(), expected_with_replace_subsequence_mode: "before editorˇ after".into(), expected_with_replace_suffix_mode: "before editorˇ after".into(), @@ -9240,7 +9240,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { initial_state: "before ediˇtor after".into(), buffer_marked_text: "before after".into(), completion_text: "editor", - expected_with_insertion_mode: "before editorˇtor after".into(), + expected_with_insert_mode: "before editorˇtor after".into(), expected_with_replace_mode: "before ediˇtor after".into(), expected_with_replace_subsequence_mode: "before ediˇtor after".into(), expected_with_replace_suffix_mode: "before ediˇtor after".into(), @@ -9250,7 +9250,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { initial_state: "before torˇ after".into(), buffer_marked_text: "before after".into(), completion_text: "editor", - expected_with_insertion_mode: "before editorˇ after".into(), + expected_with_insert_mode: "before editorˇ after".into(), expected_with_replace_mode: "before editorˇ after".into(), expected_with_replace_subsequence_mode: "before editorˇ after".into(), expected_with_replace_suffix_mode: "before editorˇ after".into(), @@ -9260,7 +9260,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { initial_state: "before ˇtor after".into(), buffer_marked_text: "before <|tor> after".into(), completion_text: "editor", - expected_with_insertion_mode: "before editorˇtor after".into(), + expected_with_insert_mode: "before editorˇtor after".into(), expected_with_replace_mode: "before editorˇ after".into(), expected_with_replace_subsequence_mode: "before editorˇ after".into(), expected_with_replace_suffix_mode: "before editorˇ after".into(), @@ -9270,7 +9270,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { initial_state: "pˇfield: bool".into(), buffer_marked_text: ": bool".into(), completion_text: "pub ", - expected_with_insertion_mode: "pub ˇfield: bool".into(), + expected_with_insert_mode: "pub ˇfield: bool".into(), expected_with_replace_mode: "pub ˇ: bool".into(), expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(), expected_with_replace_suffix_mode: "pub ˇfield: bool".into(), @@ -9280,7 +9280,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { initial_state: "[element_ˇelement_2]".into(), buffer_marked_text: "[]".into(), completion_text: "element_1", - expected_with_insertion_mode: "[element_1ˇelement_2]".into(), + expected_with_insert_mode: "[element_1ˇelement_2]".into(), expected_with_replace_mode: "[element_1ˇ]".into(), expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(), expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(), @@ -9290,7 +9290,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { initial_state: "[elˇelement]".into(), buffer_marked_text: "[]".into(), completion_text: "element", - expected_with_insertion_mode: "[elementˇelement]".into(), + expected_with_insert_mode: "[elementˇelement]".into(), expected_with_replace_mode: "[elˇement]".into(), expected_with_replace_subsequence_mode: "[elementˇelement]".into(), expected_with_replace_suffix_mode: "[elˇement]".into(), @@ -9300,7 +9300,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { initial_state: "SubˇError".into(), buffer_marked_text: "".into(), completion_text: "SubscriptionError", - expected_with_insertion_mode: "SubscriptionErrorˇError".into(), + expected_with_insert_mode: "SubscriptionErrorˇError".into(), expected_with_replace_mode: "SubscriptionErrorˇ".into(), expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(), expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(), @@ -9310,7 +9310,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { initial_state: "SubˇErr".into(), buffer_marked_text: "".into(), completion_text: "SubscriptionError", - expected_with_insertion_mode: "SubscriptionErrorˇErr".into(), + expected_with_insert_mode: "SubscriptionErrorˇErr".into(), expected_with_replace_mode: "SubscriptionErrorˇ".into(), expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(), expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(), @@ -9320,7 +9320,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { initial_state: "Suˇscrirr".into(), buffer_marked_text: "".into(), completion_text: "SubscriptionError", - expected_with_insertion_mode: "SubscriptionErrorˇscrirr".into(), + expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(), expected_with_replace_mode: "SubscriptionErrorˇ".into(), expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(), expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(), @@ -9330,7 +9330,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { initial_state: "foo(indˇix)".into(), buffer_marked_text: "foo()".into(), completion_text: "node_index", - expected_with_insertion_mode: "foo(node_indexˇix)".into(), + expected_with_insert_mode: "foo(node_indexˇix)".into(), expected_with_replace_mode: "foo(node_indexˇ)".into(), expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(), expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(), @@ -9339,7 +9339,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { for run in runs { let run_variations = [ - (LspInsertMode::Insert, run.expected_with_insertion_mode), + (LspInsertMode::Insert, run.expected_with_insert_mode), (LspInsertMode::Replace, run.expected_with_replace_mode), ( LspInsertMode::ReplaceSubsequence, @@ -9395,6 +9395,98 @@ async fn test_completion_mode(cx: &mut TestAppContext) { } } +#[gpui::test] +async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + let initial_state = "SubˇError"; + let buffer_marked_text = ""; + let completion_text = "SubscriptionError"; + let expected_with_insert_mode = "SubscriptionErrorˇError"; + let expected_with_replace_mode = "SubscriptionErrorˇ"; + + update_test_language_settings(&mut cx, |settings| { + settings.defaults.completions = Some(CompletionSettings { + words: WordsCompletionMode::Disabled, + // set the opposite here to ensure that the action is overriding the default behavior + lsp_insert_mode: LspInsertMode::Insert, + lsp: true, + lsp_fetch_timeout_ms: 0, + }); + }); + + cx.set_state(initial_state); + cx.update_editor(|editor, window, cx| { + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); + }); + + let counter = Arc::new(AtomicUsize::new(0)); + handle_completion_request_with_insert_and_replace( + &mut cx, + &buffer_marked_text, + vec![completion_text], + counter.clone(), + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + assert_eq!(counter.load(atomic::Ordering::Acquire), 1); + + let apply_additional_edits = cx.update_editor(|editor, window, cx| { + editor + .confirm_completion_replace(&ConfirmCompletionReplace, window, cx) + .unwrap() + }); + cx.assert_editor_state(&expected_with_replace_mode); + handle_resolve_completion_request(&mut cx, None).await; + apply_additional_edits.await.unwrap(); + + update_test_language_settings(&mut cx, |settings| { + settings.defaults.completions = Some(CompletionSettings { + words: WordsCompletionMode::Disabled, + // set the opposite here to ensure that the action is overriding the default behavior + lsp_insert_mode: LspInsertMode::Replace, + lsp: true, + lsp_fetch_timeout_ms: 0, + }); + }); + + cx.set_state(initial_state); + cx.update_editor(|editor, window, cx| { + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); + }); + handle_completion_request_with_insert_and_replace( + &mut cx, + &buffer_marked_text, + vec![completion_text], + counter.clone(), + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + assert_eq!(counter.load(atomic::Ordering::Acquire), 2); + + let apply_additional_edits = cx.update_editor(|editor, window, cx| { + editor + .confirm_completion_insert(&ConfirmCompletionInsert, window, cx) + .unwrap() + }); + cx.assert_editor_state(&expected_with_insert_mode); + handle_resolve_completion_request(&mut cx, None).await; + apply_additional_edits.await.unwrap(); +} + #[gpui::test] async fn test_completion(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4757353abebbc86255d50f0d62b9da7a19e28a7a..e9719b6a1fa0cf4501fd9d7387ce83c42601e536 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -461,6 +461,20 @@ impl EditorElement { cx.propagate(); } }); + register_action(editor, window, |editor, action, window, cx| { + if let Some(task) = editor.confirm_completion_replace(action, window, cx) { + task.detach_and_notify_err(window, cx); + } else { + cx.propagate(); + } + }); + register_action(editor, window, |editor, action, window, cx| { + if let Some(task) = editor.confirm_completion_insert(action, window, cx) { + task.detach_and_notify_err(window, cx); + } else { + cx.propagate(); + } + }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.compose_completion(action, window, cx) { task.detach_and_notify_err(window, cx); diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 40cafa1c8d34826ba78c1e0dcbc0e9bf122012fe..ca2c33419fc4262cbea8664b2dc54a3aaf02b512 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -370,7 +370,7 @@ fn default_words_completion_mode() -> WordsCompletionMode { } fn default_lsp_insert_mode() -> LspInsertMode { - LspInsertMode::Insert + LspInsertMode::ReplaceSuffix } fn default_lsp_fetch_timeout_ms() -> u64 { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 07a03d5381c6b5580e603a1f1e3356e13c0d93b2..20111ae688ad3ab3a3e7462bff4ee6c11de9e6c0 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -17,9 +17,7 @@ use gpui::{App, AsyncApp, Entity}; use language::{ Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, - language_settings::{ - AllLanguageSettings, InlayHintKind, LanguageSettings, LspInsertMode, language_settings, - }, + language_settings::{InlayHintKind, LanguageSettings, language_settings}, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, @@ -30,7 +28,6 @@ use lsp::{ LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities, OneOf, RenameOptions, ServerCapabilities, }; -use settings::Settings as _; use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature}; use std::{cmp::Reverse, mem, ops::Range, path::Path, sync::Arc}; use text::{BufferId, LineEnding}; @@ -2161,7 +2158,7 @@ impl LspCommand for GetCompletions { .map(Arc::new); let mut completion_edits = Vec::new(); - buffer.update(&mut cx, |buffer, cx| { + buffer.update(&mut cx, |buffer, _cx| { let snapshot = buffer.snapshot(); let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); @@ -2198,21 +2195,11 @@ impl LspCommand for GetCompletions { // If the language server provides a range to overwrite, then // check that the range is valid. Some(completion_text_edit) => { - let completion_mode = AllLanguageSettings::get_global(cx) - .defaults - .completions - .lsp_insert_mode; - - match parse_completion_text_edit( - &completion_text_edit, - &snapshot, - completion_mode, - ) { + match parse_completion_text_edit(&completion_text_edit, &snapshot) { Some(edit) => edit, None => return false, } } - // If the language server does not provide a range, then infer // the range based on the syntax tree. None => { @@ -2264,7 +2251,12 @@ impl LspCommand for GetCompletions { .as_ref() .unwrap_or(&lsp_completion.label) .clone(); - (range, text) + + ParsedCompletionEdit { + replace_range: range, + insert_range: None, + new_text: text, + } } }; @@ -2280,8 +2272,8 @@ impl LspCommand for GetCompletions { Ok(completions .into_iter() .zip(completion_edits) - .map(|(mut lsp_completion, (old_range, mut new_text))| { - LineEnding::normalize(&mut new_text); + .map(|(mut lsp_completion, mut edit)| { + LineEnding::normalize(&mut edit.new_text); if lsp_completion.data.is_none() { if let Some(default_data) = lsp_defaults .as_ref() @@ -2293,9 +2285,10 @@ impl LspCommand for GetCompletions { } } CoreCompletion { - old_range, - new_text, + replace_range: edit.replace_range, + new_text: edit.new_text, source: CompletionSource::Lsp { + insert_range: edit.insert_range, server_id, lsp_completion: Box::new(lsp_completion), lsp_defaults: lsp_defaults.clone(), @@ -2385,91 +2378,53 @@ impl LspCommand for GetCompletions { } } +pub struct ParsedCompletionEdit { + pub replace_range: Range, + pub insert_range: Option>, + pub new_text: String, +} + pub(crate) fn parse_completion_text_edit( edit: &lsp::CompletionTextEdit, snapshot: &BufferSnapshot, - completion_mode: LspInsertMode, -) -> Option<(Range, String)> { - match edit { - lsp::CompletionTextEdit::Edit(edit) => { - let range = range_from_lsp(edit.range); - let start = snapshot.clip_point_utf16(range.start, Bias::Left); - let end = snapshot.clip_point_utf16(range.end, Bias::Left); - if start != range.start.0 || end != range.end.0 { - log::info!("completion out of expected range"); - None - } else { - Some(( - snapshot.anchor_before(start)..snapshot.anchor_after(end), - edit.new_text.clone(), - )) - } - } - +) -> Option { + let (replace_range, insert_range, new_text) = match edit { + lsp::CompletionTextEdit::Edit(edit) => (edit.range, None, &edit.new_text), lsp::CompletionTextEdit::InsertAndReplace(edit) => { - let replace = match completion_mode { - LspInsertMode::Insert => false, - LspInsertMode::Replace => true, - LspInsertMode::ReplaceSubsequence => { - let range_to_replace = range_from_lsp(edit.replace); - - let start = snapshot.clip_point_utf16(range_to_replace.start, Bias::Left); - let end = snapshot.clip_point_utf16(range_to_replace.end, Bias::Left); - if start != range_to_replace.start.0 || end != range_to_replace.end.0 { - false - } else { - let mut completion_text = edit.new_text.chars(); - - let mut text_to_replace = snapshot.chars_for_range( - snapshot.anchor_before(start)..snapshot.anchor_after(end), - ); - - // is `text_to_replace` a subsequence of `completion_text` - text_to_replace.all(|needle_ch| { - completion_text.any(|haystack_ch| haystack_ch == needle_ch) - }) - } - } - LspInsertMode::ReplaceSuffix => { - let range_after_cursor = lsp::Range { - start: edit.insert.end, - end: edit.replace.end, - }; - let range_after_cursor = range_from_lsp(range_after_cursor); - - let start = snapshot.clip_point_utf16(range_after_cursor.start, Bias::Left); - let end = snapshot.clip_point_utf16(range_after_cursor.end, Bias::Left); - if start != range_after_cursor.start.0 || end != range_after_cursor.end.0 { - false - } else { - let text_after_cursor = snapshot - .text_for_range( - snapshot.anchor_before(start)..snapshot.anchor_after(end), - ) - .collect::(); - edit.new_text.ends_with(&text_after_cursor) - } - } - }; + (edit.replace, Some(edit.insert), &edit.new_text) + } + }; - let range = range_from_lsp(match replace { - true => edit.replace, - false => edit.insert, - }); + let replace_range = { + let range = range_from_lsp(replace_range); + let start = snapshot.clip_point_utf16(range.start, Bias::Left); + let end = snapshot.clip_point_utf16(range.end, Bias::Left); + if start != range.start.0 || end != range.end.0 { + log::info!("completion out of expected range"); + return None; + } + snapshot.anchor_before(start)..snapshot.anchor_after(end) + }; + let insert_range = match insert_range { + None => None, + Some(insert_range) => { + let range = range_from_lsp(insert_range); let start = snapshot.clip_point_utf16(range.start, Bias::Left); let end = snapshot.clip_point_utf16(range.end, Bias::Left); if start != range.start.0 || end != range.end.0 { - log::info!("completion out of expected range"); - None - } else { - Some(( - snapshot.anchor_before(start)..snapshot.anchor_after(end), - edit.new_text.clone(), - )) + log::info!("completion (insert) out of expected range"); + return None; } + Some(snapshot.anchor_before(start)..snapshot.anchor_after(end)) } - } + }; + + Some(ParsedCompletionEdit { + insert_range: insert_range, + replace_range: replace_range, + new_text: new_text.clone(), + }) } #[async_trait(?Send)] diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 15f168f79844a6950f8a6e7c1ff5b1e7c77019e1..09f902760ef450d1d67a3ae048e850a30d2c3e2a 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -39,8 +39,7 @@ use language::{ LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, language_settings::{ - AllLanguageSettings, FormatOnSave, Formatter, LanguageSettings, LspInsertMode, - SelectedFormatter, language_settings, + FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings, }, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, @@ -5136,7 +5135,6 @@ impl LspStore { &buffer_snapshot, completions.clone(), completion_index, - cx, ) .await .log_err() @@ -5170,7 +5168,6 @@ impl LspStore { snapshot: &BufferSnapshot, completions: Rc>>, completion_index: usize, - cx: &mut AsyncApp, ) -> Result<()> { let server_id = server.server_id(); let can_resolve = server @@ -5208,41 +5205,38 @@ impl LspStore { }; let resolved_completion = request.await?; + let mut updated_insert_range = None; if let Some(text_edit) = resolved_completion.text_edit.as_ref() { // Technically we don't have to parse the whole `text_edit`, since the only // language server we currently use that does update `text_edit` in `completionItem/resolve` // is `typescript-language-server` and they only update `text_edit.new_text`. // But we should not rely on that. - let completion_mode = cx - .read_global(|_: &SettingsStore, cx| { - AllLanguageSettings::get_global(cx) - .defaults - .completions - .lsp_insert_mode - }) - .unwrap_or(LspInsertMode::Insert); - let edit = parse_completion_text_edit(text_edit, snapshot, completion_mode); + let edit = parse_completion_text_edit(text_edit, snapshot); - if let Some((old_range, mut new_text)) = edit { - LineEnding::normalize(&mut new_text); + if let Some(mut parsed_edit) = edit { + LineEnding::normalize(&mut parsed_edit.new_text); let mut completions = completions.borrow_mut(); let completion = &mut completions[completion_index]; - completion.new_text = new_text; - completion.old_range = old_range; + completion.new_text = parsed_edit.new_text; + completion.replace_range = parsed_edit.replace_range; + + updated_insert_range = parsed_edit.insert_range; } } let mut completions = completions.borrow_mut(); let completion = &mut completions[completion_index]; if let CompletionSource::Lsp { + insert_range, lsp_completion, resolved, server_id: completion_server_id, .. } = &mut completion.source { + *insert_range = updated_insert_range; if *resolved { return Ok(()); } @@ -5384,12 +5378,19 @@ impl LspStore { let completion = &mut completions[completion_index]; completion.documentation = Some(documentation); if let CompletionSource::Lsp { + insert_range, lsp_completion, resolved, server_id: completion_server_id, lsp_defaults: _, } = &mut completion.source { + let completion_insert_range = response + .old_insert_start + .and_then(deserialize_anchor) + .zip(response.old_insert_end.and_then(deserialize_anchor)); + *insert_range = completion_insert_range.map(|(start, end)| start..end); + if *resolved { return Ok(()); } @@ -5401,14 +5402,14 @@ impl LspStore { *resolved = true; } - let old_range = response - .old_start + let replace_range = response + .old_replace_start .and_then(deserialize_anchor) - .zip(response.old_end.and_then(deserialize_anchor)); - if let Some((old_start, old_end)) = old_range { + .zip(response.old_replace_end.and_then(deserialize_anchor)); + if let Some((old_replace_start, old_replace_end)) = replace_range { if !response.new_text.is_empty() { completion.new_text = response.new_text; - completion.old_range = old_start..old_end; + completion.replace_range = old_replace_start..old_replace_end; } } @@ -5433,7 +5434,7 @@ impl LspStore { project_id, buffer_id: buffer_id.into(), completion: Some(Self::serialize_completion(&CoreCompletion { - old_range: completion.old_range, + replace_range: completion.replace_range, new_text: completion.new_text, source: completion.source, })), @@ -5477,7 +5478,6 @@ impl LspStore { &snapshot, completions.clone(), completion_index, - cx, ) .await .context("resolving completion")?; @@ -5505,7 +5505,7 @@ impl LspStore { buffer.start_transaction(); for (range, text) in edits { - let primary = &completion.old_range; + let primary = &completion.replace_range; let start_within = primary.start.cmp(&range.start, buffer).is_le() && primary.end.cmp(&range.start, buffer).is_ge(); let end_within = range.start.cmp(&primary.end, buffer).is_le() @@ -7709,8 +7709,10 @@ impl LspStore { // If we have a new buffer_id, that means we're talking to a new client // and want to check for new text_edits in the completion too. - let mut old_start = None; - let mut old_end = None; + let mut old_replace_start = None; + let mut old_replace_end = None; + let mut old_insert_start = None; + let mut old_insert_end = None; let mut new_text = String::default(); if let Ok(buffer_id) = BufferId::new(envelope.payload.buffer_id) { let buffer_snapshot = this.update(&mut cx, |this, cx| { @@ -7719,23 +7721,18 @@ impl LspStore { })??; if let Some(text_edit) = completion.text_edit.as_ref() { - let completion_mode = cx - .read_global(|_: &SettingsStore, cx| { - AllLanguageSettings::get_global(cx) - .defaults - .completions - .lsp_insert_mode - }) - .unwrap_or(LspInsertMode::Insert); + let edit = parse_completion_text_edit(text_edit, &buffer_snapshot); - let edit = parse_completion_text_edit(text_edit, &buffer_snapshot, completion_mode); + if let Some(mut edit) = edit { + LineEnding::normalize(&mut edit.new_text); - if let Some((old_range, mut text_edit_new_text)) = edit { - LineEnding::normalize(&mut text_edit_new_text); - - new_text = text_edit_new_text; - old_start = Some(serialize_anchor(&old_range.start)); - old_end = Some(serialize_anchor(&old_range.end)); + new_text = edit.new_text; + old_replace_start = Some(serialize_anchor(&edit.replace_range.start)); + old_replace_end = Some(serialize_anchor(&edit.replace_range.end)); + if let Some(insert_range) = edit.insert_range { + old_insert_start = Some(serialize_anchor(&insert_range.start)); + old_insert_end = Some(serialize_anchor(&insert_range.end)); + } } } } @@ -7743,10 +7740,12 @@ impl LspStore { Ok(proto::ResolveCompletionDocumentationResponse { documentation, documentation_is_markdown, - old_start, - old_end, + old_replace_start, + old_replace_end, new_text, lsp_completion, + old_insert_start, + old_insert_end, }) } @@ -8048,7 +8047,7 @@ impl LspStore { this.apply_additional_edits_for_completion( buffer, Rc::new(RefCell::new(Box::new([Completion { - old_range: completion.old_range, + replace_range: completion.replace_range, new_text: completion.new_text, source: completion.source, documentation: None, @@ -9103,18 +9102,26 @@ impl LspStore { pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion { let mut serialized_completion = proto::Completion { - old_start: Some(serialize_anchor(&completion.old_range.start)), - old_end: Some(serialize_anchor(&completion.old_range.end)), + old_replace_start: Some(serialize_anchor(&completion.replace_range.start)), + old_replace_end: Some(serialize_anchor(&completion.replace_range.end)), new_text: completion.new_text.clone(), ..proto::Completion::default() }; match &completion.source { CompletionSource::Lsp { + insert_range, server_id, lsp_completion, lsp_defaults, resolved, } => { + let (old_insert_start, old_insert_end) = insert_range + .as_ref() + .map(|range| (serialize_anchor(&range.start), serialize_anchor(&range.end))) + .unzip(); + + serialized_completion.old_insert_start = old_insert_start; + serialized_completion.old_insert_end = old_insert_end; serialized_completion.source = proto::completion::Source::Lsp as i32; serialized_completion.server_id = server_id.0 as u64; serialized_completion.lsp_completion = serde_json::to_vec(lsp_completion).unwrap(); @@ -9142,20 +9149,31 @@ impl LspStore { } pub(crate) fn deserialize_completion(completion: proto::Completion) -> Result { - let old_start = completion - .old_start + let old_replace_start = completion + .old_replace_start .and_then(deserialize_anchor) .context("invalid old start")?; - let old_end = completion - .old_end + let old_replace_end = completion + .old_replace_end .and_then(deserialize_anchor) .context("invalid old end")?; + let insert_range = { + match completion.old_insert_start.zip(completion.old_insert_end) { + Some((start, end)) => { + let start = deserialize_anchor(start).context("invalid insert old start")?; + let end = deserialize_anchor(end).context("invalid insert old end")?; + Some(start..end) + } + None => None, + } + }; Ok(CoreCompletion { - old_range: old_start..old_end, + replace_range: old_replace_start..old_replace_end, new_text: completion.new_text, source: match proto::completion::Source::from_i32(completion.source) { Some(proto::completion::Source::Custom) => CompletionSource::Custom, Some(proto::completion::Source::Lsp) => CompletionSource::Lsp { + insert_range, server_id: LanguageServerId::from_proto(completion.server_id), lsp_completion: serde_json::from_slice(&completion.lsp_completion)?, lsp_defaults: completion @@ -9344,7 +9362,7 @@ async fn populate_labels_for_completions( completions.push(Completion { label, documentation, - old_range: completion.old_range, + replace_range: completion.replace_range, new_text: completion.new_text, insert_text_mode: lsp_completion.insert_text_mode, source: completion.source, @@ -9358,7 +9376,7 @@ async fn populate_labels_for_completions( completions.push(Completion { label, documentation: None, - old_range: completion.old_range, + replace_range: completion.replace_range, new_text: completion.new_text, source: completion.source, insert_text_mode: None, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f2537f4c04d881a3911ec2b19143df7494798018..9dfef95f5de19b204add473937ea93160d4b850c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -359,8 +359,14 @@ pub struct InlayHint { #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)] pub enum CompletionIntent { /// The user intends to 'commit' this result, if possible - /// completion confirmations should run side effects + /// completion confirmations should run side effects. + /// + /// For LSP completions, will respect the setting `completions.lsp_insert_mode`. Complete, + /// Similar to [Self::Complete], but behaves like `lsp_insert_mode` is set to `insert`. + CompleteWithInsert, + /// Similar to [Self::Complete], but behaves like `lsp_insert_mode` is set to `replace`. + CompleteWithReplace, /// The user intends to continue 'composing' this completion /// completion confirmations should not run side effects and /// let the user continue composing their action @@ -377,11 +383,11 @@ impl CompletionIntent { } } -/// A completion provided by a language server +/// Similar to `CoreCompletion`, but with extra metadata attached. #[derive(Clone)] pub struct Completion { - /// The range of the buffer that will be replaced. - pub old_range: Range, + /// The range of text that will be replaced by this completion. + pub replace_range: Range, /// The new text that will be inserted. pub new_text: String, /// A label for this completion that is shown in the menu. @@ -404,6 +410,8 @@ pub struct Completion { #[derive(Debug, Clone)] pub enum CompletionSource { Lsp { + /// The alternate `insert` range, if provided by the LSP server. + insert_range: Option>, /// The id of the language server that produced this completion. server_id: LanguageServerId, /// The raw completion provided by the language server. @@ -508,7 +516,7 @@ impl CompletionSource { impl std::fmt::Debug for Completion { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Completion") - .field("old_range", &self.old_range) + .field("replace_range", &self.replace_range) .field("new_text", &self.new_text) .field("label", &self.label) .field("documentation", &self.documentation) @@ -517,10 +525,10 @@ impl std::fmt::Debug for Completion { } } -/// A completion provided by a language server +/// A generic completion that can come from different sources. #[derive(Clone, Debug)] pub(crate) struct CoreCompletion { - old_range: Range, + replace_range: Range, new_text: String, source: CompletionSource, } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index e0ad37835f566d72cf9313779ea05b5f4cbf61af..b499bec710a96e6336314ea570fc5ec21157bca1 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -3018,7 +3018,7 @@ async fn test_completions_with_text_edit(cx: &mut gpui::TestAppContext) { assert_eq!(completions.len(), 1); assert_eq!(completions[0].new_text, "textEditText"); assert_eq!( - completions[0].old_range.to_offset(&snapshot), + completions[0].replace_range.to_offset(&snapshot), text.len() - 3..text.len() ); } @@ -3101,7 +3101,7 @@ async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) { assert_eq!(completions.len(), 1); assert_eq!(completions[0].new_text, "insertText"); assert_eq!( - completions[0].old_range.to_offset(&snapshot), + completions[0].replace_range.to_offset(&snapshot), text.len() - 3..text.len() ); } @@ -3143,7 +3143,7 @@ async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) { assert_eq!(completions.len(), 1); assert_eq!(completions[0].new_text, "labelText"); assert_eq!( - completions[0].old_range.to_offset(&snapshot), + completions[0].replace_range.to_offset(&snapshot), text.len() - 3..text.len() ); } @@ -3213,7 +3213,7 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { assert_eq!(completions.len(), 1); assert_eq!(completions[0].new_text, "fullyQualifiedName"); assert_eq!( - completions[0].old_range.to_offset(&snapshot), + completions[0].replace_range.to_offset(&snapshot), text.len() - 3..text.len() ); @@ -3240,7 +3240,7 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { assert_eq!(completions.len(), 1); assert_eq!(completions[0].new_text, "component"); assert_eq!( - completions[0].old_range.to_offset(&snapshot), + completions[0].replace_range.to_offset(&snapshot), text.len() - 4..text.len() - 1 ); } diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 13dd3ca506a3fb77f60fdb42ef1a2d31ec8bde7e..918279e362750df1d8c005c876adb68f523e6a08 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -198,8 +198,8 @@ message ApplyCompletionAdditionalEditsResponse { } message Completion { - Anchor old_start = 1; - Anchor old_end = 2; + Anchor old_replace_start = 1; + Anchor old_replace_end = 2; string new_text = 3; uint64 server_id = 4; bytes lsp_completion = 5; @@ -208,6 +208,8 @@ message Completion { optional bytes lsp_defaults = 8; optional Anchor buffer_word_start = 9; optional Anchor buffer_word_end = 10; + Anchor old_insert_start = 11; + Anchor old_insert_end = 12; enum Source { Lsp = 0; @@ -428,10 +430,12 @@ message ResolveCompletionDocumentation { message ResolveCompletionDocumentationResponse { string documentation = 1; bool documentation_is_markdown = 2; - Anchor old_start = 3; - Anchor old_end = 4; + Anchor old_replace_start = 3; + Anchor old_replace_end = 4; string new_text = 5; bytes lsp_completion = 6; + Anchor old_insert_start = 7; + Anchor old_insert_end = 8; } message ResolveInlayHint {