Detailed changes
@@ -336,14 +336,14 @@
"active_line_width": 1,
// Determines how indent guides are colored.
// This setting can take the following three values:
- ///
+ //
// 1. "disabled"
// 2. "fixed"
// 3. "indent_aware"
"coloring": "fixed",
// Determines how indent guide backgrounds are colored.
// This setting can take the following two values:
- ///
+ //
// 1. "disabled"
// 2. "indent_aware"
"background_coloring": "disabled"
@@ -402,8 +402,8 @@
// Time to wait after scrolling the buffer, before requesting the hints,
// set to 0 to disable debouncing.
"scroll_debounce_ms": 50,
- /// A set of modifiers which, when pressed, will toggle the visibility of inlay hints.
- /// If the set if empty or not all the modifiers specified are pressed, inlay hints will not be toggled.
+ // A set of modifiers which, when pressed, will toggle the visibility of inlay hints.
+ // If the set if empty or not all the modifiers specified are pressed, inlay hints will not be toggled.
"toggle_on_modifiers_press": {
"control": false,
"shift": false,
@@ -440,7 +440,7 @@
"scrollbar": {
// When to show the scrollbar in the project panel.
// This setting can take five values:
- ///
+ //
// 1. null (default): Inherit editor settings
// 2. Show the scrollbar if there's important information or
// follow the system's configured behavior (default):
@@ -455,7 +455,7 @@
},
// Which files containing diagnostic errors/warnings to mark in the project panel.
// This setting can take the following three values:
- ///
+ //
// 1. Do not mark any files:
// "off"
// 2. Only mark files with errors:
@@ -512,7 +512,7 @@
"scrollbar": {
// When to show the scrollbar in the project panel.
// This setting can take five values:
- ///
+ //
// 1. null (default): Inherit editor settings
// 2. Show the scrollbar if there's important information or
// follow the system's configured behavior (default):
@@ -686,7 +686,7 @@
// Which files containing diagnostic errors/warnings to mark in the tabs.
// Diagnostics are only shown when file icons are also active.
// This setting only works when can take the following three values:
- ///
+ //
// 1. Do not mark any files:
// "off"
// 2. Only mark files with errors:
@@ -1014,7 +1014,7 @@
"scrollbar": {
// When to show the scrollbar in the terminal.
// This setting can take five values:
- ///
+ //
// 1. null (default): Inherit editor settings
// 2. Show the scrollbar if there's important information or
// follow the system's configured behavior (default):
@@ -1085,6 +1085,31 @@
"auto_install_extensions": {
"html": true
},
+ // Controls how completions are processed for this language.
+ "completions": {
+ // Controls how words are completed.
+ // For large documents, not all words may be fetched for completion.
+ //
+ // May take 3 values:
+ // 1. "enabled"
+ // Always fetch document's words for completions.
+ // 2. "fallback"
+ // Only if LSP response errors/times out/is empty, use document's words to show completions.
+ // 3. "disabled"
+ // Never fetch or complete document's words for completions.
+ //
+ // Default: fallback
+ "words": "fallback",
+ // Whether to fetch LSP completions or not.
+ //
+ // Default: true
+ "lsp": true,
+ // When fetching LSP completions, determines how long to wait for a response of a particular server.
+ // When set to 0, waits indefinitely.
+ //
+ // Default: 500
+ "lsp_fetch_timeout_ms": 500
+ },
// Different settings for specific languages.
"languages": {
"Astro": {
@@ -271,7 +271,10 @@ mod tests {
use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal};
use indoc::indoc;
use language::{
- language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
+ language_settings::{
+ AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
+ WordsCompletionMode,
+ },
Point,
};
use project::Project;
@@ -286,7 +289,13 @@ mod tests {
#[gpui::test(iterations = 10)]
async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
// flaky
- init_test(cx, |_| {});
+ init_test(cx, |settings| {
+ settings.defaults.completions = Some(CompletionSettings {
+ words: WordsCompletionMode::Disabled,
+ lsp: true,
+ lsp_fetch_timeout_ms: 0,
+ });
+ });
let (copilot, copilot_lsp) = Copilot::fake(cx);
let mut cx = EditorLspTestContext::new_rust(
@@ -511,7 +520,13 @@ mod tests {
cx: &mut TestAppContext,
) {
// flaky
- init_test(cx, |_| {});
+ init_test(cx, |settings| {
+ settings.defaults.completions = Some(CompletionSettings {
+ words: WordsCompletionMode::Disabled,
+ lsp: true,
+ lsp_fetch_timeout_ms: 0,
+ });
+ });
let (copilot, copilot_lsp) = Copilot::fake(cx);
let mut cx = EditorLspTestContext::new_rust(
@@ -101,6 +101,7 @@ use itertools::Itertools;
use language::{
language_settings::{
self, all_language_settings, language_settings, InlayHintSettings, RewrapBehavior,
+ WordsCompletionMode,
},
point_from_lsp, text_diff_with_options, AutoindentMode, BracketMatch, BracketPair, Buffer,
Capability, CharKind, CodeLabel, CursorShape, Diagnostic, DiffOptions, EditPredictionsMode,
@@ -4012,9 +4013,8 @@ impl Editor {
} else {
return;
};
- let show_completion_documentation = buffer
- .read(cx)
- .snapshot()
+ let buffer_snapshot = buffer.read(cx).snapshot();
+ let show_completion_documentation = buffer_snapshot
.settings_at(buffer_position, cx)
.show_completion_documentation;
@@ -4038,6 +4038,51 @@ impl Editor {
};
let completions =
provider.completions(&buffer, buffer_position, completion_context, window, cx);
+ let (old_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position);
+ let (old_range, word_to_exclude) = if word_kind == Some(CharKind::Word) {
+ let word_to_exclude = buffer_snapshot
+ .text_for_range(old_range.clone())
+ .collect::<String>();
+ (
+ buffer_snapshot.anchor_before(old_range.start)
+ ..buffer_snapshot.anchor_after(old_range.end),
+ Some(word_to_exclude),
+ )
+ } else {
+ (buffer_position..buffer_position, None)
+ };
+
+ let completion_settings = language_settings(
+ buffer_snapshot
+ .language_at(buffer_position)
+ .map(|language| language.name()),
+ buffer_snapshot.file(),
+ cx,
+ )
+ .completions;
+
+ // The document can be large, so stay in reasonable bounds when searching for words,
+ // otherwise completion pop-up might be slow to appear.
+ const WORD_LOOKUP_ROWS: u32 = 5_000;
+ let buffer_row = text::ToPoint::to_point(&buffer_position, &buffer_snapshot).row;
+ let min_word_search = buffer_snapshot.clip_point(
+ Point::new(buffer_row.saturating_sub(WORD_LOOKUP_ROWS), 0),
+ Bias::Left,
+ );
+ let max_word_search = buffer_snapshot.clip_point(
+ Point::new(buffer_row + WORD_LOOKUP_ROWS, 0).min(buffer_snapshot.max_point()),
+ Bias::Right,
+ );
+ let word_search_range = buffer_snapshot.point_to_offset(min_word_search)
+ ..buffer_snapshot.point_to_offset(max_word_search);
+ let words = match completion_settings.words {
+ WordsCompletionMode::Disabled => Task::ready(HashMap::default()),
+ WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => {
+ cx.background_spawn(async move {
+ buffer_snapshot.words_in_range(None, word_search_range)
+ })
+ }
+ };
let sort_completions = provider.sort_completions();
let id = post_inc(&mut self.next_completion_id);
@@ -4046,8 +4091,55 @@ impl Editor {
editor.update(&mut cx, |this, _| {
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
})?;
- let completions = completions.await.log_err();
- let menu = if let Some(completions) = completions {
+ let mut completions = completions.await.log_err().unwrap_or_default();
+
+ match completion_settings.words {
+ WordsCompletionMode::Enabled => {
+ completions.extend(
+ words
+ .await
+ .into_iter()
+ .filter(|(word, _)| word_to_exclude.as_ref() != Some(word))
+ .map(|(word, word_range)| Completion {
+ old_range: old_range.clone(),
+ new_text: word.clone(),
+ label: CodeLabel::plain(word, None),
+ documentation: None,
+ source: CompletionSource::BufferWord {
+ word_range,
+ resolved: false,
+ },
+ confirm: None,
+ }),
+ );
+ }
+ WordsCompletionMode::Fallback => {
+ if completions.is_empty() {
+ completions.extend(
+ words
+ .await
+ .into_iter()
+ .filter(|(word, _)| word_to_exclude.as_ref() != Some(word))
+ .map(|(word, word_range)| Completion {
+ old_range: old_range.clone(),
+ new_text: word.clone(),
+ label: CodeLabel::plain(word, None),
+ documentation: None,
+ source: CompletionSource::BufferWord {
+ word_range,
+ resolved: false,
+ },
+ confirm: None,
+ }),
+ );
+ }
+ }
+ WordsCompletionMode::Disabled => {}
+ }
+
+ let menu = if completions.is_empty() {
+ None
+ } else {
let mut menu = CompletionsMenu::new(
id,
sort_completions,
@@ -4061,8 +4153,6 @@ impl Editor {
.await;
menu.visible().then_some(menu)
- } else {
- None
};
editor.update_in(&mut cx, |editor, window, cx| {
@@ -16,7 +16,8 @@ use gpui::{
use indoc::indoc;
use language::{
language_settings::{
- AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
+ AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
+ LanguageSettingsContent, PrettierSettings,
},
BracketPairConfig,
Capability::ReadWrite,
@@ -30,7 +31,7 @@ use pretty_assertions::{assert_eq, assert_ne};
use project::project_settings::{LspSettings, ProjectSettings};
use project::FakeFs;
use serde_json::{self, json};
-use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
+use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
use std::{
iter,
sync::atomic::{self, AtomicUsize},
@@ -9169,6 +9170,101 @@ async fn test_completion(cx: &mut TestAppContext) {
apply_additional_edits.await.unwrap();
}
+#[gpui::test]
+async fn test_words_completion(cx: &mut TestAppContext) {
+ let lsp_fetch_timeout_ms = 10;
+ init_test(cx, |language_settings| {
+ language_settings.defaults.completions = Some(CompletionSettings {
+ words: WordsCompletionMode::Fallback,
+ lsp: true,
+ lsp_fetch_timeout_ms: 10,
+ });
+ });
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions {
+ trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+ ..lsp::CompletionOptions::default()
+ }),
+ signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
+ ..lsp::ServerCapabilities::default()
+ },
+ cx,
+ )
+ .await;
+
+ let throttle_completions = Arc::new(AtomicBool::new(false));
+
+ let lsp_throttle_completions = throttle_completions.clone();
+ let _completion_requests_handler =
+ cx.lsp
+ .server
+ .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
+ let lsp_throttle_completions = lsp_throttle_completions.clone();
+ async move {
+ if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
+ cx.background_executor()
+ .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
+ .await;
+ }
+ Ok(Some(lsp::CompletionResponse::Array(vec![
+ lsp::CompletionItem {
+ label: "first".into(),
+ ..Default::default()
+ },
+ lsp::CompletionItem {
+ label: "last".into(),
+ ..Default::default()
+ },
+ ])))
+ }
+ });
+
+ cx.set_state(indoc! {"
+ oneˇ
+ two
+ three
+ "});
+ cx.simulate_keystroke(".");
+ cx.executor().run_until_parked();
+ cx.condition(|editor, _| editor.context_menu_visible())
+ .await;
+ cx.update_editor(|editor, window, cx| {
+ if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
+ {
+ assert_eq!(
+ completion_menu_entries(&menu),
+ &["first", "last"],
+ "When LSP server is fast to reply, no fallback word completions are used"
+ );
+ } else {
+ panic!("expected completion menu to be open");
+ }
+ editor.cancel(&Cancel, window, cx);
+ });
+ cx.executor().run_until_parked();
+ cx.condition(|editor, _| !editor.context_menu_visible())
+ .await;
+
+ throttle_completions.store(true, atomic::Ordering::Release);
+ cx.simulate_keystroke(".");
+ cx.executor()
+ .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
+ cx.executor().run_until_parked();
+ cx.condition(|editor, _| editor.context_menu_visible())
+ .await;
+ cx.update_editor(|editor, _, _| {
+ if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
+ {
+ assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
+ "When LSP server is slow, document words can be shown instead, if configured accordingly");
+ } else {
+ panic!("expected completion menu to be open");
+ }
+ });
+}
+
#[gpui::test]
async fn test_multiline_completion(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -4145,6 +4145,63 @@ impl BufferSnapshot {
None
}
}
+
+ pub fn words_in_range(
+ &self,
+ query: Option<&str>,
+ range: Range<usize>,
+ ) -> HashMap<String, Range<Anchor>> {
+ if query.map_or(false, |query| query.is_empty()) {
+ return HashMap::default();
+ }
+
+ let classifier = CharClassifier::new(self.language.clone().map(|language| LanguageScope {
+ language,
+ override_id: None,
+ }));
+
+ let mut query_ix = 0;
+ let query = query.map(|query| query.chars().collect::<Vec<_>>());
+ let query_len = query.as_ref().map_or(0, |query| query.len());
+
+ let mut words = HashMap::default();
+ let mut current_word_start_ix = None;
+ let mut chunk_ix = range.start;
+ for chunk in self.chunks(range, false) {
+ for (i, c) in chunk.text.char_indices() {
+ let ix = chunk_ix + i;
+ if classifier.is_word(c) {
+ if current_word_start_ix.is_none() {
+ current_word_start_ix = Some(ix);
+ }
+
+ if let Some(query) = &query {
+ if query_ix < query_len {
+ let query_c = query.get(query_ix).expect(
+ "query_ix is a vec of chars, which we access only if before the end",
+ );
+ if c.to_lowercase().eq(query_c.to_lowercase()) {
+ query_ix += 1;
+ }
+ }
+ }
+ continue;
+ } else if let Some(word_start) = current_word_start_ix.take() {
+ if query_ix == query_len {
+ let word_range = self.anchor_before(word_start)..self.anchor_after(ix);
+ words.insert(
+ self.text_for_range(word_start..ix).collect::<String>(),
+ word_range,
+ );
+ }
+ }
+ query_ix = 0;
+ }
+ chunk_ix += chunk.text.len();
+ }
+
+ words
+ }
}
fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
@@ -13,6 +13,7 @@ use proto::deserialize_operation;
use rand::prelude::*;
use regex::RegexBuilder;
use settings::SettingsStore;
+use std::collections::BTreeSet;
use std::{
env,
ops::Range,
@@ -3140,6 +3141,93 @@ fn test_trailing_whitespace_ranges(mut rng: StdRng) {
);
}
+#[gpui::test]
+fn test_words_in_range(cx: &mut gpui::App) {
+ init_settings(cx, |_| {});
+
+ let contents = r#"let word=öäpple.bar你 Öäpple word2-öÄpPlE-Pizza-word ÖÄPPLE word"#;
+
+ let buffer = cx.new(|cx| {
+ let buffer = Buffer::local(contents, cx).with_language(Arc::new(rust_lang()), cx);
+ assert_eq!(buffer.text(), contents);
+ buffer.check_invariants();
+ buffer
+ });
+
+ buffer.update(cx, |buffer, _| {
+ let snapshot = buffer.snapshot();
+ assert_eq!(
+ BTreeSet::from_iter(["Pizza".to_string()]),
+ snapshot
+ .words_in_range(Some("piz"), 0..snapshot.len())
+ .into_keys()
+ .collect::<BTreeSet<_>>()
+ );
+ assert_eq!(
+ BTreeSet::from_iter([
+ "öäpple".to_string(),
+ "Öäpple".to_string(),
+ "öÄpPlE".to_string(),
+ "ÖÄPPLE".to_string(),
+ ]),
+ snapshot
+ .words_in_range(Some("öp"), 0..snapshot.len())
+ .into_keys()
+ .collect::<BTreeSet<_>>()
+ );
+ assert_eq!(
+ BTreeSet::from_iter([
+ "öÄpPlE".to_string(),
+ "Öäpple".to_string(),
+ "ÖÄPPLE".to_string(),
+ "öäpple".to_string(),
+ ]),
+ snapshot
+ .words_in_range(Some("öÄ"), 0..snapshot.len())
+ .into_keys()
+ .collect::<BTreeSet<_>>()
+ );
+ assert_eq!(
+ BTreeSet::default(),
+ snapshot
+ .words_in_range(Some("öÄ好"), 0..snapshot.len())
+ .into_keys()
+ .collect::<BTreeSet<_>>()
+ );
+ assert_eq!(
+ BTreeSet::from_iter(["bar你".to_string(),]),
+ snapshot
+ .words_in_range(Some("你"), 0..snapshot.len())
+ .into_keys()
+ .collect::<BTreeSet<_>>()
+ );
+ assert_eq!(
+ BTreeSet::default(),
+ snapshot
+ .words_in_range(Some(""), 0..snapshot.len())
+ .into_keys()
+ .collect::<BTreeSet<_>>()
+ );
+ assert_eq!(
+ BTreeSet::from_iter([
+ "bar你".to_string(),
+ "öÄpPlE".to_string(),
+ "Öäpple".to_string(),
+ "ÖÄPPLE".to_string(),
+ "öäpple".to_string(),
+ "let".to_string(),
+ "Pizza".to_string(),
+ "word".to_string(),
+ "word2".to_string(),
+ ]),
+ snapshot
+ .words_in_range(None, 0..snapshot.len())
+ .into_keys()
+ .collect::<BTreeSet<_>>()
+ );
+ });
+}
+
fn ruby_lang() -> Language {
Language::new(
LanguageConfig {
@@ -79,10 +79,10 @@ pub struct LanguageSettings {
/// The column at which to soft-wrap lines, for buffers where soft-wrap
/// is enabled.
pub preferred_line_length: u32,
- // Whether to show wrap guides (vertical rulers) in the editor.
- // Setting this to true will show a guide at the 'preferred_line_length' value
- // if softwrap is set to 'preferred_line_length', and will show any
- // additional guides as specified by the 'wrap_guides' setting.
+ /// Whether to show wrap guides (vertical rulers) in the editor.
+ /// Setting this to true will show a guide at the 'preferred_line_length' value
+ /// if softwrap is set to 'preferred_line_length', and will show any
+ /// additional guides as specified by the 'wrap_guides' setting.
pub show_wrap_guides: bool,
/// Character counts at which to show wrap guides (vertical rulers) in the editor.
pub wrap_guides: Vec<usize>,
@@ -137,7 +137,7 @@ pub struct LanguageSettings {
pub use_on_type_format: bool,
/// Whether indentation of pasted content should be adjusted based on the context.
pub auto_indent_on_paste: bool,
- // Controls how the editor handles the autoclosed characters.
+ /// Controls how the editor handles the autoclosed characters.
pub always_treat_brackets_as_autoclosed: bool,
/// Which code actions to run on save
pub code_actions_on_format: HashMap<String, bool>,
@@ -151,6 +151,8 @@ pub struct LanguageSettings {
/// Whether to display inline and alongside documentation for items in the
/// completions menu.
pub show_completion_documentation: bool,
+ /// Completion settings for this language.
+ pub completions: CompletionSettings,
}
impl LanguageSettings {
@@ -306,6 +308,50 @@ pub struct AllLanguageSettingsContent {
pub file_types: HashMap<Arc<str>, Vec<String>>,
}
+/// Controls how completions are processed for this language.
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct CompletionSettings {
+ /// Controls how words are completed.
+ /// For large documents, not all words may be fetched for completion.
+ ///
+ /// Default: `fallback`
+ #[serde(default = "default_words_completion_mode")]
+ pub words: WordsCompletionMode,
+ /// Whether to fetch LSP completions or not.
+ ///
+ /// Default: true
+ #[serde(default = "default_true")]
+ pub lsp: bool,
+ /// When fetching LSP completions, determines how long to wait for a response of a particular server.
+ /// When set to 0, waits indefinitely.
+ ///
+ /// Default: 500
+ #[serde(default = "lsp_fetch_timeout_ms")]
+ pub lsp_fetch_timeout_ms: u64,
+}
+
+/// Controls how document's words are completed.
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum WordsCompletionMode {
+ /// Always fetch document's words for completions.
+ Enabled,
+ /// Only if LSP response errors/times out/is empty,
+ /// use document's words to show completions.
+ Fallback,
+ /// Never fetch or complete document's words for completions.
+ Disabled,
+}
+
+fn default_words_completion_mode() -> WordsCompletionMode {
+ WordsCompletionMode::Fallback
+}
+
+fn lsp_fetch_timeout_ms() -> u64 {
+ 500
+}
+
/// The settings for a particular language.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct LanguageSettingsContent {
@@ -478,6 +524,8 @@ pub struct LanguageSettingsContent {
///
/// Default: true
pub show_completion_documentation: Option<bool>,
+ /// Controls how completions are processed for this language.
+ pub completions: Option<CompletionSettings>,
}
/// The behavior of `editor::Rewrap`.
@@ -1381,6 +1429,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
&mut settings.show_completion_documentation,
src.show_completion_documentation,
);
+ merge(&mut settings.completions, src.completions);
}
/// Allows to enable/disable formatting with Prettier
@@ -23,13 +23,13 @@ use client::{proto, TypedEnvelope};
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
use futures::{
future::{join_all, Shared},
- select,
+ select, select_biased,
stream::FuturesUnordered,
AsyncWriteExt, Future, FutureExt, StreamExt,
};
use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder};
use gpui::{
- App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task,
+ App, AppContext, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task,
WeakEntity,
};
use http_client::HttpClient;
@@ -4325,6 +4325,15 @@ impl LspStore {
let offset = position.to_offset(&snapshot);
let scope = snapshot.language_scope_at(offset);
let language = snapshot.language().cloned();
+ let completion_settings = language_settings(
+ language.as_ref().map(|language| language.name()),
+ buffer.read(cx).file(),
+ cx,
+ )
+ .completions;
+ if !completion_settings.lsp {
+ return Task::ready(Ok(Vec::new()));
+ }
let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| {
local
@@ -4341,23 +4350,51 @@ impl LspStore {
});
let buffer = buffer.clone();
+ let lsp_timeout = completion_settings.lsp_fetch_timeout_ms;
+ let lsp_timeout = if lsp_timeout > 0 {
+ Some(Duration::from_millis(lsp_timeout))
+ } else {
+ None
+ };
cx.spawn(move |this, mut cx| async move {
let mut tasks = Vec::with_capacity(server_ids.len());
- this.update(&mut cx, |this, cx| {
+ this.update(&mut cx, |lsp_store, cx| {
for server_id in server_ids {
- let lsp_adapter = this.language_server_adapter_for_id(server_id);
- tasks.push((
- lsp_adapter,
- this.request_lsp(
- buffer.clone(),
- LanguageServerToQuery::Other(server_id),
- GetCompletions {
- position,
- context: context.clone(),
+ let lsp_adapter = lsp_store.language_server_adapter_for_id(server_id);
+ let lsp_timeout = lsp_timeout
+ .map(|lsp_timeout| cx.background_executor().timer(lsp_timeout));
+ let mut timeout = cx.background_spawn(async move {
+ match lsp_timeout {
+ Some(lsp_timeout) => {
+ lsp_timeout.await;
+ true
},
- cx,
- ),
- ));
+ None => false,
+ }
+ }).fuse();
+ let mut lsp_request = lsp_store.request_lsp(
+ buffer.clone(),
+ LanguageServerToQuery::Other(server_id),
+ GetCompletions {
+ position,
+ context: context.clone(),
+ },
+ cx,
+ ).fuse();
+ let new_task = cx.background_spawn(async move {
+ select_biased! {
+ response = lsp_request => response,
+ timeout_happened = timeout => {
+ if timeout_happened {
+ log::warn!("Fetching completions from server {server_id} timed out, timeout ms: {}", completion_settings.lsp_fetch_timeout_ms);
+ return anyhow::Ok(Vec::new())
+ } else {
+ lsp_request.await
+ }
+ },
+ }
+ });
+ tasks.push((lsp_adapter, new_task));
}
})?;
@@ -4416,47 +4453,58 @@ impl LspStore {
{
did_resolve = true;
}
+ } else {
+ resolve_word_completion(
+ &buffer_snapshot,
+ &mut completions.borrow_mut()[completion_index],
+ );
}
}
} else {
for completion_index in completion_indices {
- let Some(server_id) = completions.borrow()[completion_index].source.server_id()
- else {
- continue;
- };
-
- let server_and_adapter = this
- .read_with(&cx, |lsp_store, _| {
- let server = lsp_store.language_server_for_id(server_id)?;
- let adapter =
- lsp_store.language_server_adapter_for_id(server.server_id())?;
- Some((server, adapter))
- })
- .ok()
- .flatten();
- let Some((server, adapter)) = server_and_adapter else {
- continue;
+ let server_id = {
+ let completion = &completions.borrow()[completion_index];
+ completion.source.server_id()
};
+ if let Some(server_id) = server_id {
+ let server_and_adapter = this
+ .read_with(&cx, |lsp_store, _| {
+ let server = lsp_store.language_server_for_id(server_id)?;
+ let adapter =
+ lsp_store.language_server_adapter_for_id(server.server_id())?;
+ Some((server, adapter))
+ })
+ .ok()
+ .flatten();
+ let Some((server, adapter)) = server_and_adapter else {
+ continue;
+ };
- let resolved = Self::resolve_completion_local(
- server,
- &buffer_snapshot,
- completions.clone(),
- completion_index,
- )
- .await
- .log_err()
- .is_some();
- if resolved {
- Self::regenerate_completion_labels(
- adapter,
+ let resolved = Self::resolve_completion_local(
+ server,
&buffer_snapshot,
completions.clone(),
completion_index,
)
.await
- .log_err();
- did_resolve = true;
+ .log_err()
+ .is_some();
+ if resolved {
+ Self::regenerate_completion_labels(
+ adapter,
+ &buffer_snapshot,
+ completions.clone(),
+ completion_index,
+ )
+ .await
+ .log_err();
+ did_resolve = true;
+ }
+ } else {
+ resolve_word_completion(
+ &buffer_snapshot,
+ &mut completions.borrow_mut()[completion_index],
+ );
}
}
}
@@ -4500,7 +4548,9 @@ impl LspStore {
);
server.request::<lsp::request::ResolveCompletionItem>(*lsp_completion.clone())
}
- CompletionSource::Custom => return Ok(()),
+ CompletionSource::BufferWord { .. } | CompletionSource::Custom => {
+ return Ok(());
+ }
}
};
let resolved_completion = request.await?;
@@ -4641,7 +4691,9 @@ impl LspStore {
}
serde_json::to_string(lsp_completion).unwrap().into_bytes()
}
- CompletionSource::Custom => return Ok(()),
+ CompletionSource::Custom | CompletionSource::BufferWord { .. } => {
+ return Ok(());
+ }
}
};
let request = proto::ResolveCompletionDocumentation {
@@ -8172,51 +8224,54 @@ impl LspStore {
}
pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion {
- let (source, server_id, lsp_completion, lsp_defaults, resolved) = match &completion.source {
+ let mut serialized_completion = proto::Completion {
+ old_start: Some(serialize_anchor(&completion.old_range.start)),
+ old_end: Some(serialize_anchor(&completion.old_range.end)),
+ new_text: completion.new_text.clone(),
+ ..proto::Completion::default()
+ };
+ match &completion.source {
CompletionSource::Lsp {
server_id,
lsp_completion,
lsp_defaults,
resolved,
- } => (
- proto::completion::Source::Lsp as i32,
- server_id.0 as u64,
- serde_json::to_vec(lsp_completion).unwrap(),
- lsp_defaults
+ } => {
+ 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();
+ serialized_completion.lsp_defaults = lsp_defaults
.as_deref()
- .map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap()),
- *resolved,
- ),
- CompletionSource::Custom => (
- proto::completion::Source::Custom as i32,
- 0,
- Vec::new(),
- None,
- true,
- ),
- };
-
- proto::Completion {
- old_start: Some(serialize_anchor(&completion.old_range.start)),
- old_end: Some(serialize_anchor(&completion.old_range.end)),
- new_text: completion.new_text.clone(),
- server_id,
- lsp_completion,
- lsp_defaults,
- resolved,
- source,
+ .map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap());
+ serialized_completion.resolved = *resolved;
+ }
+ CompletionSource::BufferWord {
+ word_range,
+ resolved,
+ } => {
+ serialized_completion.source = proto::completion::Source::BufferWord as i32;
+ serialized_completion.buffer_word_start = Some(serialize_anchor(&word_range.start));
+ serialized_completion.buffer_word_end = Some(serialize_anchor(&word_range.end));
+ serialized_completion.resolved = *resolved;
+ }
+ CompletionSource::Custom => {
+ serialized_completion.source = proto::completion::Source::Custom as i32;
+ serialized_completion.resolved = true;
+ }
}
+
+ serialized_completion
}
pub(crate) fn deserialize_completion(completion: proto::Completion) -> Result<CoreCompletion> {
let old_start = completion
.old_start
.and_then(deserialize_anchor)
- .ok_or_else(|| anyhow!("invalid old start"))?;
+ .context("invalid old start")?;
let old_end = completion
.old_end
.and_then(deserialize_anchor)
- .ok_or_else(|| anyhow!("invalid old end"))?;
+ .context("invalid old end")?;
Ok(CoreCompletion {
old_range: old_start..old_end,
new_text: completion.new_text,
@@ -8232,6 +8287,20 @@ impl LspStore {
.transpose()?,
resolved: completion.resolved,
},
+ Some(proto::completion::Source::BufferWord) => {
+ let word_range = completion
+ .buffer_word_start
+ .and_then(deserialize_anchor)
+ .context("invalid buffer word start")?
+ ..completion
+ .buffer_word_end
+ .and_then(deserialize_anchor)
+ .context("invalid buffer word end")?;
+ CompletionSource::BufferWord {
+ word_range,
+ resolved: completion.resolved,
+ }
+ }
_ => anyhow::bail!("Unexpected completion source {}", completion.source),
},
})
@@ -8296,6 +8365,40 @@ impl LspStore {
}
}
+fn resolve_word_completion(snapshot: &BufferSnapshot, completion: &mut Completion) {
+ let CompletionSource::BufferWord {
+ word_range,
+ resolved,
+ } = &mut completion.source
+ else {
+ return;
+ };
+ if *resolved {
+ return;
+ }
+
+ if completion.new_text
+ != snapshot
+ .text_for_range(word_range.clone())
+ .collect::<String>()
+ {
+ return;
+ }
+
+ let mut offset = 0;
+ for chunk in snapshot.chunks(word_range.clone(), true) {
+ let end_offset = offset + chunk.text.len();
+ if let Some(highlight_id) = chunk.syntax_highlight_id {
+ completion
+ .label
+ .runs
+ .push((offset..end_offset, highlight_id));
+ }
+ offset = end_offset;
+ }
+ *resolved = true;
+}
+
impl EventEmitter<LspStoreEvent> for LspStore {}
fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
@@ -388,6 +388,10 @@ pub enum CompletionSource {
resolved: bool,
},
Custom,
+ BufferWord {
+ word_range: Range<Anchor>,
+ resolved: bool,
+ },
}
impl CompletionSource {
@@ -1002,10 +1002,13 @@ message Completion {
bool resolved = 6;
Source source = 7;
optional bytes lsp_defaults = 8;
+ optional Anchor buffer_word_start = 9;
+ optional Anchor buffer_word_end = 10;
enum Source {
Lsp = 0;
Custom = 1;
+ BufferWord = 2;
}
}