Compare commits
8 Commits
fix-git-ht
...
v0.166.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77932ac71c | ||
|
|
51213a2b9a | ||
|
|
01883a4e53 | ||
|
|
bb01d30923 | ||
|
|
ee38693937 | ||
|
|
546b49c628 | ||
|
|
6a7ef111c6 | ||
|
|
83feaaa39e |
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -13930,7 +13930,6 @@ dependencies = [
|
||||
"futures-lite 1.13.0",
|
||||
"git2",
|
||||
"globset",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
@@ -15944,7 +15943,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.166.0"
|
||||
version = "0.166.2"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
use std::{
|
||||
cell::Cell,
|
||||
cmp::{min, Reverse},
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{cell::Cell, cmp::Reverse, ops::Range, sync::Arc};
|
||||
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
@@ -16,9 +11,8 @@ use language::{CodeLabel, Documentation};
|
||||
use lsp::LanguageServerId;
|
||||
use multi_buffer::{Anchor, ExcerptId};
|
||||
use ordered_float::OrderedFloat;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use parking_lot::RwLock;
|
||||
use project::{CodeAction, Completion, TaskSourceKind};
|
||||
use std::iter;
|
||||
use task::ResolvedTask;
|
||||
use ui::{
|
||||
h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement,
|
||||
@@ -151,7 +145,6 @@ pub struct CompletionsMenu {
|
||||
resolve_completions: bool,
|
||||
pub aside_was_displayed: Cell<bool>,
|
||||
show_completion_documentation: bool,
|
||||
last_rendered_range: Arc<Mutex<Option<Range<usize>>>>,
|
||||
}
|
||||
|
||||
impl CompletionsMenu {
|
||||
@@ -180,6 +173,7 @@ impl CompletionsMenu {
|
||||
sort_completions,
|
||||
initial_position,
|
||||
buffer,
|
||||
show_completion_documentation,
|
||||
completions: Arc::new(RwLock::new(completions)),
|
||||
match_candidates,
|
||||
matches: Vec::new().into(),
|
||||
@@ -187,8 +181,6 @@ impl CompletionsMenu {
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: true,
|
||||
aside_was_displayed: Cell::new(aside_was_displayed),
|
||||
show_completion_documentation,
|
||||
last_rendered_range: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +236,6 @@ impl CompletionsMenu {
|
||||
resolve_completions: false,
|
||||
aside_was_displayed: Cell::new(false),
|
||||
show_completion_documentation: false,
|
||||
last_rendered_range: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +244,11 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.update_selection_index(0, provider, cx);
|
||||
self.selected_item = 0;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_prev(
|
||||
@@ -261,7 +256,15 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.update_selection_index(self.prev_match_index(), provider, cx);
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item -= 1;
|
||||
} else {
|
||||
self.selected_item = self.matches.len() - 1;
|
||||
}
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(
|
||||
@@ -269,7 +272,15 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.update_selection_index(self.next_match_index(), provider, cx);
|
||||
if self.selected_item + 1 < self.matches.len() {
|
||||
self.selected_item += 1;
|
||||
} else {
|
||||
self.selected_item = 0;
|
||||
}
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_last(
|
||||
@@ -277,41 +288,14 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.update_selection_index(self.matches.len() - 1, provider, cx);
|
||||
self.selected_item = self.matches.len() - 1;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn update_selection_index(
|
||||
&mut self,
|
||||
match_index: usize,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if self.selected_item != match_index {
|
||||
self.selected_item = match_index;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_visible_completions(provider, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_match_index(&self) -> usize {
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item - 1
|
||||
} else {
|
||||
self.matches.len() - 1
|
||||
}
|
||||
}
|
||||
|
||||
fn next_match_index(&self) -> usize {
|
||||
if self.selected_item + 1 < self.matches.len() {
|
||||
self.selected_item + 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_visible_completions(
|
||||
pub fn resolve_selected_completion(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
@@ -323,59 +307,10 @@ impl CompletionsMenu {
|
||||
return;
|
||||
};
|
||||
|
||||
// Attempt to resolve completions for every item that will be displayed. This matters
|
||||
// because single line documentation may be displayed inline with the completion.
|
||||
//
|
||||
// When navigating to the very beginning or end of completions, `last_rendered_range` may
|
||||
// have no overlap with the completions that will be displayed, so instead use a range based
|
||||
// on the last rendered count.
|
||||
const APPROXIMATE_VISIBLE_COUNT: usize = 12;
|
||||
let last_rendered_range = self.last_rendered_range.lock().clone();
|
||||
let visible_count = last_rendered_range
|
||||
.clone()
|
||||
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
|
||||
let matches_range = if self.selected_item == 0 {
|
||||
0..min(visible_count, self.matches.len())
|
||||
} else if self.selected_item == self.matches.len() - 1 {
|
||||
self.matches.len().saturating_sub(visible_count)..self.matches.len()
|
||||
} else {
|
||||
last_rendered_range.unwrap_or_else(|| self.selected_item..self.selected_item + 1)
|
||||
};
|
||||
|
||||
// Expand the range to resolve more completions than are predicted to be visible, to reduce
|
||||
// jank on navigation.
|
||||
const EXTRA_TO_RESOLVE: usize = 4;
|
||||
let matches_indices = util::iterate_expanded_and_wrapped_usize_range(
|
||||
matches_range.clone(),
|
||||
EXTRA_TO_RESOLVE,
|
||||
EXTRA_TO_RESOLVE,
|
||||
self.matches.len(),
|
||||
);
|
||||
|
||||
// Avoid work by sometimes filtering out completions that already have documentation.
|
||||
// This filtering doesn't happen if the completions are currently being updated.
|
||||
let candidate_ids = matches_indices.map(|i| self.matches[i].candidate_id);
|
||||
let candidate_ids = match self.completions.try_read() {
|
||||
None => candidate_ids.collect::<Vec<usize>>(),
|
||||
Some(completions) => candidate_ids
|
||||
.filter(|i| completions[*i].documentation.is_none())
|
||||
.collect::<Vec<usize>>(),
|
||||
};
|
||||
|
||||
// Current selection is always resolved even if it already has documentation, to handle
|
||||
// out-of-spec language servers that return more results later.
|
||||
let selected_candidate_id = self.matches[self.selected_item].candidate_id;
|
||||
let candidate_ids = iter::once(selected_candidate_id)
|
||||
.chain(
|
||||
candidate_ids
|
||||
.into_iter()
|
||||
.filter(|id| *id != selected_candidate_id),
|
||||
)
|
||||
.collect::<Vec<usize>>();
|
||||
|
||||
let completion_index = self.matches[self.selected_item].candidate_id;
|
||||
let resolve_task = provider.resolve_completions(
|
||||
self.buffer.clone(),
|
||||
candidate_ids,
|
||||
vec![completion_index],
|
||||
self.completions.clone(),
|
||||
cx,
|
||||
);
|
||||
@@ -471,14 +406,11 @@ impl CompletionsMenu {
|
||||
.occlude()
|
||||
});
|
||||
|
||||
let last_rendered_range = self.last_rendered_range.clone();
|
||||
|
||||
let list = uniform_list(
|
||||
cx.view().clone(),
|
||||
"completions",
|
||||
matches.len(),
|
||||
move |_editor, range, cx| {
|
||||
last_rendered_range.lock().replace(range.clone());
|
||||
let start_ix = range.start;
|
||||
let completions_guard = completions.read();
|
||||
|
||||
|
||||
@@ -3684,7 +3684,7 @@ impl Editor {
|
||||
|
||||
if editor.focus_handle.is_focused(cx) && menu.is_some() {
|
||||
let mut menu = menu.unwrap();
|
||||
menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx);
|
||||
menu.resolve_selected_completion(editor.completion_provider.as_deref(), cx);
|
||||
*context_menu = Some(CodeContextMenu::Completions(menu));
|
||||
drop(context_menu);
|
||||
editor.discard_inline_completion(false, cx);
|
||||
@@ -5271,7 +5271,8 @@ impl Editor {
|
||||
if end_point == start_point {
|
||||
let offset = text::ToOffset::to_offset(&range.start, &snapshot)
|
||||
.saturating_sub(1);
|
||||
start_point = TP::to_point(&offset, &snapshot);
|
||||
start_point =
|
||||
snapshot.clip_point(TP::to_point(&offset, &snapshot), Bias::Left);
|
||||
};
|
||||
|
||||
(start_point..end_point, empty_str.clone())
|
||||
|
||||
@@ -25,15 +25,15 @@ use language::{
|
||||
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
|
||||
use multi_buffer::MultiBufferIndentGuide;
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use project::{buffer_store::BufferChangeSet, FakeFs};
|
||||
use project::{
|
||||
lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
|
||||
project_settings::{LspSettings, ProjectSettings},
|
||||
};
|
||||
use serde_json::{self, json};
|
||||
use std::sync::atomic::{self, AtomicUsize};
|
||||
use std::{cell::RefCell, future::Future, iter, rc::Rc, time::Instant};
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::{self, AtomicBool};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
|
||||
use test::editor_lsp_test_context::rust_lang;
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
@@ -10717,62 +10717,6 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
|
||||
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let item_0 = lsp::CompletionItem {
|
||||
label: "abs".into(),
|
||||
insert_text: Some("abs".into()),
|
||||
data: Some(json!({ "very": "special"})),
|
||||
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
|
||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||
lsp::InsertReplaceEdit {
|
||||
new_text: "abs".to_string(),
|
||||
insert: lsp::Range::default(),
|
||||
replace: lsp::Range::default(),
|
||||
},
|
||||
)),
|
||||
..lsp::CompletionItem::default()
|
||||
};
|
||||
let items = iter::once(item_0.clone())
|
||||
.chain((11..51).map(|i| lsp::CompletionItem {
|
||||
label: format!("item_{}", i),
|
||||
insert_text: Some(format!("item_{}", i)),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
..lsp::CompletionItem::default()
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let default_commit_characters = vec!["?".to_string()];
|
||||
let default_data = json!({ "default": "data"});
|
||||
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
|
||||
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
|
||||
let default_edit_range = lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
};
|
||||
|
||||
let item_0_out = lsp::CompletionItem {
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
insert_text_format: Some(default_insert_text_format),
|
||||
..item_0
|
||||
};
|
||||
let items_out = iter::once(item_0_out)
|
||||
.chain(items[1..].iter().map(|item| lsp::CompletionItem {
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
data: Some(default_data.clone()),
|
||||
insert_text_mode: Some(default_insert_text_mode),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: default_edit_range,
|
||||
new_text: item.label.clone(),
|
||||
})),
|
||||
..item.clone()
|
||||
}))
|
||||
.collect::<Vec<lsp::CompletionItem>>();
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
@@ -10789,15 +10733,138 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
|
||||
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
|
||||
cx.simulate_keystroke(".");
|
||||
|
||||
let default_commit_characters = vec!["?".to_string()];
|
||||
let default_data = json!({ "very": "special"});
|
||||
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
|
||||
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
|
||||
let default_edit_range = lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
};
|
||||
|
||||
let resolve_requests_number = Arc::new(AtomicUsize::new(0));
|
||||
let expect_first_item = Arc::new(AtomicBool::new(true));
|
||||
cx.lsp
|
||||
.server
|
||||
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
|
||||
let closure_default_data = default_data.clone();
|
||||
let closure_resolve_requests_number = resolve_requests_number.clone();
|
||||
let closure_expect_first_item = expect_first_item.clone();
|
||||
let closure_default_commit_characters = default_commit_characters.clone();
|
||||
move |item_to_resolve, _| {
|
||||
closure_resolve_requests_number.fetch_add(1, atomic::Ordering::Release);
|
||||
let default_data = closure_default_data.clone();
|
||||
let default_commit_characters = closure_default_commit_characters.clone();
|
||||
let expect_first_item = closure_expect_first_item.clone();
|
||||
async move {
|
||||
if expect_first_item.load(atomic::Ordering::Acquire) {
|
||||
assert_eq!(
|
||||
item_to_resolve.label, "Some(2)",
|
||||
"Should have selected the first item"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.data,
|
||||
Some(json!({ "very": "special"})),
|
||||
"First item should bring its own data for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.commit_characters,
|
||||
Some(default_commit_characters),
|
||||
"First item had no own commit characters and should inherit the default ones"
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
item_to_resolve.text_edit,
|
||||
Some(lsp::CompletionTextEdit::InsertAndReplace { .. })
|
||||
),
|
||||
"First item should bring its own edit range for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_format,
|
||||
Some(default_insert_text_format),
|
||||
"First item had no own insert text format and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_mode,
|
||||
Some(lsp::InsertTextMode::ADJUST_INDENTATION),
|
||||
"First item should bring its own insert text mode for resolving"
|
||||
);
|
||||
Ok(item_to_resolve)
|
||||
} else {
|
||||
assert_eq!(
|
||||
item_to_resolve.label, "vec![2]",
|
||||
"Should have selected the last item"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.data,
|
||||
Some(default_data),
|
||||
"Last item has no own resolve data and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.commit_characters,
|
||||
Some(default_commit_characters),
|
||||
"Last item had no own commit characters and should inherit the default ones"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.text_edit,
|
||||
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: default_edit_range,
|
||||
new_text: "vec![2]".to_string()
|
||||
})),
|
||||
"Last item had no own edit range and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_format,
|
||||
Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
"Last item should bring its own insert text format for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_mode,
|
||||
Some(default_insert_text_mode),
|
||||
"Last item had no own insert text mode and should inherit the default one"
|
||||
);
|
||||
|
||||
Ok(item_to_resolve)
|
||||
}
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
|
||||
let completion_data = default_data.clone();
|
||||
let completion_characters = default_commit_characters.clone();
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
|
||||
let default_data = completion_data.clone();
|
||||
let default_commit_characters = completion_characters.clone();
|
||||
let items = items.clone();
|
||||
async move {
|
||||
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
items,
|
||||
items: vec![
|
||||
lsp::CompletionItem {
|
||||
label: "Some(2)".into(),
|
||||
insert_text: Some("Some(2)".into()),
|
||||
data: Some(json!({ "very": "special"})),
|
||||
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
|
||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||
lsp::InsertReplaceEdit {
|
||||
new_text: "Some(2)".to_string(),
|
||||
insert: lsp::Range::default(),
|
||||
replace: lsp::Range::default(),
|
||||
},
|
||||
)),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "vec![2]".into(),
|
||||
insert_text: Some("vec![2]".into()),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
],
|
||||
item_defaults: Some(lsp::CompletionListItemDefaults {
|
||||
data: Some(default_data.clone()),
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
@@ -10814,21 +10881,6 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
|
||||
.next()
|
||||
.await;
|
||||
|
||||
let resolved_items = Arc::new(Mutex::new(Vec::new()));
|
||||
cx.lsp
|
||||
.server
|
||||
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
|
||||
let closure_resolved_items = resolved_items.clone();
|
||||
move |item_to_resolve, _| {
|
||||
let closure_resolved_items = closure_resolved_items.clone();
|
||||
async move {
|
||||
closure_resolved_items.lock().push(item_to_resolve.clone());
|
||||
Ok(item_to_resolve)
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
@@ -10840,50 +10892,40 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
|
||||
completions_menu
|
||||
.matches
|
||||
.iter()
|
||||
.map(|c| c.string.clone())
|
||||
.collect::<Vec<String>>(),
|
||||
items_out
|
||||
.iter()
|
||||
.map(|completion| completion.label.clone())
|
||||
.collect::<Vec<String>>()
|
||||
.map(|c| c.string.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["Some(2)", "vec![2]"]
|
||||
);
|
||||
}
|
||||
CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
|
||||
}
|
||||
});
|
||||
// Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
|
||||
// with 4 from the end.
|
||||
assert_eq!(
|
||||
*resolved_items.lock(),
|
||||
[
|
||||
&items_out[0..16],
|
||||
&items_out[items_out.len() - 4..items_out.len()]
|
||||
]
|
||||
.concat()
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<lsp::CompletionItem>>()
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"While there are 2 items in the completion list, only 1 resolve request should have been sent, for the selected item"
|
||||
);
|
||||
resolved_items.lock().clear();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_prev(&ContextMenuPrev, cx);
|
||||
editor.context_menu_first(&ContextMenuFirst, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
// Completions that have already been resolved are skipped.
|
||||
assert_eq!(
|
||||
*resolved_items.lock(),
|
||||
[
|
||||
// Selected item is always resolved even if it was resolved before.
|
||||
&items_out[items_out.len() - 1..items_out.len()],
|
||||
&items_out[items_out.len() - 16..items_out.len() - 4]
|
||||
]
|
||||
.concat()
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<lsp::CompletionItem>>()
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
2,
|
||||
"After re-selecting the first item, another resolve request should have been sent"
|
||||
);
|
||||
|
||||
expect_first_item.store(false, atomic::Ordering::Release);
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_last(&ContextMenuLast, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
3,
|
||||
"After selecting the other item, another resolve request should have been sent"
|
||||
);
|
||||
resolved_items.lock().clear();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -14,8 +14,7 @@ use crate::{
|
||||
|
||||
pub struct ProjectEnvironment {
|
||||
cli_environment: Option<HashMap<String, String>>,
|
||||
get_environment_task: Option<Shared<Task<Option<HashMap<String, String>>>>>,
|
||||
cached_shell_environments: HashMap<WorktreeId, HashMap<String, String>>,
|
||||
environments: HashMap<WorktreeId, Shared<Task<Option<HashMap<String, String>>>>>,
|
||||
environment_error_messages: HashMap<WorktreeId, EnvironmentErrorMessage>,
|
||||
}
|
||||
|
||||
@@ -35,27 +34,15 @@ impl ProjectEnvironment {
|
||||
|
||||
Self {
|
||||
cli_environment,
|
||||
get_environment_task: None,
|
||||
cached_shell_environments: Default::default(),
|
||||
environments: Default::default(),
|
||||
environment_error_messages: Default::default(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn set_cached(
|
||||
&mut self,
|
||||
shell_environments: &[(WorktreeId, HashMap<String, String>)],
|
||||
) {
|
||||
self.cached_shell_environments = shell_environments
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<HashMap<_, _>>();
|
||||
}
|
||||
|
||||
pub(crate) fn remove_worktree_environment(&mut self, worktree_id: WorktreeId) {
|
||||
self.cached_shell_environments.remove(&worktree_id);
|
||||
self.environment_error_messages.remove(&worktree_id);
|
||||
self.environments.remove(&worktree_id);
|
||||
}
|
||||
|
||||
/// Returns the inherited CLI environment, if this project was opened from the Zed CLI.
|
||||
@@ -91,96 +78,83 @@ impl ProjectEnvironment {
|
||||
worktree_abs_path: Option<Arc<Path>>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Shared<Task<Option<HashMap<String, String>>>> {
|
||||
if let Some(task) = self.get_environment_task.as_ref() {
|
||||
if cfg!(any(test, feature = "test-support")) {
|
||||
return Task::ready(Some(HashMap::default())).shared();
|
||||
}
|
||||
|
||||
if let Some(cli_environment) = self.get_cli_environment() {
|
||||
return cx
|
||||
.spawn(|_, _| async move {
|
||||
let path = cli_environment
|
||||
.get("PATH")
|
||||
.map(|path| path.as_str())
|
||||
.unwrap_or_default();
|
||||
log::info!(
|
||||
"using project environment variables from CLI. PATH={:?}",
|
||||
path
|
||||
);
|
||||
Some(cli_environment)
|
||||
})
|
||||
.shared();
|
||||
}
|
||||
|
||||
let Some((worktree_id, worktree_abs_path)) = worktree_id.zip(worktree_abs_path) else {
|
||||
return Task::ready(None).shared();
|
||||
};
|
||||
|
||||
if let Some(task) = self.environments.get(&worktree_id) {
|
||||
task.clone()
|
||||
} else {
|
||||
let task = self
|
||||
.build_environment_task(worktree_id, worktree_abs_path, cx)
|
||||
.get_worktree_env(worktree_id, worktree_abs_path, cx)
|
||||
.shared();
|
||||
|
||||
self.get_environment_task = Some(task.clone());
|
||||
self.environments.insert(worktree_id, task.clone());
|
||||
task
|
||||
}
|
||||
}
|
||||
|
||||
fn build_environment_task(
|
||||
&mut self,
|
||||
worktree_id: Option<WorktreeId>,
|
||||
worktree_abs_path: Option<Arc<Path>>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Option<HashMap<String, String>>> {
|
||||
let worktree = worktree_id.zip(worktree_abs_path);
|
||||
|
||||
let cli_environment = self.get_cli_environment();
|
||||
if let Some(environment) = cli_environment {
|
||||
cx.spawn(|_, _| async move {
|
||||
let path = environment
|
||||
.get("PATH")
|
||||
.map(|path| path.as_str())
|
||||
.unwrap_or_default();
|
||||
log::info!(
|
||||
"using project environment variables from CLI. PATH={:?}",
|
||||
path
|
||||
);
|
||||
Some(environment)
|
||||
})
|
||||
} else if let Some((worktree_id, worktree_abs_path)) = worktree {
|
||||
self.get_worktree_env(worktree_id, worktree_abs_path, cx)
|
||||
} else {
|
||||
Task::ready(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_worktree_env(
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
worktree_abs_path: Arc<Path>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Option<HashMap<String, String>>> {
|
||||
let cached_env = self.cached_shell_environments.get(&worktree_id).cloned();
|
||||
if let Some(env) = cached_env {
|
||||
Task::ready(Some(env))
|
||||
} else {
|
||||
let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
|
||||
let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let (mut shell_env, error_message) = cx
|
||||
.background_executor()
|
||||
.spawn({
|
||||
let cwd = worktree_abs_path.clone();
|
||||
async move { load_shell_environment(&cwd, &load_direnv).await }
|
||||
})
|
||||
.await;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let (mut shell_env, error_message) = cx
|
||||
.background_executor()
|
||||
.spawn({
|
||||
let worktree_abs_path = worktree_abs_path.clone();
|
||||
async move {
|
||||
load_worktree_shell_environment(&worktree_abs_path, &load_direnv).await
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(shell_env) = shell_env.as_mut() {
|
||||
let path = shell_env
|
||||
.get("PATH")
|
||||
.map(|path| path.as_str())
|
||||
.unwrap_or_default();
|
||||
log::info!(
|
||||
"using project environment variables shell launched in {:?}. PATH={:?}",
|
||||
worktree_abs_path,
|
||||
path
|
||||
);
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.cached_shell_environments
|
||||
.insert(worktree_id, shell_env.clone());
|
||||
})
|
||||
.log_err();
|
||||
if let Some(shell_env) = shell_env.as_mut() {
|
||||
let path = shell_env
|
||||
.get("PATH")
|
||||
.map(|path| path.as_str())
|
||||
.unwrap_or_default();
|
||||
log::info!(
|
||||
"using project environment variables shell launched in {:?}. PATH={:?}",
|
||||
worktree_abs_path,
|
||||
path
|
||||
);
|
||||
|
||||
set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
|
||||
}
|
||||
set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
|
||||
}
|
||||
|
||||
if let Some(error) = error_message {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.environment_error_messages.insert(worktree_id, error);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
if let Some(error) = error_message {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.environment_error_messages.insert(worktree_id, error);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
shell_env
|
||||
})
|
||||
}
|
||||
shell_env
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +187,42 @@ impl EnvironmentErrorMessage {
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_worktree_shell_environment(
|
||||
worktree_abs_path: &Path,
|
||||
load_direnv: &DirenvSettings,
|
||||
) -> (
|
||||
Option<HashMap<String, String>>,
|
||||
Option<EnvironmentErrorMessage>,
|
||||
) {
|
||||
match smol::fs::metadata(worktree_abs_path).await {
|
||||
Ok(meta) => {
|
||||
let dir = if meta.is_dir() {
|
||||
worktree_abs_path
|
||||
} else if let Some(parent) = worktree_abs_path.parent() {
|
||||
parent
|
||||
} else {
|
||||
return (
|
||||
None,
|
||||
Some(EnvironmentErrorMessage(format!(
|
||||
"Failed to load shell environment in {}: not a directory",
|
||||
worktree_abs_path.display()
|
||||
))),
|
||||
);
|
||||
};
|
||||
|
||||
load_shell_environment(&dir, load_direnv).await
|
||||
}
|
||||
Err(err) => (
|
||||
None,
|
||||
Some(EnvironmentErrorMessage(format!(
|
||||
"Failed to load shell environment in {}: {}",
|
||||
worktree_abs_path.display(),
|
||||
err
|
||||
))),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
async fn load_shell_environment(
|
||||
_dir: &Path,
|
||||
|
||||
@@ -1205,13 +1205,6 @@ impl Project {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
let tree_id = tree.read(cx).id();
|
||||
project.environment.update(cx, |environment, _| {
|
||||
environment.set_cached(&[(tree_id, HashMap::default())])
|
||||
});
|
||||
});
|
||||
|
||||
tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -899,8 +899,7 @@ pub fn new_terminal_pane(
|
||||
pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| {
|
||||
if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
|
||||
let this_pane = cx.view().clone();
|
||||
let belongs_to_this_pane = tab.pane == this_pane;
|
||||
let item = if belongs_to_this_pane {
|
||||
let item = if tab.pane == this_pane {
|
||||
pane.item_for_index(tab.ix)
|
||||
} else {
|
||||
tab.pane.read(cx).item_for_index(tab.ix)
|
||||
@@ -910,53 +909,57 @@ pub fn new_terminal_pane(
|
||||
let source = tab.pane.clone();
|
||||
let item_id_to_move = item.item_id();
|
||||
|
||||
let new_pane = pane.drag_split_direction().and_then(|split_direction| {
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
let is_zoomed = if terminal_panel.active_pane == this_pane {
|
||||
pane.is_zoomed()
|
||||
} else {
|
||||
terminal_panel.active_pane.read(cx).is_zoomed()
|
||||
};
|
||||
let new_pane = new_terminal_pane(
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
is_zoomed,
|
||||
cx,
|
||||
);
|
||||
terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
|
||||
terminal_panel
|
||||
.center
|
||||
.split(&this_pane, &new_pane, split_direction)
|
||||
.log_err()?;
|
||||
Some(new_pane)
|
||||
let new_split_pane = pane
|
||||
.drag_split_direction()
|
||||
.map(|split_direction| {
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
let is_zoomed = if terminal_panel.active_pane == this_pane {
|
||||
pane.is_zoomed()
|
||||
} else {
|
||||
terminal_panel.active_pane.read(cx).is_zoomed()
|
||||
};
|
||||
let new_pane = new_terminal_pane(
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
is_zoomed,
|
||||
cx,
|
||||
);
|
||||
terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
|
||||
terminal_panel.center.split(
|
||||
&this_pane,
|
||||
&new_pane,
|
||||
split_direction,
|
||||
)?;
|
||||
anyhow::Ok(new_pane)
|
||||
})
|
||||
})
|
||||
});
|
||||
.transpose();
|
||||
|
||||
let destination;
|
||||
let destination_index;
|
||||
if let Some(new_pane) = new_pane {
|
||||
destination_index = new_pane.read(cx).active_item_index();
|
||||
destination = new_pane;
|
||||
} else if belongs_to_this_pane {
|
||||
return ControlFlow::Break(());
|
||||
} else {
|
||||
destination = cx.view().clone();
|
||||
destination_index = pane.active_item_index();
|
||||
}
|
||||
// Destination pane may be the one currently updated, so defer the move.
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
cx.update(|cx| {
|
||||
move_item(
|
||||
&source,
|
||||
&destination,
|
||||
item_id_to_move,
|
||||
destination_index,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
match new_split_pane {
|
||||
// Source pane may be the one currently updated, so defer the move.
|
||||
Ok(Some(new_pane)) => cx
|
||||
.spawn(|_, mut cx| async move {
|
||||
cx.update(|cx| {
|
||||
move_item(
|
||||
&source,
|
||||
&new_pane,
|
||||
item_id_to_move,
|
||||
new_pane.read(cx).active_item_index(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach(),
|
||||
// If we drop into existing pane or current pane,
|
||||
// regular pane drop handler will take care of it,
|
||||
// using the right tab index for the operation.
|
||||
Ok(None) => return ControlFlow::Continue(()),
|
||||
err @ Err(_) => {
|
||||
err.log_err();
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
};
|
||||
} else if let Some(project_path) = item.project_path(cx) {
|
||||
if let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
|
||||
{
|
||||
|
||||
@@ -24,7 +24,6 @@ futures-lite.workspace = true
|
||||
futures.workspace = true
|
||||
git2 = { workspace = true, optional = true }
|
||||
globset.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
rand = { workspace = true, optional = true }
|
||||
regex.workspace = true
|
||||
|
||||
@@ -8,7 +8,6 @@ pub mod test;
|
||||
|
||||
use futures::Future;
|
||||
|
||||
use itertools::Either;
|
||||
use regex::Regex;
|
||||
use std::sync::OnceLock;
|
||||
use std::{
|
||||
@@ -200,35 +199,6 @@ pub fn measure<R>(label: &str, f: impl FnOnce() -> R) -> R {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iterate_expanded_and_wrapped_usize_range(
|
||||
range: Range<usize>,
|
||||
additional_before: usize,
|
||||
additional_after: usize,
|
||||
wrap_length: usize,
|
||||
) -> impl Iterator<Item = usize> {
|
||||
let start_wraps = range.start < additional_before;
|
||||
let end_wraps = wrap_length < range.end + additional_after;
|
||||
if start_wraps && end_wraps {
|
||||
Either::Left(0..wrap_length)
|
||||
} else if start_wraps {
|
||||
let wrapped_start = (range.start + wrap_length).saturating_sub(additional_before);
|
||||
if wrapped_start <= range.end {
|
||||
Either::Left(0..wrap_length)
|
||||
} else {
|
||||
Either::Right((0..range.end + additional_after).chain(wrapped_start..wrap_length))
|
||||
}
|
||||
} else if end_wraps {
|
||||
let wrapped_end = range.end + additional_after - wrap_length;
|
||||
if range.start <= wrapped_end {
|
||||
Either::Left(0..wrap_length)
|
||||
} else {
|
||||
Either::Right((0..wrapped_end).chain(range.start - additional_before..wrap_length))
|
||||
}
|
||||
} else {
|
||||
Either::Left((range.start - additional_before)..(range.end + additional_after))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ResultExt<E> {
|
||||
type Ok;
|
||||
|
||||
@@ -763,48 +733,4 @@ Line 2
|
||||
Line 3"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iterate_expanded_and_wrapped_usize_range() {
|
||||
// Neither wrap
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(2..4, 1, 1, 8).collect::<Vec<usize>>(),
|
||||
(1..5).collect::<Vec<usize>>()
|
||||
);
|
||||
// Start wraps
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(2..4, 3, 1, 8).collect::<Vec<usize>>(),
|
||||
((0..5).chain(7..8)).collect::<Vec<usize>>()
|
||||
);
|
||||
// Start wraps all the way around
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(2..4, 5, 1, 8).collect::<Vec<usize>>(),
|
||||
(0..8).collect::<Vec<usize>>()
|
||||
);
|
||||
// Start wraps all the way around and past 0
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(2..4, 10, 1, 8).collect::<Vec<usize>>(),
|
||||
(0..8).collect::<Vec<usize>>()
|
||||
);
|
||||
// End wraps
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(3..5, 1, 4, 8).collect::<Vec<usize>>(),
|
||||
(0..1).chain(2..8).collect::<Vec<usize>>()
|
||||
);
|
||||
// End wraps all the way around
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(3..5, 1, 5, 8).collect::<Vec<usize>>(),
|
||||
(0..8).collect::<Vec<usize>>()
|
||||
);
|
||||
// End wraps all the way around and past the end
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(3..5, 1, 10, 8).collect::<Vec<usize>>(),
|
||||
(0..8).collect::<Vec<usize>>()
|
||||
);
|
||||
// Both start and end wrap
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(3..5, 4, 4, 8).collect::<Vec<usize>>(),
|
||||
(0..8).collect::<Vec<usize>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1429,7 +1429,7 @@ impl Pane {
|
||||
// Always propose to save singleton files without any project paths: those cannot be saved via multibuffer, as require a file path selection modal.
|
||||
|| cx
|
||||
.update(|cx| {
|
||||
item_to_close.is_dirty(cx)
|
||||
item_to_close.can_save(cx) && item_to_close.is_dirty(cx)
|
||||
&& item_to_close.is_singleton(cx)
|
||||
&& item_to_close.project_path(cx).is_none()
|
||||
})
|
||||
@@ -3936,11 +3936,8 @@ mod tests {
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
cx.simulate_prompt_answer(2);
|
||||
cx.executor().run_until_parked();
|
||||
cx.simulate_prompt_answer(2);
|
||||
cx.executor().run_until_parked();
|
||||
save.await.unwrap();
|
||||
assert_item_labels(&pane, ["A*^", "B^", "C^"], cx);
|
||||
assert_item_labels(&pane, [], cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.166.0"
|
||||
version = "0.166.2"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
stable
|
||||
Reference in New Issue
Block a user