Detailed changes
@@ -532,6 +532,7 @@
"context": "Editor && showing_completions",
"bindings": {
"enter": "editor::ConfirmCompletion",
+ "shift-enter": "editor::ConfirmCompletionReplace",
"tab": "editor::ComposeCompletion"
}
},
@@ -681,6 +681,7 @@
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
+ "shift-enter": "editor::ConfirmCompletionReplace",
"tab": "editor::ComposeCompletion"
}
},
@@ -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,
@@ -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()
@@ -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,
@@ -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(),
@@ -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,
@@ -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(),
@@ -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<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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<Task<Result<()>>> {
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::<usize>(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::<String>();
+ let old_text = buffer
+ .text_for_range(replace_range.clone())
+ .collect::<String>();
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<Buffer>,
+ cx: &mut Context<Editor>,
+) -> Range<usize> {
+ fn should_replace(
+ completion: &Completion,
+ insert_range: &Range<text::Anchor>,
+ 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::<String>();
+ 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<usize>,
@@ -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 {
@@ -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 <edi|> 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 <edi|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 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 <tor|> 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: "<p|field>: 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: "[<element_|element_2>]".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: "[<el|element>]".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: "<Sub|Error>".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: "<Sub|Err>".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: "<Su|scrirr>".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(<ind|ix>)".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 = "<Sub|Error>";
+ 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, |_| {});
@@ -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);
@@ -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 {
@@ -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<Anchor>,
+ pub insert_range: Option<Range<Anchor>>,
+ pub new_text: String,
+}
+
pub(crate) fn parse_completion_text_edit(
edit: &lsp::CompletionTextEdit,
snapshot: &BufferSnapshot,
- completion_mode: LspInsertMode,
-) -> Option<(Range<Anchor>, 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<ParsedCompletionEdit> {
+ 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::<String>();
- 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)]
@@ -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<RefCell<Box<[Completion]>>>,
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<CoreCompletion> {
- 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,
@@ -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<Anchor>,
+ /// The range of text that will be replaced by this completion.
+ pub replace_range: Range<Anchor>,
/// 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<Range<Anchor>>,
/// 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<Anchor>,
+ replace_range: Range<Anchor>,
new_text: String,
source: CompletionSource,
}
@@ -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
);
}
@@ -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 {