Add completions.lsp_insert_mode
setting to control what ranges are replaced when a completion is inserted (#27453)
This PR adds `completions.lsp_insert_mode` and effectively changes the default from `"replace"` to `"replace_suffix"`, which automatically detects whether to use the LSP `replace` range instead of `insert` range. `"replace_suffix"` was chosen as a default because it's more conservative than `"replace_subsequence"`, considering that deleting text is usually faster and less disruptive than having to rewrite a long replaced word. Fixes #27197 Fixes #23395 (again) Fixes #4816 (again) Release Notes: - Added new setting `completions.lsp_insert_mode` that changes what will be replaced when an LSP completion is accepted. The default is `"replace_suffix"`, but it accepts 4 values: `"insert"` for replacing only the text before the cursor, `"replace"` for replacing the whole text, `"replace_suffix"` that acts like `"replace"` when the text after the cursor is a suffix of the completion, and `"replace_subsequence"` that acts like `"replace"` when the text around your cursor is a subsequence of the completion (similiar to a fuzzy match). Check [the documentation](https://zed.dev/docs/configuring-zed#LSP-Insert-Mode) for more information. --------- Co-authored-by: João Marcos <marcospb19@hotmail.com> Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
parent
108ae0b5b0
commit
07a77792c5
7 changed files with 467 additions and 13 deletions
|
@ -1201,7 +1201,27 @@
|
||||||
// When set to 0, waits indefinitely.
|
// When set to 0, waits indefinitely.
|
||||||
//
|
//
|
||||||
// Default: 0
|
// Default: 0
|
||||||
"lsp_fetch_timeout_ms": 0
|
"lsp_fetch_timeout_ms": 0,
|
||||||
|
// Controls what range to replace when accepting LSP completions.
|
||||||
|
//
|
||||||
|
// When LSP servers give an `InsertReplaceEdit` completion, they provides two ranges: `insert` and `replace`. Usually, `insert`
|
||||||
|
// contains the word prefix before your cursor and `replace` contains the whole word.
|
||||||
|
//
|
||||||
|
// Effectively, this setting just changes whether Zed will use the received range for `insert` or `replace`, so the results may
|
||||||
|
// differ depending on the underlying LSP server.
|
||||||
|
//
|
||||||
|
// Possible values:
|
||||||
|
// 1. "insert"
|
||||||
|
// Replaces text before the cursor, using the `insert` range described in the LSP specification.
|
||||||
|
// 2. "replace"
|
||||||
|
// Replaces text before and after the cursor, using the `replace` range described in the LSP specification.
|
||||||
|
// 3. "replace_subsequence"
|
||||||
|
// Behaves like `"replace"` if the text that would be replaced is a subsequence of the completion text,
|
||||||
|
// and like `"insert"` otherwise.
|
||||||
|
// 4. "replace_suffix"
|
||||||
|
// Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like
|
||||||
|
// `"insert"` otherwise.
|
||||||
|
"lsp_insert_mode": "replace_suffix"
|
||||||
},
|
},
|
||||||
// Different settings for specific languages.
|
// Different settings for specific languages.
|
||||||
"languages": {
|
"languages": {
|
||||||
|
|
|
@ -273,7 +273,7 @@ mod tests {
|
||||||
use language::{
|
use language::{
|
||||||
Point,
|
Point,
|
||||||
language_settings::{
|
language_settings::{
|
||||||
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
|
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, LspInsertMode,
|
||||||
WordsCompletionMode,
|
WordsCompletionMode,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -294,6 +294,7 @@ mod tests {
|
||||||
words: WordsCompletionMode::Disabled,
|
words: WordsCompletionMode::Disabled,
|
||||||
lsp: true,
|
lsp: true,
|
||||||
lsp_fetch_timeout_ms: 0,
|
lsp_fetch_timeout_ms: 0,
|
||||||
|
lsp_insert_mode: LspInsertMode::Insert,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -525,6 +526,7 @@ mod tests {
|
||||||
words: WordsCompletionMode::Disabled,
|
words: WordsCompletionMode::Disabled,
|
||||||
lsp: true,
|
lsp: true,
|
||||||
lsp_fetch_timeout_ms: 0,
|
lsp_fetch_timeout_ms: 0,
|
||||||
|
lsp_insert_mode: LspInsertMode::Insert,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ use language::{
|
||||||
Override, Point,
|
Override, Point,
|
||||||
language_settings::{
|
language_settings::{
|
||||||
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
|
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
|
||||||
LanguageSettingsContent, PrettierSettings,
|
LanguageSettingsContent, LspInsertMode, PrettierSettings,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
|
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
|
||||||
|
@ -6382,7 +6382,7 @@ async fn test_autoindent_selections(cx: &mut TestAppContext) {
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
|
|
||||||
cx.update(|_, cx| {
|
cx.update(|_, cx| {
|
||||||
pretty_assertions::assert_eq!(
|
assert_eq!(
|
||||||
buffer.read(cx).text(),
|
buffer.read(cx).text(),
|
||||||
indoc! { "
|
indoc! { "
|
||||||
impl A {
|
impl A {
|
||||||
|
@ -9198,6 +9198,203 @@ async fn test_signature_help(cx: &mut TestAppContext) {
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_completion_mode(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;
|
||||||
|
|
||||||
|
struct Run {
|
||||||
|
run_description: &'static str,
|
||||||
|
initial_state: String,
|
||||||
|
buffer_marked_text: String,
|
||||||
|
completion_text: &'static str,
|
||||||
|
expected_with_insertion_mode: String,
|
||||||
|
expected_with_replace_mode: String,
|
||||||
|
expected_with_replace_subsequence_mode: String,
|
||||||
|
expected_with_replace_suffix_mode: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let runs = [
|
||||||
|
Run {
|
||||||
|
run_description: "Start of word matches completion text",
|
||||||
|
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_replace_mode: "before editorˇ after".into(),
|
||||||
|
expected_with_replace_subsequence_mode: "before editorˇ after".into(),
|
||||||
|
expected_with_replace_suffix_mode: "before editorˇ after".into(),
|
||||||
|
},
|
||||||
|
Run {
|
||||||
|
run_description: "Accept same text at the middle of the word",
|
||||||
|
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_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(),
|
||||||
|
},
|
||||||
|
Run {
|
||||||
|
run_description: "End of word matches completion text -- cursor at end",
|
||||||
|
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_replace_mode: "before editorˇ after".into(),
|
||||||
|
expected_with_replace_subsequence_mode: "before editorˇ after".into(),
|
||||||
|
expected_with_replace_suffix_mode: "before editorˇ after".into(),
|
||||||
|
},
|
||||||
|
Run {
|
||||||
|
run_description: "End of word matches completion text -- cursor at start",
|
||||||
|
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_replace_mode: "before editorˇ after".into(),
|
||||||
|
expected_with_replace_subsequence_mode: "before editorˇ after".into(),
|
||||||
|
expected_with_replace_suffix_mode: "before editorˇ after".into(),
|
||||||
|
},
|
||||||
|
Run {
|
||||||
|
run_description: "Prepend text containing whitespace",
|
||||||
|
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_replace_mode: "pub ˇ: bool".into(),
|
||||||
|
expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
|
||||||
|
expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
|
||||||
|
},
|
||||||
|
Run {
|
||||||
|
run_description: "Add element to start of list",
|
||||||
|
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_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(),
|
||||||
|
},
|
||||||
|
Run {
|
||||||
|
run_description: "Add element to start of list -- first and second elements are equal",
|
||||||
|
initial_state: "[elˇelement]".into(),
|
||||||
|
buffer_marked_text: "[<el|element>]".into(),
|
||||||
|
completion_text: "element",
|
||||||
|
expected_with_insertion_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(),
|
||||||
|
},
|
||||||
|
Run {
|
||||||
|
run_description: "Ends with matching suffix",
|
||||||
|
initial_state: "SubˇError".into(),
|
||||||
|
buffer_marked_text: "<Sub|Error>".into(),
|
||||||
|
completion_text: "SubscriptionError",
|
||||||
|
expected_with_insertion_mode: "SubscriptionErrorˇError".into(),
|
||||||
|
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
|
||||||
|
expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
|
||||||
|
expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
|
||||||
|
},
|
||||||
|
Run {
|
||||||
|
run_description: "Suffix is a subsequence -- contiguous",
|
||||||
|
initial_state: "SubˇErr".into(),
|
||||||
|
buffer_marked_text: "<Sub|Err>".into(),
|
||||||
|
completion_text: "SubscriptionError",
|
||||||
|
expected_with_insertion_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(),
|
||||||
|
},
|
||||||
|
Run {
|
||||||
|
run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
|
||||||
|
initial_state: "Suˇscrirr".into(),
|
||||||
|
buffer_marked_text: "<Su|scrirr>".into(),
|
||||||
|
completion_text: "SubscriptionError",
|
||||||
|
expected_with_insertion_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(),
|
||||||
|
},
|
||||||
|
Run {
|
||||||
|
run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
|
||||||
|
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_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(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for run in runs {
|
||||||
|
let run_variations = [
|
||||||
|
(LspInsertMode::Insert, run.expected_with_insertion_mode),
|
||||||
|
(LspInsertMode::Replace, run.expected_with_replace_mode),
|
||||||
|
(
|
||||||
|
LspInsertMode::ReplaceSubsequence,
|
||||||
|
run.expected_with_replace_subsequence_mode,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
LspInsertMode::ReplaceSuffix,
|
||||||
|
run.expected_with_replace_suffix_mode,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (lsp_insert_mode, expected_text) in run_variations {
|
||||||
|
eprintln!(
|
||||||
|
"run = {:?}, mode = {lsp_insert_mode:.?}",
|
||||||
|
run.run_description,
|
||||||
|
);
|
||||||
|
|
||||||
|
update_test_language_settings(&mut cx, |settings| {
|
||||||
|
settings.defaults.completions = Some(CompletionSettings {
|
||||||
|
lsp_insert_mode,
|
||||||
|
words: WordsCompletionMode::Disabled,
|
||||||
|
lsp: true,
|
||||||
|
lsp_fetch_timeout_ms: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.set_state(&run.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,
|
||||||
|
&run.buffer_marked_text,
|
||||||
|
vec![run.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(&ConfirmCompletion::default(), window, cx)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(&expected_text);
|
||||||
|
handle_resolve_completion_request(&mut cx, None).await;
|
||||||
|
apply_additional_edits.await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_completion(cx: &mut TestAppContext) {
|
async fn test_completion(cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
@ -9419,6 +9616,7 @@ async fn test_word_completion(cx: &mut TestAppContext) {
|
||||||
words: WordsCompletionMode::Fallback,
|
words: WordsCompletionMode::Fallback,
|
||||||
lsp: true,
|
lsp: true,
|
||||||
lsp_fetch_timeout_ms: 10,
|
lsp_fetch_timeout_ms: 10,
|
||||||
|
lsp_insert_mode: LspInsertMode::Insert,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9514,6 +9712,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext
|
||||||
words: WordsCompletionMode::Enabled,
|
words: WordsCompletionMode::Enabled,
|
||||||
lsp: true,
|
lsp: true,
|
||||||
lsp_fetch_timeout_ms: 0,
|
lsp_fetch_timeout_ms: 0,
|
||||||
|
lsp_insert_mode: LspInsertMode::Insert,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9576,6 +9775,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
|
||||||
words: WordsCompletionMode::Disabled,
|
words: WordsCompletionMode::Disabled,
|
||||||
lsp: true,
|
lsp: true,
|
||||||
lsp_fetch_timeout_ms: 0,
|
lsp_fetch_timeout_ms: 0,
|
||||||
|
lsp_insert_mode: LspInsertMode::Insert,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9648,6 +9848,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
|
||||||
words: WordsCompletionMode::Fallback,
|
words: WordsCompletionMode::Fallback,
|
||||||
lsp: false,
|
lsp: false,
|
||||||
lsp_fetch_timeout_ms: 0,
|
lsp_fetch_timeout_ms: 0,
|
||||||
|
lsp_insert_mode: LspInsertMode::Insert,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -18482,7 +18683,10 @@ pub fn handle_signature_help_request(
|
||||||
|
|
||||||
/// Handle completion request passing a marked string specifying where the completion
|
/// Handle completion request passing a marked string specifying where the completion
|
||||||
/// should be triggered from using '|' character, what range should be replaced, and what completions
|
/// should be triggered from using '|' character, what range should be replaced, and what completions
|
||||||
/// should be returned using '<' and '>' to delimit the range
|
/// should be returned using '<' and '>' to delimit the range.
|
||||||
|
///
|
||||||
|
/// Also see `handle_completion_request_with_insert_and_replace`.
|
||||||
|
#[track_caller]
|
||||||
pub fn handle_completion_request(
|
pub fn handle_completion_request(
|
||||||
cx: &mut EditorLspTestContext,
|
cx: &mut EditorLspTestContext,
|
||||||
marked_string: &str,
|
marked_string: &str,
|
||||||
|
@ -18532,6 +18736,66 @@ pub fn handle_completion_request(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
|
||||||
|
/// given instead, which also contains an `insert` range.
|
||||||
|
///
|
||||||
|
/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
|
||||||
|
/// that is, `replace_range.start..cursor_pos`.
|
||||||
|
pub fn handle_completion_request_with_insert_and_replace(
|
||||||
|
cx: &mut EditorLspTestContext,
|
||||||
|
marked_string: &str,
|
||||||
|
completions: Vec<&'static str>,
|
||||||
|
counter: Arc<AtomicUsize>,
|
||||||
|
) -> impl Future<Output = ()> {
|
||||||
|
let complete_from_marker: TextRangeMarker = '|'.into();
|
||||||
|
let replace_range_marker: TextRangeMarker = ('<', '>').into();
|
||||||
|
let (_, mut marked_ranges) = marked_text_ranges_by(
|
||||||
|
marked_string,
|
||||||
|
vec![complete_from_marker.clone(), replace_range_marker.clone()],
|
||||||
|
);
|
||||||
|
|
||||||
|
let complete_from_position =
|
||||||
|
cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
|
||||||
|
let replace_range =
|
||||||
|
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
|
||||||
|
|
||||||
|
let mut request =
|
||||||
|
cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
|
||||||
|
let completions = completions.clone();
|
||||||
|
counter.fetch_add(1, atomic::Ordering::Release);
|
||||||
|
async move {
|
||||||
|
assert_eq!(params.text_document_position.text_document.uri, url.clone());
|
||||||
|
assert_eq!(
|
||||||
|
params.text_document_position.position, complete_from_position,
|
||||||
|
"marker `|` position doesn't match",
|
||||||
|
);
|
||||||
|
Ok(Some(lsp::CompletionResponse::Array(
|
||||||
|
completions
|
||||||
|
.iter()
|
||||||
|
.map(|completion_text| lsp::CompletionItem {
|
||||||
|
label: completion_text.to_string(),
|
||||||
|
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||||
|
lsp::InsertReplaceEdit {
|
||||||
|
insert: lsp::Range {
|
||||||
|
start: replace_range.start,
|
||||||
|
end: complete_from_position,
|
||||||
|
},
|
||||||
|
replace: replace_range,
|
||||||
|
new_text: completion_text.to_string(),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async move {
|
||||||
|
request.next().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_resolve_completion_request(
|
fn handle_resolve_completion_request(
|
||||||
cx: &mut EditorLspTestContext,
|
cx: &mut EditorLspTestContext,
|
||||||
edits: Option<Vec<(&'static str, &'static str)>>,
|
edits: Option<Vec<(&'static str, &'static str)>>,
|
||||||
|
|
|
@ -61,7 +61,7 @@ pub fn all_language_settings<'a>(
|
||||||
pub struct AllLanguageSettings {
|
pub struct AllLanguageSettings {
|
||||||
/// The edit prediction settings.
|
/// The edit prediction settings.
|
||||||
pub edit_predictions: EditPredictionSettings,
|
pub edit_predictions: EditPredictionSettings,
|
||||||
defaults: LanguageSettings,
|
pub defaults: LanguageSettings,
|
||||||
languages: HashMap<LanguageName, LanguageSettings>,
|
languages: HashMap<LanguageName, LanguageSettings>,
|
||||||
pub(crate) file_types: HashMap<Arc<str>, GlobSet>,
|
pub(crate) file_types: HashMap<Arc<str>, GlobSet>,
|
||||||
}
|
}
|
||||||
|
@ -329,6 +329,11 @@ pub struct CompletionSettings {
|
||||||
/// Default: 0
|
/// Default: 0
|
||||||
#[serde(default = "default_lsp_fetch_timeout_ms")]
|
#[serde(default = "default_lsp_fetch_timeout_ms")]
|
||||||
pub lsp_fetch_timeout_ms: u64,
|
pub lsp_fetch_timeout_ms: u64,
|
||||||
|
/// Controls how LSP completions are inserted.
|
||||||
|
///
|
||||||
|
/// Default: "replace_suffix"
|
||||||
|
#[serde(default = "default_lsp_insert_mode")]
|
||||||
|
pub lsp_insert_mode: LspInsertMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Controls how document's words are completed.
|
/// Controls how document's words are completed.
|
||||||
|
@ -345,10 +350,29 @@ pub enum WordsCompletionMode {
|
||||||
Disabled,
|
Disabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum LspInsertMode {
|
||||||
|
/// Replaces text before the cursor, using the `insert` range described in the LSP specification.
|
||||||
|
Insert,
|
||||||
|
/// Replaces text before and after the cursor, using the `replace` range described in the LSP specification.
|
||||||
|
Replace,
|
||||||
|
/// Behaves like `"replace"` if the text that would be replaced is a subsequence of the completion text,
|
||||||
|
/// and like `"insert"` otherwise.
|
||||||
|
ReplaceSubsequence,
|
||||||
|
/// Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like
|
||||||
|
/// `"insert"` otherwise.
|
||||||
|
ReplaceSuffix,
|
||||||
|
}
|
||||||
|
|
||||||
fn default_words_completion_mode() -> WordsCompletionMode {
|
fn default_words_completion_mode() -> WordsCompletionMode {
|
||||||
WordsCompletionMode::Fallback
|
WordsCompletionMode::Fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_lsp_insert_mode() -> LspInsertMode {
|
||||||
|
LspInsertMode::Insert
|
||||||
|
}
|
||||||
|
|
||||||
fn default_lsp_fetch_timeout_ms() -> u64 {
|
fn default_lsp_fetch_timeout_ms() -> u64 {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,9 @@ use gpui::{App, AsyncApp, Entity};
|
||||||
use language::{
|
use language::{
|
||||||
Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, OffsetRangeExt, PointUtf16,
|
Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, OffsetRangeExt, PointUtf16,
|
||||||
ToOffset, ToPointUtf16, Transaction, Unclipped,
|
ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||||
language_settings::{InlayHintKind, LanguageSettings, language_settings},
|
language_settings::{
|
||||||
|
AllLanguageSettings, InlayHintKind, LanguageSettings, LspInsertMode, language_settings,
|
||||||
|
},
|
||||||
point_from_lsp, point_to_lsp,
|
point_from_lsp, point_to_lsp,
|
||||||
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
||||||
range_from_lsp, range_to_lsp,
|
range_from_lsp, range_to_lsp,
|
||||||
|
@ -28,6 +30,7 @@ use lsp::{
|
||||||
LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities, OneOf, RenameOptions,
|
LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities, OneOf, RenameOptions,
|
||||||
ServerCapabilities,
|
ServerCapabilities,
|
||||||
};
|
};
|
||||||
|
use settings::Settings as _;
|
||||||
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
|
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
|
||||||
use std::{cmp::Reverse, mem, ops::Range, path::Path, sync::Arc};
|
use std::{cmp::Reverse, mem, ops::Range, path::Path, sync::Arc};
|
||||||
use text::{BufferId, LineEnding};
|
use text::{BufferId, LineEnding};
|
||||||
|
@ -2085,7 +2088,7 @@ impl LspCommand for GetCompletions {
|
||||||
.map(Arc::new);
|
.map(Arc::new);
|
||||||
|
|
||||||
let mut completion_edits = Vec::new();
|
let mut completion_edits = Vec::new();
|
||||||
buffer.update(&mut cx, |buffer, _cx| {
|
buffer.update(&mut cx, |buffer, cx| {
|
||||||
let snapshot = buffer.snapshot();
|
let snapshot = buffer.snapshot();
|
||||||
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
|
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
|
||||||
|
|
||||||
|
@ -2122,7 +2125,16 @@ impl LspCommand for GetCompletions {
|
||||||
// If the language server provides a range to overwrite, then
|
// If the language server provides a range to overwrite, then
|
||||||
// check that the range is valid.
|
// check that the range is valid.
|
||||||
Some(completion_text_edit) => {
|
Some(completion_text_edit) => {
|
||||||
match parse_completion_text_edit(&completion_text_edit, &snapshot) {
|
let completion_mode = AllLanguageSettings::get_global(cx)
|
||||||
|
.defaults
|
||||||
|
.completions
|
||||||
|
.lsp_insert_mode;
|
||||||
|
|
||||||
|
match parse_completion_text_edit(
|
||||||
|
&completion_text_edit,
|
||||||
|
&snapshot,
|
||||||
|
completion_mode,
|
||||||
|
) {
|
||||||
Some(edit) => edit,
|
Some(edit) => edit,
|
||||||
None => return false,
|
None => return false,
|
||||||
}
|
}
|
||||||
|
@ -2303,6 +2315,7 @@ impl LspCommand for GetCompletions {
|
||||||
pub(crate) fn parse_completion_text_edit(
|
pub(crate) fn parse_completion_text_edit(
|
||||||
edit: &lsp::CompletionTextEdit,
|
edit: &lsp::CompletionTextEdit,
|
||||||
snapshot: &BufferSnapshot,
|
snapshot: &BufferSnapshot,
|
||||||
|
completion_mode: LspInsertMode,
|
||||||
) -> Option<(Range<Anchor>, String)> {
|
) -> Option<(Range<Anchor>, String)> {
|
||||||
match edit {
|
match edit {
|
||||||
lsp::CompletionTextEdit::Edit(edit) => {
|
lsp::CompletionTextEdit::Edit(edit) => {
|
||||||
|
@ -2321,7 +2334,55 @@ pub(crate) fn parse_completion_text_edit(
|
||||||
}
|
}
|
||||||
|
|
||||||
lsp::CompletionTextEdit::InsertAndReplace(edit) => {
|
lsp::CompletionTextEdit::InsertAndReplace(edit) => {
|
||||||
let range = range_from_lsp(edit.replace);
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let range = range_from_lsp(match replace {
|
||||||
|
true => edit.replace,
|
||||||
|
false => edit.insert,
|
||||||
|
});
|
||||||
|
|
||||||
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
||||||
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
|
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
|
||||||
|
|
|
@ -39,7 +39,8 @@ use language::{
|
||||||
LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16,
|
LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16,
|
||||||
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||||
language_settings::{
|
language_settings::{
|
||||||
FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings,
|
AllLanguageSettings, FormatOnSave, Formatter, LanguageSettings, LspInsertMode,
|
||||||
|
SelectedFormatter, language_settings,
|
||||||
},
|
},
|
||||||
point_to_lsp,
|
point_to_lsp,
|
||||||
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
||||||
|
@ -5151,6 +5152,7 @@ impl LspStore {
|
||||||
&buffer_snapshot,
|
&buffer_snapshot,
|
||||||
completions.clone(),
|
completions.clone(),
|
||||||
completion_index,
|
completion_index,
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.log_err()
|
.log_err()
|
||||||
|
@ -5184,6 +5186,7 @@ impl LspStore {
|
||||||
snapshot: &BufferSnapshot,
|
snapshot: &BufferSnapshot,
|
||||||
completions: Rc<RefCell<Box<[Completion]>>>,
|
completions: Rc<RefCell<Box<[Completion]>>>,
|
||||||
completion_index: usize,
|
completion_index: usize,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let server_id = server.server_id();
|
let server_id = server.server_id();
|
||||||
let can_resolve = server
|
let can_resolve = server
|
||||||
|
@ -5226,7 +5229,15 @@ impl LspStore {
|
||||||
// language server we currently use that does update `text_edit` in `completionItem/resolve`
|
// 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`.
|
// is `typescript-language-server` and they only update `text_edit.new_text`.
|
||||||
// But we should not rely on that.
|
// But we should not rely on that.
|
||||||
let edit = parse_completion_text_edit(text_edit, snapshot);
|
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);
|
||||||
|
|
||||||
if let Some((old_range, mut new_text)) = edit {
|
if let Some((old_range, mut new_text)) = edit {
|
||||||
LineEnding::normalize(&mut new_text);
|
LineEnding::normalize(&mut new_text);
|
||||||
|
@ -5482,6 +5493,7 @@ impl LspStore {
|
||||||
&snapshot,
|
&snapshot,
|
||||||
completions.clone(),
|
completions.clone(),
|
||||||
completion_index,
|
completion_index,
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.context("resolving completion")?;
|
.context("resolving completion")?;
|
||||||
|
@ -7723,7 +7735,16 @@ impl LspStore {
|
||||||
})??;
|
})??;
|
||||||
|
|
||||||
if let Some(text_edit) = completion.text_edit.as_ref() {
|
if let Some(text_edit) = completion.text_edit.as_ref() {
|
||||||
let edit = parse_completion_text_edit(text_edit, &buffer_snapshot);
|
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, completion_mode);
|
||||||
|
|
||||||
if let Some((old_range, mut text_edit_new_text)) = edit {
|
if let Some((old_range, mut text_edit_new_text)) = edit {
|
||||||
LineEnding::normalize(&mut text_edit_new_text);
|
LineEnding::normalize(&mut text_edit_new_text);
|
||||||
|
|
|
@ -2051,6 +2051,68 @@ Examples:
|
||||||
|
|
||||||
`boolean` values
|
`boolean` values
|
||||||
|
|
||||||
|
## Completions
|
||||||
|
|
||||||
|
- Description: Controls how completions are processed for this language.
|
||||||
|
- Setting: `completions`
|
||||||
|
- Default:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"completions": {
|
||||||
|
"words": "fallback",
|
||||||
|
"lsp": true,
|
||||||
|
"lsp_fetch_timeout_ms": 0,
|
||||||
|
"lsp_insert_mode": "replace_suffix"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Words
|
||||||
|
|
||||||
|
- Description: Controls how words are completed. For large documents, not all words may be fetched for completion.
|
||||||
|
- Setting: `words`
|
||||||
|
- Default: `fallback`
|
||||||
|
|
||||||
|
**Options**
|
||||||
|
|
||||||
|
1. `enabled` - Always fetch document's words for completions along with LSP completions
|
||||||
|
2. `fallback` - Only if LSP response errors or times out, use document's words to show completions
|
||||||
|
3. `disabled` - Never fetch or complete document's words for completions (word-based completions can still be queried via a separate action)
|
||||||
|
|
||||||
|
### LSP
|
||||||
|
|
||||||
|
- Description: Whether to fetch LSP completions or not.
|
||||||
|
- Setting: `lsp`
|
||||||
|
- Default: `true`
|
||||||
|
|
||||||
|
**Options**
|
||||||
|
|
||||||
|
`boolean` values
|
||||||
|
|
||||||
|
### LSP Fetch Timeout (ms)
|
||||||
|
|
||||||
|
- Description: When fetching LSP completions, determines how long to wait for a response of a particular server. When set to 0, waits indefinitely.
|
||||||
|
- Setting: `lsp_fetch_timeout_ms`
|
||||||
|
- Default: `0`
|
||||||
|
|
||||||
|
**Options**
|
||||||
|
|
||||||
|
`integer` values representing milliseconds
|
||||||
|
|
||||||
|
### LSP Insert Mode
|
||||||
|
|
||||||
|
- Description: Controls what range to replace when accepting LSP completions.
|
||||||
|
- Setting: `lsp_insert_mode`
|
||||||
|
- Default: `replace_suffix`
|
||||||
|
|
||||||
|
**Options**
|
||||||
|
|
||||||
|
1. `insert` - Replaces text before the cursor, using the `insert` range described in the LSP specification
|
||||||
|
2. `replace` - Replaces text before and after the cursor, using the `replace` range described in the LSP specification
|
||||||
|
3. `replace_subsequence` - Behaves like `"replace"` if the text that would be replaced is a subsequence of the completion text, and like `"insert"` otherwise
|
||||||
|
4. `replace_suffix` - Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like `"insert"` otherwise
|
||||||
|
|
||||||
## Show Completions On Input
|
## Show Completions On Input
|
||||||
|
|
||||||
- Description: Whether or not to show completions as you type.
|
- Description: Whether or not to show completions as you type.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue