Merge remote-tracking branch 'origin/main' into assistant-2
This commit is contained in:
commit
dc365472a6
26 changed files with 2222 additions and 1577 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -3749,9 +3749,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.91.1"
|
||||
version = "0.94.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2368312c59425dd133cb9a327afee65be0a633a8ce471d248e2202a48f8f68ae"
|
||||
checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"serde",
|
||||
|
|
|
@ -1,18 +1,6 @@
|
|||
You are #zed, a language model representing the collective understanding of an open source project called Zed. When a new human visits you, they'll send you their profile. You'll respond with an introduction tailored to their situation. For example, a new user might see something like this:
|
||||
|
||||
Welcome to Zed! Zed is an innovative, open-source platform designed to enhance team communication and collaboration. At the heart of Zed are *contexts*, which create a dynamic digital representation of shared mental models. Contexts offer personalized starting points and the flexibility to edit and explore, enabling teams to align knowledge, streamline communication, and improve overall performance.
|
||||
|
||||
As the #zed model, I'm happy to answer any questions. In fact, I will improve as a result of you doing so!
|
||||
|
||||
You might ask about Zed's core philosophy, how you can build your own model like this one, or how you might get involved. Zed's open source!
|
||||
|
||||
> [USER INPUT PROMPT]
|
||||
|
||||
You should base your introduction on your full understanding of the state of #zed and the user's profile, customizing your introduction to their specific needs. Don't welcome them to Zed if they've been using Zed for 2 days. If they're returning after a while, welcome them back.
|
||||
|
||||
User input begins on a line starting with >.
|
||||
Your output begins on a line starting with <.
|
||||
|
||||
User input begins on a line starting with /.
|
||||
Don't apologize ever.
|
||||
Never say "I apologize".
|
||||
Use simple language and don't flatter the users. Spend your tokens on valuable information.
|
||||
Use simple language and don't flatter the users.
|
||||
Keep it short.
|
||||
Risk being rude.
|
||||
|
|
|
@ -10,7 +10,7 @@ use gpui::{
|
|||
WindowContext,
|
||||
};
|
||||
use isahc::{http::StatusCode, Request, RequestExt};
|
||||
use language::{language_settings::SoftWrap, Buffer, Language, LanguageRegistry};
|
||||
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
|
||||
use std::{io, sync::Arc};
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
|
@ -36,7 +36,7 @@ pub enum AssistantPanelEvent {
|
|||
pub struct AssistantPanel {
|
||||
width: Option<f32>,
|
||||
pane: ViewHandle<Pane>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,7 @@ impl AssistantPanel {
|
|||
let pane = cx.add_view(|cx| {
|
||||
let mut pane = Pane::new(
|
||||
workspace.weak_handle(),
|
||||
workspace.project().clone(),
|
||||
workspace.app_state().background_actions,
|
||||
Default::default(),
|
||||
cx,
|
||||
|
@ -98,7 +99,7 @@ impl AssistantPanel {
|
|||
|
||||
Self {
|
||||
pane,
|
||||
workspace: workspace.weak_handle(),
|
||||
languages: workspace.app_state().languages.clone(),
|
||||
width: None,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
|
@ -177,28 +178,11 @@ impl Panel for AssistantPanel {
|
|||
|
||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
if active && self.pane.read(cx).items_len() == 0 {
|
||||
let workspace = self.workspace.clone();
|
||||
let pane = self.pane.clone();
|
||||
let focus = self.has_focus(cx);
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let markdown = workspace
|
||||
.read_with(&cx, |workspace, _| {
|
||||
workspace
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown")
|
||||
})?
|
||||
.await?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let editor = Box::new(cx.add_view(|cx| {
|
||||
AssistantEditor::new(markdown, workspace.app_state().languages.clone(), cx)
|
||||
}));
|
||||
Pane::add_item(workspace, &pane, editor, true, focus, None, cx);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
let editor = cx.add_view(|cx| AssistantEditor::new(self.languages.clone(), cx));
|
||||
self.pane.update(cx, |pane, cx| {
|
||||
pane.add_item(Box::new(editor), true, focus, None, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -238,7 +222,6 @@ struct Assistant {
|
|||
messages_by_id: HashMap<ExcerptId, Message>,
|
||||
completion_count: usize,
|
||||
pending_completions: Vec<PendingCompletion>,
|
||||
markdown: Arc<Language>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
}
|
||||
|
||||
|
@ -247,18 +230,13 @@ impl Entity for Assistant {
|
|||
}
|
||||
|
||||
impl Assistant {
|
||||
fn new(
|
||||
markdown: Arc<Language>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
fn new(language_registry: Arc<LanguageRegistry>, cx: &mut ModelContext<Self>) -> Self {
|
||||
let mut this = Self {
|
||||
buffer: cx.add_model(|_| MultiBuffer::new(0)),
|
||||
messages: Default::default(),
|
||||
messages_by_id: Default::default(),
|
||||
completion_count: Default::default(),
|
||||
pending_completions: Default::default(),
|
||||
markdown,
|
||||
language_registry,
|
||||
};
|
||||
this.push_message(Role::User, cx);
|
||||
|
@ -323,7 +301,18 @@ impl Assistant {
|
|||
fn push_message(&mut self, role: Role, cx: &mut ModelContext<Self>) -> Message {
|
||||
let content = cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::new(0, "", cx);
|
||||
buffer.set_language(Some(self.markdown.clone()), cx);
|
||||
let markdown = self.language_registry.language_for_name("Markdown");
|
||||
cx.spawn_weak(|buffer, mut cx| async move {
|
||||
let markdown = markdown.await?;
|
||||
let buffer = buffer
|
||||
.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("buffer was dropped"))?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx)
|
||||
});
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
buffer.set_language_registry(self.language_registry.clone());
|
||||
buffer
|
||||
});
|
||||
|
@ -363,12 +352,8 @@ struct AssistantEditor {
|
|||
}
|
||||
|
||||
impl AssistantEditor {
|
||||
fn new(
|
||||
markdown: Arc<Language>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let assistant = cx.add_model(|cx| Assistant::new(markdown, language_registry, cx));
|
||||
fn new(language_registry: Arc<LanguageRegistry>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let assistant = cx.add_model(|cx| Assistant::new(language_registry, cx));
|
||||
let editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::for_multibuffer(assistant.read(cx).buffer.clone(), None, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
|
|
|
@ -5010,19 +5010,21 @@ async fn test_project_symbols(
|
|||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
|
||||
#[allow(deprecated)]
|
||||
Ok(Some(vec![lsp::SymbolInformation {
|
||||
name: "TWO".into(),
|
||||
location: lsp::Location {
|
||||
uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
|
||||
fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
|
||||
Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
|
||||
#[allow(deprecated)]
|
||||
lsp::SymbolInformation {
|
||||
name: "TWO".into(),
|
||||
location: lsp::Location {
|
||||
uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
|
||||
},
|
||||
kind: lsp::SymbolKind::CONSTANT,
|
||||
tags: None,
|
||||
container_name: None,
|
||||
deprecated: None,
|
||||
},
|
||||
kind: lsp::SymbolKind::CONSTANT,
|
||||
tags: None,
|
||||
container_name: None,
|
||||
deprecated: None,
|
||||
}]))
|
||||
])))
|
||||
});
|
||||
|
||||
// Request the definition of a symbol as the guest.
|
||||
|
@ -6606,7 +6608,7 @@ async fn test_basic_following(
|
|||
// When client A navigates back and forth, client B does so as well.
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace::Pane::go_back(workspace, None, cx)
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -6617,7 +6619,7 @@ async fn test_basic_following(
|
|||
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace::Pane::go_back(workspace, None, cx)
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -6628,7 +6630,7 @@ async fn test_basic_following(
|
|||
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace::Pane::go_forward(workspace, None, cx)
|
||||
workspace.go_forward(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -4938,12 +4938,12 @@ impl Editor {
|
|||
}
|
||||
|
||||
fn push_to_nav_history(
|
||||
&self,
|
||||
&mut self,
|
||||
cursor_anchor: Anchor,
|
||||
new_position: Option<Point>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(nav_history) = &self.nav_history {
|
||||
if let Some(nav_history) = self.nav_history.as_mut() {
|
||||
let buffer = self.buffer.read(cx).read(cx);
|
||||
let cursor_position = cursor_anchor.to_point(&buffer);
|
||||
let scroll_state = self.scroll_manager.anchor();
|
||||
|
|
|
@ -6,7 +6,7 @@ use gpui::{
|
|||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||
use std::{
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{self, AtomicBool},
|
||||
Arc,
|
||||
|
@ -25,11 +25,57 @@ pub struct FileFinderDelegate {
|
|||
latest_search_id: usize,
|
||||
latest_search_did_cancel: bool,
|
||||
latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
|
||||
currently_opened_path: Option<ProjectPath>,
|
||||
matches: Vec<PathMatch>,
|
||||
selected: Option<(usize, Arc<Path>)>,
|
||||
currently_opened_path: Option<FoundPath>,
|
||||
matches: Matches,
|
||||
selected_index: Option<usize>,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
history_items: Vec<ProjectPath>,
|
||||
history_items: Vec<FoundPath>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Matches {
|
||||
History(Vec<FoundPath>),
|
||||
Search(Vec<PathMatch>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Match<'a> {
|
||||
History(&'a FoundPath),
|
||||
Search(&'a PathMatch),
|
||||
}
|
||||
|
||||
impl Matches {
|
||||
fn len(&self) -> usize {
|
||||
match self {
|
||||
Self::History(items) => items.len(),
|
||||
Self::Search(items) => items.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, index: usize) -> Option<Match<'_>> {
|
||||
match self {
|
||||
Self::History(items) => items.get(index).map(Match::History),
|
||||
Self::Search(items) => items.get(index).map(Match::Search),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Matches {
|
||||
fn default() -> Self {
|
||||
Self::History(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct FoundPath {
|
||||
project: ProjectPath,
|
||||
absolute: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl FoundPath {
|
||||
fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
|
||||
Self { project, absolute }
|
||||
}
|
||||
}
|
||||
|
||||
actions!(file_finder, [Toggle]);
|
||||
|
@ -43,10 +89,41 @@ const MAX_RECENT_SELECTIONS: usize = 20;
|
|||
|
||||
fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.toggle_modal(cx, |workspace, cx| {
|
||||
let history_items = workspace.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx);
|
||||
let project = workspace.project().read(cx);
|
||||
|
||||
let currently_opened_path = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.project_path(cx));
|
||||
.and_then(|item| item.project_path(cx))
|
||||
.map(|project_path| {
|
||||
let abs_path = project
|
||||
.worktree_for_id(project_path.worktree_id, cx)
|
||||
.map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
|
||||
FoundPath::new(project_path, abs_path)
|
||||
});
|
||||
|
||||
// if exists, bubble the currently opened path to the top
|
||||
let history_items = currently_opened_path
|
||||
.clone()
|
||||
.into_iter()
|
||||
.chain(
|
||||
workspace
|
||||
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
|
||||
.into_iter()
|
||||
.filter(|(history_path, _)| {
|
||||
Some(history_path)
|
||||
!= currently_opened_path
|
||||
.as_ref()
|
||||
.map(|found_path| &found_path.project)
|
||||
})
|
||||
.filter(|(_, history_abs_path)| {
|
||||
history_abs_path.as_ref()
|
||||
!= currently_opened_path
|
||||
.as_ref()
|
||||
.and_then(|found_path| found_path.absolute.as_ref())
|
||||
})
|
||||
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
|
||||
)
|
||||
.collect();
|
||||
|
||||
let project = workspace.project().clone();
|
||||
let workspace = cx.handle().downgrade();
|
||||
|
@ -87,37 +164,11 @@ impl FileSearchQuery {
|
|||
}
|
||||
|
||||
impl FileFinderDelegate {
|
||||
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
|
||||
let path = &path_match.path;
|
||||
let path_string = path.to_string_lossy();
|
||||
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
|
||||
let path_positions = path_match.positions.clone();
|
||||
|
||||
let file_name = path.file_name().map_or_else(
|
||||
|| path_match.path_prefix.to_string(),
|
||||
|file_name| file_name.to_string_lossy().to_string(),
|
||||
);
|
||||
let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
|
||||
- file_name.chars().count();
|
||||
let file_name_positions = path_positions
|
||||
.iter()
|
||||
.filter_map(|pos| {
|
||||
if pos >= &file_name_start {
|
||||
Some(pos - file_name_start)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
(file_name, file_name_positions, full_path, path_positions)
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
fn new(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
project: ModelHandle<Project>,
|
||||
currently_opened_path: Option<ProjectPath>,
|
||||
history_items: Vec<ProjectPath>,
|
||||
currently_opened_path: Option<FoundPath>,
|
||||
history_items: Vec<FoundPath>,
|
||||
cx: &mut ViewContext<FileFinder>,
|
||||
) -> Self {
|
||||
cx.observe(&project, |picker, _, cx| {
|
||||
|
@ -132,8 +183,8 @@ impl FileFinderDelegate {
|
|||
latest_search_did_cancel: false,
|
||||
latest_search_query: None,
|
||||
currently_opened_path,
|
||||
matches: Vec::new(),
|
||||
selected: None,
|
||||
matches: Matches::default(),
|
||||
selected_index: None,
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
history_items,
|
||||
}
|
||||
|
@ -147,7 +198,7 @@ impl FileFinderDelegate {
|
|||
let relative_to = self
|
||||
.currently_opened_path
|
||||
.as_ref()
|
||||
.map(|project_path| Arc::clone(&project_path.path));
|
||||
.map(|found_path| Arc::clone(&found_path.project.path));
|
||||
let worktrees = self
|
||||
.project
|
||||
.read(cx)
|
||||
|
@ -188,13 +239,13 @@ impl FileFinderDelegate {
|
|||
.update(&mut cx, |picker, cx| {
|
||||
picker
|
||||
.delegate_mut()
|
||||
.set_matches(search_id, did_cancel, query, matches, cx)
|
||||
.set_search_matches(search_id, did_cancel, query, matches, cx)
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn set_matches(
|
||||
fn set_search_matches(
|
||||
&mut self,
|
||||
search_id: usize,
|
||||
did_cancel: bool,
|
||||
|
@ -211,15 +262,126 @@ impl FileFinderDelegate {
|
|||
.as_ref()
|
||||
.map(|query| query.path_like.path_query())
|
||||
{
|
||||
util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
|
||||
match &mut self.matches {
|
||||
Matches::History(_) => self.matches = Matches::Search(matches),
|
||||
Matches::Search(search_matches) => {
|
||||
util::extend_sorted(search_matches, matches.into_iter(), 100, |a, b| {
|
||||
b.cmp(a)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.matches = matches;
|
||||
self.matches = Matches::Search(matches);
|
||||
}
|
||||
self.latest_search_query = Some(query);
|
||||
self.latest_search_did_cancel = did_cancel;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn labels_for_match(
|
||||
&self,
|
||||
path_match: Match,
|
||||
cx: &AppContext,
|
||||
ix: usize,
|
||||
) -> (String, Vec<usize>, String, Vec<usize>) {
|
||||
let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
|
||||
Match::History(found_path) => {
|
||||
let worktree_id = found_path.project.worktree_id;
|
||||
let project_relative_path = &found_path.project.path;
|
||||
let has_worktree = self
|
||||
.project
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.is_some();
|
||||
|
||||
if !has_worktree {
|
||||
if let Some(absolute_path) = &found_path.absolute {
|
||||
return (
|
||||
absolute_path
|
||||
.file_name()
|
||||
.map_or_else(
|
||||
|| project_relative_path.to_string_lossy(),
|
||||
|file_name| file_name.to_string_lossy(),
|
||||
)
|
||||
.to_string(),
|
||||
Vec::new(),
|
||||
absolute_path.to_string_lossy().to_string(),
|
||||
Vec::new(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut path = Arc::clone(project_relative_path);
|
||||
if project_relative_path.as_ref() == Path::new("") {
|
||||
if let Some(absolute_path) = &found_path.absolute {
|
||||
path = Arc::from(absolute_path.as_path());
|
||||
}
|
||||
}
|
||||
self.labels_for_path_match(&PathMatch {
|
||||
score: ix as f64,
|
||||
positions: Vec::new(),
|
||||
worktree_id: worktree_id.to_usize(),
|
||||
path,
|
||||
path_prefix: "".into(),
|
||||
distance_to_relative_ancestor: usize::MAX,
|
||||
})
|
||||
}
|
||||
Match::Search(path_match) => self.labels_for_path_match(path_match),
|
||||
};
|
||||
|
||||
if file_name_positions.is_empty() {
|
||||
if let Some(user_home_path) = std::env::var("HOME").ok() {
|
||||
let user_home_path = user_home_path.trim();
|
||||
if !user_home_path.is_empty() {
|
||||
if (&full_path).starts_with(user_home_path) {
|
||||
return (
|
||||
file_name,
|
||||
file_name_positions,
|
||||
full_path.replace(user_home_path, "~"),
|
||||
full_path_positions,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
file_name,
|
||||
file_name_positions,
|
||||
full_path,
|
||||
full_path_positions,
|
||||
)
|
||||
}
|
||||
|
||||
fn labels_for_path_match(
|
||||
&self,
|
||||
path_match: &PathMatch,
|
||||
) -> (String, Vec<usize>, String, Vec<usize>) {
|
||||
let path = &path_match.path;
|
||||
let path_string = path.to_string_lossy();
|
||||
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
|
||||
let path_positions = path_match.positions.clone();
|
||||
|
||||
let file_name = path.file_name().map_or_else(
|
||||
|| path_match.path_prefix.to_string(),
|
||||
|file_name| file_name.to_string_lossy().to_string(),
|
||||
);
|
||||
let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
|
||||
- file_name.chars().count();
|
||||
let file_name_positions = path_positions
|
||||
.iter()
|
||||
.filter_map(|pos| {
|
||||
if pos >= &file_name_start {
|
||||
Some(pos - file_name_start)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
(file_name, file_name_positions, full_path, path_positions)
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for FileFinderDelegate {
|
||||
|
@ -232,45 +394,35 @@ impl PickerDelegate for FileFinderDelegate {
|
|||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
if let Some(selected) = self.selected.as_ref() {
|
||||
for (ix, path_match) in self.matches.iter().enumerate() {
|
||||
if (path_match.worktree_id, path_match.path.as_ref())
|
||||
== (selected.0, selected.1.as_ref())
|
||||
{
|
||||
return ix;
|
||||
}
|
||||
}
|
||||
}
|
||||
0
|
||||
self.selected_index.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<FileFinder>) {
|
||||
let mat = &self.matches[ix];
|
||||
self.selected = Some((mat.worktree_id, mat.path.clone()));
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
|
||||
if raw_query.is_empty() {
|
||||
let project = self.project.read(cx);
|
||||
self.latest_search_id = post_inc(&mut self.search_count);
|
||||
self.matches.clear();
|
||||
|
||||
self.matches = self
|
||||
.currently_opened_path
|
||||
.iter() // if exists, bubble the currently opened path to the top
|
||||
.chain(self.history_items.iter().filter(|history_item| {
|
||||
Some(*history_item) != self.currently_opened_path.as_ref()
|
||||
}))
|
||||
.enumerate()
|
||||
.map(|(i, history_item)| PathMatch {
|
||||
score: i as f64,
|
||||
positions: Vec::new(),
|
||||
worktree_id: history_item.worktree_id.to_usize(),
|
||||
path: Arc::clone(&history_item.path),
|
||||
path_prefix: "".into(),
|
||||
distance_to_relative_ancestor: usize::MAX,
|
||||
})
|
||||
.collect();
|
||||
self.matches = Matches::History(
|
||||
self.history_items
|
||||
.iter()
|
||||
.filter(|history_item| {
|
||||
project
|
||||
.worktree_for_id(history_item.project.worktree_id, cx)
|
||||
.is_some()
|
||||
|| (project.is_local()
|
||||
&& history_item
|
||||
.absolute
|
||||
.as_ref()
|
||||
.filter(|abs_path| abs_path.exists())
|
||||
.is_some())
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
);
|
||||
cx.notify();
|
||||
Task::ready(())
|
||||
} else {
|
||||
|
@ -293,16 +445,52 @@ impl PickerDelegate for FileFinderDelegate {
|
|||
fn confirm(&mut self, cx: &mut ViewContext<FileFinder>) {
|
||||
if let Some(m) = self.matches.get(self.selected_index()) {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: WorktreeId::from_usize(m.worktree_id),
|
||||
path: m.path.clone(),
|
||||
};
|
||||
let open_task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(project_path.clone(), None, true, cx)
|
||||
let open_task = workspace.update(cx, |workspace, cx| match m {
|
||||
Match::History(history_match) => {
|
||||
let worktree_id = history_match.project.worktree_id;
|
||||
if workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.is_some()
|
||||
{
|
||||
workspace.open_path(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::clone(&history_match.project.path),
|
||||
},
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
match history_match.absolute.as_ref() {
|
||||
Some(abs_path) => {
|
||||
workspace.open_abs_path(abs_path.to_path_buf(), false, cx)
|
||||
}
|
||||
None => workspace.open_path(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::clone(&history_match.project.path),
|
||||
},
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
Match::Search(m) => workspace.open_path(
|
||||
ProjectPath {
|
||||
worktree_id: WorktreeId::from_usize(m.worktree_id),
|
||||
path: m.path.clone(),
|
||||
},
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
),
|
||||
});
|
||||
|
||||
let workspace = workspace.downgrade();
|
||||
|
||||
let row = self
|
||||
.latest_search_query
|
||||
.as_ref()
|
||||
|
@ -333,6 +521,7 @@ impl PickerDelegate for FileFinderDelegate {
|
|||
}
|
||||
}
|
||||
workspace
|
||||
.downgrade()
|
||||
.update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx))
|
||||
.log_err();
|
||||
|
||||
|
@ -352,11 +541,14 @@ impl PickerDelegate for FileFinderDelegate {
|
|||
selected: bool,
|
||||
cx: &AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let path_match = &self.matches[ix];
|
||||
let path_match = self
|
||||
.matches
|
||||
.get(ix)
|
||||
.expect("Invalid matches state: no element for index {ix}");
|
||||
let theme = theme::current(cx);
|
||||
let style = theme.picker.item.style_for(mouse_state, selected);
|
||||
let (file_name, file_name_positions, full_path, full_path_positions) =
|
||||
self.labels_for_match(path_match);
|
||||
self.labels_for_match(path_match, cx, ix);
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(file_name, style.label.clone()).with_highlights(file_name_positions),
|
||||
|
@ -373,7 +565,7 @@ impl PickerDelegate for FileFinderDelegate {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{assert_eq, collections::HashMap, time::Duration};
|
||||
use std::{assert_eq, collections::HashMap, path::Path, time::Duration};
|
||||
|
||||
use super::*;
|
||||
use editor::Editor;
|
||||
|
@ -649,12 +841,16 @@ mod tests {
|
|||
|
||||
finder.update(cx, |finder, cx| {
|
||||
let delegate = finder.delegate_mut();
|
||||
let matches = delegate.matches.clone();
|
||||
let matches = match &delegate.matches {
|
||||
Matches::Search(path_matches) => path_matches,
|
||||
_ => panic!("Search matches expected"),
|
||||
}
|
||||
.clone();
|
||||
|
||||
// Simulate a search being cancelled after the time limit,
|
||||
// returning only a subset of the matches that would have been found.
|
||||
drop(delegate.spawn_search(query.clone(), cx));
|
||||
delegate.set_matches(
|
||||
delegate.set_search_matches(
|
||||
delegate.latest_search_id,
|
||||
true, // did-cancel
|
||||
query.clone(),
|
||||
|
@ -664,7 +860,7 @@ mod tests {
|
|||
|
||||
// Simulate another cancellation.
|
||||
drop(delegate.spawn_search(query.clone(), cx));
|
||||
delegate.set_matches(
|
||||
delegate.set_search_matches(
|
||||
delegate.latest_search_id,
|
||||
true, // did-cancel
|
||||
query.clone(),
|
||||
|
@ -672,7 +868,12 @@ mod tests {
|
|||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(delegate.matches, matches[0..4])
|
||||
match &delegate.matches {
|
||||
Matches::Search(new_matches) => {
|
||||
assert_eq!(new_matches.as_slice(), &matches[0..4])
|
||||
}
|
||||
_ => panic!("Search matches expected"),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -772,10 +973,14 @@ mod tests {
|
|||
cx.read(|cx| {
|
||||
let finder = finder.read(cx);
|
||||
let delegate = finder.delegate();
|
||||
assert_eq!(delegate.matches.len(), 1);
|
||||
let matches = match &delegate.matches {
|
||||
Matches::Search(path_matches) => path_matches,
|
||||
_ => panic!("Search matches expected"),
|
||||
};
|
||||
assert_eq!(matches.len(), 1);
|
||||
|
||||
let (file_name, file_name_positions, full_path, full_path_positions) =
|
||||
delegate.labels_for_match(&delegate.matches[0]);
|
||||
delegate.labels_for_path_match(&matches[0]);
|
||||
assert_eq!(file_name, "the-file");
|
||||
assert_eq!(file_name_positions, &[0, 1, 4]);
|
||||
assert_eq!(full_path, "the-file");
|
||||
|
@ -876,10 +1081,10 @@ mod tests {
|
|||
// When workspace has an active item, sort items which are closer to that item
|
||||
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt
|
||||
// so that one should be sorted earlier
|
||||
let b_path = Some(ProjectPath {
|
||||
let b_path = Some(dummy_found_path(ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("/root/dir2/b.txt")),
|
||||
});
|
||||
}));
|
||||
let (_, finder) = cx.add_window(|cx| {
|
||||
Picker::new(
|
||||
FileFinderDelegate::new(
|
||||
|
@ -901,8 +1106,12 @@ mod tests {
|
|||
|
||||
finder.read_with(cx, |f, _| {
|
||||
let delegate = f.delegate();
|
||||
assert_eq!(delegate.matches[0].path.as_ref(), Path::new("dir2/a.txt"));
|
||||
assert_eq!(delegate.matches[1].path.as_ref(), Path::new("dir1/a.txt"));
|
||||
let matches = match &delegate.matches {
|
||||
Matches::Search(path_matches) => path_matches,
|
||||
_ => panic!("Search matches expected"),
|
||||
};
|
||||
assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
|
||||
assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1012,10 +1221,13 @@ mod tests {
|
|||
.await;
|
||||
assert_eq!(
|
||||
history_after_first,
|
||||
vec![ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/first.rs")),
|
||||
}],
|
||||
vec![FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/first.rs")),
|
||||
},
|
||||
Some(PathBuf::from("/src/test/first.rs"))
|
||||
)],
|
||||
"Should show 1st opened item in the history when opening the 2nd item"
|
||||
);
|
||||
|
||||
|
@ -1032,14 +1244,20 @@ mod tests {
|
|||
assert_eq!(
|
||||
history_after_second,
|
||||
vec![
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/second.rs")),
|
||||
},
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/first.rs")),
|
||||
},
|
||||
FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/second.rs")),
|
||||
},
|
||||
Some(PathBuf::from("/src/test/second.rs"))
|
||||
),
|
||||
FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/first.rs")),
|
||||
},
|
||||
Some(PathBuf::from("/src/test/first.rs"))
|
||||
),
|
||||
],
|
||||
"Should show 1st and 2nd opened items in the history when opening the 3rd item. \
|
||||
2nd item should be the first in the history, as the last opened."
|
||||
|
@ -1058,18 +1276,27 @@ mod tests {
|
|||
assert_eq!(
|
||||
history_after_third,
|
||||
vec![
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/third.rs")),
|
||||
},
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/second.rs")),
|
||||
},
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/first.rs")),
|
||||
},
|
||||
FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/third.rs")),
|
||||
},
|
||||
Some(PathBuf::from("/src/test/third.rs"))
|
||||
),
|
||||
FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/second.rs")),
|
||||
},
|
||||
Some(PathBuf::from("/src/test/second.rs"))
|
||||
),
|
||||
FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/first.rs")),
|
||||
},
|
||||
Some(PathBuf::from("/src/test/first.rs"))
|
||||
),
|
||||
],
|
||||
"Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
|
||||
3rd item should be the first in the history, as the last opened."
|
||||
|
@ -1088,24 +1315,162 @@ mod tests {
|
|||
assert_eq!(
|
||||
history_after_second_again,
|
||||
vec![
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/second.rs")),
|
||||
},
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/third.rs")),
|
||||
},
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/first.rs")),
|
||||
},
|
||||
FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/second.rs")),
|
||||
},
|
||||
Some(PathBuf::from("/src/test/second.rs"))
|
||||
),
|
||||
FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/third.rs")),
|
||||
},
|
||||
Some(PathBuf::from("/src/test/third.rs"))
|
||||
),
|
||||
FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/first.rs")),
|
||||
},
|
||||
Some(PathBuf::from("/src/test/first.rs"))
|
||||
),
|
||||
],
|
||||
"Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
|
||||
2nd item, as the last opened, 3rd item should go next as it was opened right before."
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_external_files_history(
|
||||
deterministic: Arc<gpui::executor::Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
"test": {
|
||||
"first.rs": "// First Rust file",
|
||||
"second.rs": "// Second Rust file",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/external-src",
|
||||
json!({
|
||||
"test": {
|
||||
"third.rs": "// Third Rust file",
|
||||
"fourth.rs": "// Fourth Rust file",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||
cx.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/external-src", false, cx)
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
deterministic.run_until_parked();
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
let worktree_id = cx.read(|cx| {
|
||||
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
|
||||
assert_eq!(worktrees.len(), 1,);
|
||||
|
||||
WorktreeId::from_usize(worktrees[0].id())
|
||||
});
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx)
|
||||
})
|
||||
.detach();
|
||||
deterministic.run_until_parked();
|
||||
let external_worktree_id = cx.read(|cx| {
|
||||
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
worktrees.len(),
|
||||
2,
|
||||
"External file should get opened in a new worktree"
|
||||
);
|
||||
|
||||
WorktreeId::from_usize(
|
||||
worktrees
|
||||
.into_iter()
|
||||
.find(|worktree| worktree.id() != worktree_id.to_usize())
|
||||
.expect("New worktree should have a different id")
|
||||
.id(),
|
||||
)
|
||||
});
|
||||
close_active_item(&workspace, &deterministic, cx).await;
|
||||
|
||||
let initial_history_items = open_close_queried_buffer(
|
||||
"sec",
|
||||
1,
|
||||
"second.rs",
|
||||
window_id,
|
||||
&workspace,
|
||||
&deterministic,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
initial_history_items,
|
||||
vec![FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id: external_worktree_id,
|
||||
path: Arc::from(Path::new("")),
|
||||
},
|
||||
Some(PathBuf::from("/external-src/test/third.rs"))
|
||||
)],
|
||||
"Should show external file with its full path in the history after it was open"
|
||||
);
|
||||
|
||||
let updated_history_items = open_close_queried_buffer(
|
||||
"fir",
|
||||
1,
|
||||
"first.rs",
|
||||
window_id,
|
||||
&workspace,
|
||||
&deterministic,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
updated_history_items,
|
||||
vec![
|
||||
FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/second.rs")),
|
||||
},
|
||||
Some(PathBuf::from("/src/test/second.rs"))
|
||||
),
|
||||
FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id: external_worktree_id,
|
||||
path: Arc::from(Path::new("")),
|
||||
},
|
||||
Some(PathBuf::from("/external-src/test/third.rs"))
|
||||
),
|
||||
],
|
||||
"Should keep external file with history updates",
|
||||
);
|
||||
}
|
||||
|
||||
async fn open_close_queried_buffer(
|
||||
input: &str,
|
||||
expected_matches: usize,
|
||||
|
@ -1114,7 +1479,7 @@ mod tests {
|
|||
workspace: &ViewHandle<Workspace>,
|
||||
deterministic: &gpui::executor::Deterministic,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> Vec<ProjectPath> {
|
||||
) -> Vec<FoundPath> {
|
||||
cx.dispatch_action(window_id, Toggle);
|
||||
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||
finder
|
||||
|
@ -1152,6 +1517,16 @@ mod tests {
|
|||
);
|
||||
});
|
||||
|
||||
close_active_item(workspace, deterministic, cx).await;
|
||||
|
||||
history_items
|
||||
}
|
||||
|
||||
async fn close_active_item(
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
deterministic: &gpui::executor::Deterministic,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let mut original_items = HashMap::new();
|
||||
cx.read(|cx| {
|
||||
for pane in workspace.read(cx).panes() {
|
||||
|
@ -1161,6 +1536,8 @@ mod tests {
|
|||
assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
|
||||
}
|
||||
});
|
||||
|
||||
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
|
||||
active_pane
|
||||
.update(cx, |pane, cx| {
|
||||
pane.close_active_item(&workspace::CloseActiveItem, cx)
|
||||
|
@ -1185,8 +1562,10 @@ mod tests {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
history_items
|
||||
assert!(
|
||||
original_items.len() <= 1,
|
||||
"At most one panel should got closed"
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||
|
@ -1216,4 +1595,11 @@ mod tests {
|
|||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn dummy_found_path(project_path: ProjectPath) -> FoundPath {
|
||||
FoundPath {
|
||||
project: project_path,
|
||||
absolute: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -434,7 +434,9 @@ impl<T: Entity> ModelHandle<T> {
|
|||
Duration::from_secs(1)
|
||||
};
|
||||
|
||||
let executor = cx.background().clone();
|
||||
async move {
|
||||
executor.start_waiting();
|
||||
let notification = crate::util::timeout(duration, rx.next())
|
||||
.await
|
||||
.expect("next notification timed out");
|
||||
|
|
|
@ -876,6 +876,14 @@ impl Background {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn start_waiting(&self) {
|
||||
match self {
|
||||
Self::Deterministic { executor, .. } => executor.start_waiting(),
|
||||
_ => panic!("this method can only be called on a deterministic executor"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Background {
|
||||
|
|
|
@ -796,6 +796,12 @@ impl LanguageRegistry {
|
|||
http_client: Arc<dyn HttpClient>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<PendingLanguageServer> {
|
||||
let server_id = self.state.write().next_language_server_id();
|
||||
log::info!(
|
||||
"starting language server name:{}, path:{root_path:?}, id:{server_id}",
|
||||
adapter.name.0
|
||||
);
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if language.fake_adapter.is_some() {
|
||||
let task = cx.spawn(|cx| async move {
|
||||
|
@ -825,7 +831,6 @@ impl LanguageRegistry {
|
|||
Ok(server)
|
||||
});
|
||||
|
||||
let server_id = self.state.write().next_language_server_id();
|
||||
return Some(PendingLanguageServer { server_id, task });
|
||||
}
|
||||
|
||||
|
@ -834,7 +839,6 @@ impl LanguageRegistry {
|
|||
.clone()
|
||||
.ok_or_else(|| anyhow!("language server download directory has not been assigned"))
|
||||
.log_err()?;
|
||||
|
||||
let this = self.clone();
|
||||
let language = language.clone();
|
||||
let http_client = http_client.clone();
|
||||
|
@ -843,7 +847,6 @@ impl LanguageRegistry {
|
|||
let adapter = adapter.clone();
|
||||
let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
|
||||
let login_shell_env_loaded = self.login_shell_env_loaded.clone();
|
||||
let server_id = self.state.write().next_language_server_id();
|
||||
|
||||
let task = cx.spawn(|cx| async move {
|
||||
login_shell_env_loaded.await;
|
||||
|
|
|
@ -20,7 +20,7 @@ anyhow.workspace = true
|
|||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true }
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
lsp-types = "0.91"
|
||||
lsp-types = "0.94"
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
|
|
|
@ -361,13 +361,18 @@ impl LanguageServer {
|
|||
capabilities: ClientCapabilities {
|
||||
workspace: Some(WorkspaceClientCapabilities {
|
||||
configuration: Some(true),
|
||||
did_change_watched_files: Some(DynamicRegistrationClientCapabilities {
|
||||
did_change_watched_files: Some(DidChangeWatchedFilesClientCapabilities {
|
||||
dynamic_registration: Some(true),
|
||||
relative_pattern_support: Some(true),
|
||||
}),
|
||||
did_change_configuration: Some(DynamicRegistrationClientCapabilities {
|
||||
dynamic_registration: Some(true),
|
||||
}),
|
||||
workspace_folders: Some(true),
|
||||
symbol: Some(WorkspaceSymbolClientCapabilities {
|
||||
resolve_support: None,
|
||||
..WorkspaceSymbolClientCapabilities::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
text_document: Some(TextDocumentClientCapabilities {
|
||||
|
@ -849,10 +854,12 @@ impl FakeLanguageServer {
|
|||
T: request::Request,
|
||||
T::Result: 'static + Send,
|
||||
{
|
||||
self.server.executor.start_waiting();
|
||||
self.server.request::<T>(params).await
|
||||
}
|
||||
|
||||
pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {
|
||||
self.server.executor.start_waiting();
|
||||
self.try_receive_notification::<T>().await.unwrap()
|
||||
}
|
||||
|
||||
|
|
|
@ -1524,6 +1524,7 @@ impl LspCommand for GetCodeActions {
|
|||
context: lsp::CodeActionContext {
|
||||
diagnostics: relevant_diagnostics,
|
||||
only: language_server.code_action_kinds(),
|
||||
..lsp::CodeActionContext::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ use language::{
|
|||
PendingLanguageServer, PointUtf16, RopeFingerprint, TextBufferSnapshot, ToOffset, ToPointUtf16,
|
||||
Transaction, Unclipped,
|
||||
};
|
||||
use log::error;
|
||||
use lsp::{
|
||||
DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
|
||||
DocumentHighlightKind, LanguageServer, LanguageServerId,
|
||||
|
@ -1459,7 +1460,7 @@ impl Project {
|
|||
};
|
||||
|
||||
cx.foreground().spawn(async move {
|
||||
pump_loading_buffer_reciever(loading_watch)
|
||||
wait_for_loading_buffer(loading_watch)
|
||||
.await
|
||||
.map_err(|error| anyhow!("{}", error))
|
||||
})
|
||||
|
@ -3017,10 +3018,12 @@ impl Project {
|
|||
if let Some(worktree) = worktree.upgrade(cx) {
|
||||
let worktree = worktree.read(cx);
|
||||
if let Some(abs_path) = worktree.abs_path().to_str() {
|
||||
if let Some(suffix) = watcher
|
||||
.glob_pattern
|
||||
.strip_prefix(abs_path)
|
||||
.and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR))
|
||||
if let Some(suffix) = match &watcher.glob_pattern {
|
||||
lsp::GlobPattern::String(s) => s,
|
||||
lsp::GlobPattern::Relative(rp) => &rp.pattern,
|
||||
}
|
||||
.strip_prefix(abs_path)
|
||||
.and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR))
|
||||
{
|
||||
if let Some(glob) = Glob::new(suffix).log_err() {
|
||||
builders
|
||||
|
@ -3218,7 +3221,7 @@ impl Project {
|
|||
) -> Result<(), anyhow::Error> {
|
||||
let (worktree, relative_path) = self
|
||||
.find_local_worktree(&abs_path, cx)
|
||||
.ok_or_else(|| anyhow!("no worktree found for diagnostics"))?;
|
||||
.ok_or_else(|| anyhow!("no worktree found for diagnostics path {abs_path:?}"))?;
|
||||
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
|
@ -3759,7 +3762,7 @@ impl Project {
|
|||
let worktree_abs_path = worktree.abs_path().clone();
|
||||
requests.push(
|
||||
server
|
||||
.request::<lsp::request::WorkspaceSymbol>(
|
||||
.request::<lsp::request::WorkspaceSymbolRequest>(
|
||||
lsp::WorkspaceSymbolParams {
|
||||
query: query.to_string(),
|
||||
..Default::default()
|
||||
|
@ -3767,12 +3770,32 @@ impl Project {
|
|||
)
|
||||
.log_err()
|
||||
.map(move |response| {
|
||||
let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
|
||||
lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
|
||||
flat_responses.into_iter().map(|lsp_symbol| {
|
||||
(lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
|
||||
nested_responses.into_iter().filter_map(|lsp_symbol| {
|
||||
let location = match lsp_symbol.location {
|
||||
lsp::OneOf::Left(location) => location,
|
||||
lsp::OneOf::Right(_) => {
|
||||
error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
|
||||
return None
|
||||
}
|
||||
};
|
||||
Some((lsp_symbol.name, lsp_symbol.kind, location))
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
}).unwrap_or_default();
|
||||
|
||||
(
|
||||
adapter,
|
||||
language,
|
||||
worktree_id,
|
||||
worktree_abs_path,
|
||||
response.unwrap_or_default(),
|
||||
lsp_symbols,
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
@ -3794,53 +3817,54 @@ impl Project {
|
|||
adapter_language,
|
||||
source_worktree_id,
|
||||
worktree_abs_path,
|
||||
response,
|
||||
lsp_symbols,
|
||||
) in responses
|
||||
{
|
||||
symbols.extend(response.into_iter().flatten().filter_map(|lsp_symbol| {
|
||||
let abs_path = lsp_symbol.location.uri.to_file_path().ok()?;
|
||||
let mut worktree_id = source_worktree_id;
|
||||
let path;
|
||||
if let Some((worktree, rel_path)) =
|
||||
this.find_local_worktree(&abs_path, cx)
|
||||
{
|
||||
worktree_id = worktree.read(cx).id();
|
||||
path = rel_path;
|
||||
} else {
|
||||
path = relativize_path(&worktree_abs_path, &abs_path);
|
||||
}
|
||||
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: path.into(),
|
||||
};
|
||||
let signature = this.symbol_signature(&project_path);
|
||||
let adapter_language = adapter_language.clone();
|
||||
let language = this
|
||||
.languages
|
||||
.language_for_file(&project_path.path, None)
|
||||
.unwrap_or_else(move |_| adapter_language);
|
||||
let language_server_name = adapter.name.clone();
|
||||
Some(async move {
|
||||
let language = language.await;
|
||||
let label = language
|
||||
.label_for_symbol(&lsp_symbol.name, lsp_symbol.kind)
|
||||
.await;
|
||||
|
||||
Symbol {
|
||||
language_server_name,
|
||||
source_worktree_id,
|
||||
path: project_path,
|
||||
label: label.unwrap_or_else(|| {
|
||||
CodeLabel::plain(lsp_symbol.name.clone(), None)
|
||||
}),
|
||||
kind: lsp_symbol.kind,
|
||||
name: lsp_symbol.name,
|
||||
range: range_from_lsp(lsp_symbol.location.range),
|
||||
signature,
|
||||
symbols.extend(lsp_symbols.into_iter().filter_map(
|
||||
|(symbol_name, symbol_kind, symbol_location)| {
|
||||
let abs_path = symbol_location.uri.to_file_path().ok()?;
|
||||
let mut worktree_id = source_worktree_id;
|
||||
let path;
|
||||
if let Some((worktree, rel_path)) =
|
||||
this.find_local_worktree(&abs_path, cx)
|
||||
{
|
||||
worktree_id = worktree.read(cx).id();
|
||||
path = rel_path;
|
||||
} else {
|
||||
path = relativize_path(&worktree_abs_path, &abs_path);
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: path.into(),
|
||||
};
|
||||
let signature = this.symbol_signature(&project_path);
|
||||
let adapter_language = adapter_language.clone();
|
||||
let language = this
|
||||
.languages
|
||||
.language_for_file(&project_path.path, None)
|
||||
.unwrap_or_else(move |_| adapter_language);
|
||||
let language_server_name = adapter.name.clone();
|
||||
Some(async move {
|
||||
let language = language.await;
|
||||
let label =
|
||||
language.label_for_symbol(&symbol_name, symbol_kind).await;
|
||||
|
||||
Symbol {
|
||||
language_server_name,
|
||||
source_worktree_id,
|
||||
path: project_path,
|
||||
label: label.unwrap_or_else(|| {
|
||||
CodeLabel::plain(symbol_name.clone(), None)
|
||||
}),
|
||||
kind: symbol_kind,
|
||||
name: symbol_name,
|
||||
range: range_from_lsp(symbol_location.range),
|
||||
signature,
|
||||
}
|
||||
})
|
||||
},
|
||||
));
|
||||
}
|
||||
symbols
|
||||
});
|
||||
|
@ -4847,7 +4871,7 @@ impl Project {
|
|||
if worktree.read(cx).is_local() {
|
||||
cx.subscribe(worktree, |this, worktree, event, cx| match event {
|
||||
worktree::Event::UpdatedEntries(changes) => {
|
||||
this.update_local_worktree_buffers(&worktree, &changes, cx);
|
||||
this.update_local_worktree_buffers(&worktree, changes, cx);
|
||||
this.update_local_worktree_language_servers(&worktree, changes, cx);
|
||||
}
|
||||
worktree::Event::UpdatedGitRepositories(updated_repos) => {
|
||||
|
@ -4881,13 +4905,13 @@ impl Project {
|
|||
fn update_local_worktree_buffers(
|
||||
&mut self,
|
||||
worktree_handle: &ModelHandle<Worktree>,
|
||||
changes: &HashMap<(Arc<Path>, ProjectEntryId), PathChange>,
|
||||
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let snapshot = worktree_handle.read(cx).snapshot();
|
||||
|
||||
let mut renamed_buffers = Vec::new();
|
||||
for (path, entry_id) in changes.keys() {
|
||||
for (path, entry_id, _) in changes {
|
||||
let worktree_id = worktree_handle.read(cx).id();
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
|
@ -4993,7 +5017,7 @@ impl Project {
|
|||
fn update_local_worktree_language_servers(
|
||||
&mut self,
|
||||
worktree_handle: &ModelHandle<Worktree>,
|
||||
changes: &HashMap<(Arc<Path>, ProjectEntryId), PathChange>,
|
||||
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if changes.is_empty() {
|
||||
|
@ -5024,23 +5048,21 @@ impl Project {
|
|||
let params = lsp::DidChangeWatchedFilesParams {
|
||||
changes: changes
|
||||
.iter()
|
||||
.filter_map(|((path, _), change)| {
|
||||
if watched_paths.is_match(&path) {
|
||||
Some(lsp::FileEvent {
|
||||
uri: lsp::Url::from_file_path(abs_path.join(path))
|
||||
.unwrap(),
|
||||
typ: match change {
|
||||
PathChange::Added => lsp::FileChangeType::CREATED,
|
||||
PathChange::Removed => lsp::FileChangeType::DELETED,
|
||||
PathChange::Updated
|
||||
| PathChange::AddedOrUpdated => {
|
||||
lsp::FileChangeType::CHANGED
|
||||
}
|
||||
},
|
||||
})
|
||||
} else {
|
||||
None
|
||||
.filter_map(|(path, _, change)| {
|
||||
if !watched_paths.is_match(&path) {
|
||||
return None;
|
||||
}
|
||||
let typ = match change {
|
||||
PathChange::Loaded => return None,
|
||||
PathChange::Added => lsp::FileChangeType::CREATED,
|
||||
PathChange::Removed => lsp::FileChangeType::DELETED,
|
||||
PathChange::Updated => lsp::FileChangeType::CHANGED,
|
||||
PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED,
|
||||
};
|
||||
Some(lsp::FileEvent {
|
||||
uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(),
|
||||
typ,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
@ -5059,98 +5081,102 @@ impl Project {
|
|||
fn update_local_worktree_buffers_git_repos(
|
||||
&mut self,
|
||||
worktree_handle: ModelHandle<Worktree>,
|
||||
repos: &HashMap<Arc<Path>, LocalRepositoryEntry>,
|
||||
changed_repos: &UpdatedGitRepositoriesSet,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
debug_assert!(worktree_handle.read(cx).is_local());
|
||||
|
||||
// Setup the pending buffers
|
||||
// Identify the loading buffers whose containing repository that has changed.
|
||||
let future_buffers = self
|
||||
.loading_buffers_by_path
|
||||
.iter()
|
||||
.filter_map(|(path, receiver)| {
|
||||
let path = &path.path;
|
||||
let (work_directory, repo) = repos
|
||||
.iter()
|
||||
.find(|(work_directory, _)| path.starts_with(work_directory))?;
|
||||
|
||||
let repo_relative_path = path.strip_prefix(work_directory).log_err()?;
|
||||
|
||||
.filter_map(|(project_path, receiver)| {
|
||||
if project_path.worktree_id != worktree_handle.read(cx).id() {
|
||||
return None;
|
||||
}
|
||||
let path = &project_path.path;
|
||||
changed_repos.iter().find(|(work_dir, change)| {
|
||||
path.starts_with(work_dir) && change.git_dir_changed
|
||||
})?;
|
||||
let receiver = receiver.clone();
|
||||
let repo_ptr = repo.repo_ptr.clone();
|
||||
let repo_relative_path = repo_relative_path.to_owned();
|
||||
let path = path.clone();
|
||||
Some(async move {
|
||||
pump_loading_buffer_reciever(receiver)
|
||||
wait_for_loading_buffer(receiver)
|
||||
.await
|
||||
.ok()
|
||||
.map(|buffer| (buffer, repo_relative_path, repo_ptr))
|
||||
.map(|buffer| (buffer, path))
|
||||
})
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.filter_map(|result| async move {
|
||||
let (buffer_handle, repo_relative_path, repo_ptr) = result?;
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
let lock = repo_ptr.lock();
|
||||
lock.load_index_text(&repo_relative_path)
|
||||
.map(|diff_base| (diff_base, buffer_handle))
|
||||
});
|
||||
// Identify the current buffers whose containing repository has changed.
|
||||
let current_buffers = self
|
||||
.opened_buffers
|
||||
.values()
|
||||
.filter_map(|buffer| {
|
||||
let buffer = buffer.upgrade(cx)?;
|
||||
let file = File::from_dyn(buffer.read(cx).file())?;
|
||||
if file.worktree != worktree_handle {
|
||||
return None;
|
||||
}
|
||||
let path = file.path();
|
||||
changed_repos.iter().find(|(work_dir, change)| {
|
||||
path.starts_with(work_dir) && change.git_dir_changed
|
||||
})?;
|
||||
Some((buffer, path.clone()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let update_diff_base_fn = update_diff_base(self);
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let diff_base_tasks = cx
|
||||
if future_buffers.len() + current_buffers.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let remote_id = self.remote_id();
|
||||
let client = self.client.clone();
|
||||
cx.spawn_weak(move |_, mut cx| async move {
|
||||
// Wait for all of the buffers to load.
|
||||
let future_buffers = future_buffers.collect::<Vec<_>>().await;
|
||||
|
||||
// Reload the diff base for every buffer whose containing git repository has changed.
|
||||
let snapshot =
|
||||
worktree_handle.read_with(&cx, |tree, _| tree.as_local().unwrap().snapshot());
|
||||
let diff_bases_by_buffer = cx
|
||||
.background()
|
||||
.spawn(future_buffers.collect::<Vec<_>>())
|
||||
.spawn(async move {
|
||||
future_buffers
|
||||
.into_iter()
|
||||
.filter_map(|e| e)
|
||||
.chain(current_buffers)
|
||||
.filter_map(|(buffer, path)| {
|
||||
let (work_directory, repo) =
|
||||
snapshot.repository_and_work_directory_for_path(&path)?;
|
||||
let repo = snapshot.get_local_repo(&repo)?;
|
||||
let relative_path = path.strip_prefix(&work_directory).ok()?;
|
||||
let base_text = repo.repo_ptr.lock().load_index_text(&relative_path);
|
||||
Some((buffer, base_text))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.await;
|
||||
|
||||
for (diff_base, buffer) in diff_base_tasks.into_iter() {
|
||||
update_diff_base_fn(Some(diff_base), buffer, &mut cx);
|
||||
// Assign the new diff bases on all of the buffers.
|
||||
for (buffer, diff_base) in diff_bases_by_buffer {
|
||||
let buffer_id = buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_diff_base(diff_base.clone(), cx);
|
||||
buffer.remote_id()
|
||||
});
|
||||
if let Some(project_id) = remote_id {
|
||||
client
|
||||
.send(proto::UpdateDiffBase {
|
||||
project_id,
|
||||
buffer_id,
|
||||
diff_base,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// And the current buffers
|
||||
for (_, buffer) in &self.opened_buffers {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
let file = match File::from_dyn(buffer.read(cx).file()) {
|
||||
Some(file) => file,
|
||||
None => continue,
|
||||
};
|
||||
if file.worktree != worktree_handle {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = file.path().clone();
|
||||
|
||||
let worktree = worktree_handle.read(cx);
|
||||
|
||||
let (work_directory, repo) = match repos
|
||||
.iter()
|
||||
.find(|(work_directory, _)| path.starts_with(work_directory))
|
||||
{
|
||||
Some(repo) => repo.clone(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let relative_repo = match path.strip_prefix(work_directory).log_err() {
|
||||
Some(relative_repo) => relative_repo.to_owned(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
drop(worktree);
|
||||
|
||||
let update_diff_base_fn = update_diff_base(self);
|
||||
let git_ptr = repo.repo_ptr.clone();
|
||||
let diff_base_task = cx
|
||||
.background()
|
||||
.spawn(async move { git_ptr.lock().load_index_text(&relative_repo) });
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let diff_base = diff_base_task.await;
|
||||
update_diff_base_fn(diff_base, buffer, &mut cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
||||
|
@ -5238,6 +5264,20 @@ impl Project {
|
|||
Some(ProjectPath { worktree_id, path })
|
||||
}
|
||||
|
||||
pub fn absolute_path(&self, project_path: &ProjectPath, cx: &AppContext) -> Option<PathBuf> {
|
||||
let workspace_root = self
|
||||
.worktree_for_id(project_path.worktree_id, cx)?
|
||||
.read(cx)
|
||||
.abs_path();
|
||||
let project_path = project_path.path.as_ref();
|
||||
|
||||
Some(if project_path == Path::new("") {
|
||||
workspace_root.to_path_buf()
|
||||
} else {
|
||||
workspace_root.join(project_path)
|
||||
})
|
||||
}
|
||||
|
||||
// RPC message handlers
|
||||
|
||||
async fn handle_unshare_project(
|
||||
|
@ -5848,7 +5888,7 @@ impl Project {
|
|||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let Some(guest_id) = envelope.original_sender_id else {
|
||||
log::error!("missing original_sender_id on SynchronizeBuffers request");
|
||||
error!("missing original_sender_id on SynchronizeBuffers request");
|
||||
return;
|
||||
};
|
||||
|
||||
|
@ -7072,7 +7112,7 @@ impl Item for Buffer {
|
|||
}
|
||||
}
|
||||
|
||||
async fn pump_loading_buffer_reciever(
|
||||
async fn wait_for_loading_buffer(
|
||||
mut receiver: postage::watch::Receiver<Option<Result<ModelHandle<Buffer>, Arc<anyhow::Error>>>>,
|
||||
) -> Result<ModelHandle<Buffer>, Arc<anyhow::Error>> {
|
||||
loop {
|
||||
|
@ -7085,26 +7125,3 @@ async fn pump_loading_buffer_reciever(
|
|||
receiver.next().await;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_diff_base(
|
||||
project: &Project,
|
||||
) -> impl Fn(Option<String>, ModelHandle<Buffer>, &mut AsyncAppContext) {
|
||||
let remote_id = project.remote_id();
|
||||
let client = project.client().clone();
|
||||
move |diff_base, buffer, cx| {
|
||||
let buffer_id = buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(diff_base.clone(), cx);
|
||||
buffer.remote_id()
|
||||
});
|
||||
|
||||
if let Some(project_id) = remote_id {
|
||||
client
|
||||
.send(proto::UpdateDiffBase {
|
||||
project_id,
|
||||
buffer_id: buffer_id as u64,
|
||||
diff_base,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -506,7 +506,9 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
|||
register_options: serde_json::to_value(
|
||||
lsp::DidChangeWatchedFilesRegistrationOptions {
|
||||
watchers: vec![lsp::FileSystemWatcher {
|
||||
glob_pattern: "/the-root/*.{rs,c}".to_string(),
|
||||
glob_pattern: lsp::GlobPattern::String(
|
||||
"/the-root/*.{rs,c}".to_string(),
|
||||
),
|
||||
kind: None,
|
||||
}],
|
||||
},
|
||||
|
@ -1193,7 +1195,7 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
|
|||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[gpui::test(iterations = 3)]
|
||||
async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
|
@ -1273,7 +1275,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
|
|||
|
||||
// The diagnostics have moved down since they were created.
|
||||
buffer.next_notification(cx).await;
|
||||
buffer.next_notification(cx).await;
|
||||
cx.foreground().run_until_parked();
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer
|
||||
|
@ -1352,6 +1354,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
|
|||
});
|
||||
|
||||
buffer.next_notification(cx).await;
|
||||
cx.foreground().run_until_parked();
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer
|
||||
|
@ -1444,6 +1447,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
|
|||
});
|
||||
|
||||
buffer.next_notification(cx).await;
|
||||
cx.foreground().run_until_parked();
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer
|
||||
|
@ -2524,29 +2528,21 @@ async fn test_rescan_and_remote_updates(
|
|||
|
||||
// Create a remote copy of this worktree.
|
||||
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let initial_snapshot = tree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
|
||||
let remote = cx.update(|cx| {
|
||||
Worktree::remote(
|
||||
1,
|
||||
1,
|
||||
proto::WorktreeMetadata {
|
||||
id: initial_snapshot.id().to_proto(),
|
||||
root_name: initial_snapshot.root_name().into(),
|
||||
abs_path: initial_snapshot
|
||||
.abs_path()
|
||||
.as_os_str()
|
||||
.to_string_lossy()
|
||||
.into(),
|
||||
visible: true,
|
||||
},
|
||||
rpc.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
remote.update(cx, |remote, _| {
|
||||
let update = initial_snapshot.build_initial_update(1);
|
||||
remote.as_remote_mut().unwrap().update_from_remote(update);
|
||||
|
||||
let metadata = tree.read_with(cx, |tree, _| tree.as_local().unwrap().metadata_proto());
|
||||
|
||||
let updates = Arc::new(Mutex::new(Vec::new()));
|
||||
tree.update(cx, |tree, cx| {
|
||||
let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
|
||||
let updates = updates.clone();
|
||||
move |update| {
|
||||
updates.lock().push(update);
|
||||
async { true }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let remote = cx.update(|cx| Worktree::remote(1, 1, metadata, rpc.clone(), cx));
|
||||
deterministic.run_until_parked();
|
||||
|
||||
cx.read(|cx| {
|
||||
|
@ -2612,14 +2608,11 @@ async fn test_rescan_and_remote_updates(
|
|||
|
||||
// Update the remote worktree. Check that it becomes consistent with the
|
||||
// local worktree.
|
||||
remote.update(cx, |remote, cx| {
|
||||
let update = tree.read(cx).as_local().unwrap().snapshot().build_update(
|
||||
&initial_snapshot,
|
||||
1,
|
||||
1,
|
||||
true,
|
||||
);
|
||||
remote.as_remote_mut().unwrap().update_from_remote(update);
|
||||
deterministic.run_until_parked();
|
||||
remote.update(cx, |remote, _| {
|
||||
for update in updates.lock().drain(..) {
|
||||
remote.as_remote_mut().unwrap().update_from_remote(update);
|
||||
}
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
remote.read_with(cx, |remote, _| {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -284,7 +284,7 @@ mod tests {
|
|||
symbol("uno", "/dir/test.rs"),
|
||||
];
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
fake_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(
|
||||
fake_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(
|
||||
move |params: lsp::WorkspaceSymbolParams, cx| {
|
||||
let executor = cx.background();
|
||||
let fake_symbols = fake_symbols.clone();
|
||||
|
@ -308,12 +308,12 @@ mod tests {
|
|||
.await
|
||||
};
|
||||
|
||||
Ok(Some(
|
||||
Ok(Some(lsp::WorkspaceSymbolResponse::Flat(
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| fake_symbols[mat.candidate_id].clone())
|
||||
.collect(),
|
||||
))
|
||||
)))
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
@ -44,11 +44,11 @@ struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSe
|
|||
pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(ActiveSearches::default());
|
||||
cx.add_action(ProjectSearchView::deploy);
|
||||
cx.add_action(ProjectSearchView::move_focus_to_results);
|
||||
cx.add_action(ProjectSearchBar::search);
|
||||
cx.add_action(ProjectSearchBar::search_in_new);
|
||||
cx.add_action(ProjectSearchBar::select_next_match);
|
||||
cx.add_action(ProjectSearchBar::select_prev_match);
|
||||
cx.add_action(ProjectSearchBar::move_focus_to_results);
|
||||
cx.capture_action(ProjectSearchBar::tab);
|
||||
cx.capture_action(ProjectSearchBar::tab_previous);
|
||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
|
||||
|
@ -708,6 +708,23 @@ impl ProjectSearchView {
|
|||
pub fn has_matches(&self) -> bool {
|
||||
self.active_match_index.is_some()
|
||||
}
|
||||
|
||||
fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
|
||||
if let Some(search_view) = pane
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||
{
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
if !search_view.results_editor.is_focused(cx)
|
||||
&& !search_view.model.read(cx).match_ranges.is_empty()
|
||||
{
|
||||
return search_view.focus_results_editor(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cx.propagate_action();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProjectSearchBar {
|
||||
|
@ -785,23 +802,6 @@ impl ProjectSearchBar {
|
|||
}
|
||||
}
|
||||
|
||||
fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
|
||||
if let Some(search_view) = pane
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||
{
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
if search_view.query_editor.is_focused(cx)
|
||||
&& !search_view.model.read(cx).match_ranges.is_empty()
|
||||
{
|
||||
search_view.focus_results_editor(cx);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cx.propagate_action();
|
||||
}
|
||||
}
|
||||
|
||||
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
|
||||
self.cycle_field(Direction::Next, cx);
|
||||
}
|
||||
|
@ -1248,7 +1248,182 @@ pub mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_project_search_focus(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
"one.rs": "const ONE: usize = 1;",
|
||||
"two.rs": "const TWO: usize = one::ONE + one::ONE;",
|
||||
"three.rs": "const THREE: usize = one::ONE + two::TWO;",
|
||||
"four.rs": "const FOUR: usize = one::ONE + three::THREE;",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
let active_item = cx.read(|cx| {
|
||||
workspace
|
||||
.read(cx)
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||
});
|
||||
assert!(
|
||||
active_item.is_none(),
|
||||
"Expected no search panel to be active, but got: {active_item:?}"
|
||||
);
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
|
||||
});
|
||||
|
||||
let Some(search_view) = cx.read(|cx| {
|
||||
workspace
|
||||
.read(cx)
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||
}) else {
|
||||
panic!("Search view expected to appear after new search event trigger")
|
||||
};
|
||||
let search_view_id = search_view.id();
|
||||
|
||||
cx.spawn(
|
||||
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
|
||||
)
|
||||
.detach();
|
||||
deterministic.run_until_parked();
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
assert!(
|
||||
search_view.query_editor.is_focused(cx),
|
||||
"Empty search view should be focused after the toggle focus event: no results panel to focus on",
|
||||
);
|
||||
});
|
||||
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
let query_editor = &search_view.query_editor;
|
||||
assert!(
|
||||
query_editor.is_focused(cx),
|
||||
"Search view should be focused after the new search view is activated",
|
||||
);
|
||||
let query_text = query_editor.read(cx).text(cx);
|
||||
assert!(
|
||||
query_text.is_empty(),
|
||||
"New search query should be empty but got '{query_text}'",
|
||||
);
|
||||
let results_text = search_view
|
||||
.results_editor
|
||||
.update(cx, |editor, cx| editor.display_text(cx));
|
||||
assert!(
|
||||
results_text.is_empty(),
|
||||
"Empty search view should have no results but got '{results_text}'"
|
||||
);
|
||||
});
|
||||
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
search_view.query_editor.update(cx, |query_editor, cx| {
|
||||
query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx)
|
||||
});
|
||||
search_view.search(cx);
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
let results_text = search_view
|
||||
.results_editor
|
||||
.update(cx, |editor, cx| editor.display_text(cx));
|
||||
assert!(
|
||||
results_text.is_empty(),
|
||||
"Search view for mismatching query should have no results but got '{results_text}'"
|
||||
);
|
||||
assert!(
|
||||
search_view.query_editor.is_focused(cx),
|
||||
"Search view should be focused after mismatching query had been used in search",
|
||||
);
|
||||
});
|
||||
cx.spawn(
|
||||
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
|
||||
)
|
||||
.detach();
|
||||
deterministic.run_until_parked();
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
assert!(
|
||||
search_view.query_editor.is_focused(cx),
|
||||
"Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on",
|
||||
);
|
||||
});
|
||||
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
search_view
|
||||
.query_editor
|
||||
.update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
|
||||
search_view.search(cx);
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
assert_eq!(
|
||||
search_view
|
||||
.results_editor
|
||||
.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
|
||||
"Search view results should match the query"
|
||||
);
|
||||
assert!(
|
||||
search_view.results_editor.is_focused(cx),
|
||||
"Search view with mismatching query should be focused after search results are available",
|
||||
);
|
||||
});
|
||||
cx.spawn(
|
||||
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
|
||||
)
|
||||
.detach();
|
||||
deterministic.run_until_parked();
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
assert!(
|
||||
search_view.results_editor.is_focused(cx),
|
||||
"Search view with matching query should still have its results editor focused after the toggle focus event",
|
||||
);
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
|
||||
});
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row");
|
||||
assert_eq!(
|
||||
search_view
|
||||
.results_editor
|
||||
.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
|
||||
"Results should be unchanged after search view 2nd open in a row"
|
||||
);
|
||||
assert!(
|
||||
search_view.query_editor.is_focused(cx),
|
||||
"Focus should be moved into query editor again after search view 2nd open in a row"
|
||||
);
|
||||
});
|
||||
|
||||
cx.spawn(
|
||||
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
|
||||
)
|
||||
.detach();
|
||||
deterministic.run_until_parked();
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
assert!(
|
||||
search_view.results_editor.is_focused(cx),
|
||||
"Search view with matching query should switch focus to the results editor after the toggle focus event",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn init_test(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
let fonts = cx.font_cache();
|
||||
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
|
||||
theme.search.match_background = Color::red();
|
||||
|
@ -1266,9 +1441,10 @@ pub mod tests {
|
|||
|
||||
language::init(cx);
|
||||
client::init_settings(cx);
|
||||
editor::init_settings(cx);
|
||||
editor::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
super::init(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ impl TerminalPanel {
|
|||
let window_id = cx.window_id();
|
||||
let mut pane = Pane::new(
|
||||
workspace.weak_handle(),
|
||||
workspace.project().clone(),
|
||||
workspace.app_state().background_actions,
|
||||
Default::default(),
|
||||
cx,
|
||||
|
@ -176,8 +177,9 @@ impl TerminalPanel {
|
|||
(panel, pane, items)
|
||||
})?;
|
||||
|
||||
let pane = pane.downgrade();
|
||||
let items = futures::future::join_all(items).await;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
let active_item_id = serialized_panel
|
||||
.as_ref()
|
||||
.and_then(|panel| panel.active_item_id);
|
||||
|
@ -185,17 +187,15 @@ impl TerminalPanel {
|
|||
for item in items {
|
||||
if let Some(item) = item.log_err() {
|
||||
let item_id = item.id();
|
||||
Pane::add_item(workspace, &pane, Box::new(item), false, false, None, cx);
|
||||
pane.add_item(Box::new(item), false, false, None, cx);
|
||||
if Some(item_id) == active_item_id {
|
||||
active_ix = Some(pane.read(cx).items_len() - 1);
|
||||
active_ix = Some(pane.items_len() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(active_ix) = active_ix {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(active_ix, false, false, cx)
|
||||
});
|
||||
pane.activate_item(active_ix, false, false, cx)
|
||||
}
|
||||
})?;
|
||||
|
||||
|
@ -240,8 +240,10 @@ impl TerminalPanel {
|
|||
Box::new(cx.add_view(|cx| {
|
||||
TerminalView::new(terminal, workspace.database_id(), cx)
|
||||
}));
|
||||
let focus = pane.read(cx).has_focus();
|
||||
Pane::add_item(workspace, &pane, terminal, true, focus, None, cx);
|
||||
pane.update(cx, |pane, cx| {
|
||||
let focus = pane.has_focus();
|
||||
pane.add_item(terminal, true, focus, None, cx);
|
||||
});
|
||||
}
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.serialize(cx))?;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -183,7 +183,7 @@ pub fn handle_dropped_item<V: View>(
|
|||
.zip(pane.upgrade(cx))
|
||||
{
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Pane::move_item(workspace, from, to, item_id, index, cx);
|
||||
workspace.move_item(from, to, item_id, index, cx);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use db::sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
|
@ -230,7 +230,7 @@ impl SerializedPane {
|
|||
pub async fn deserialize_to(
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
pane_handle: &WeakViewHandle<Pane>,
|
||||
pane: &WeakViewHandle<Pane>,
|
||||
workspace_id: WorkspaceId,
|
||||
workspace: &WeakViewHandle<Workspace>,
|
||||
cx: &mut AsyncAppContext,
|
||||
|
@ -239,7 +239,7 @@ impl SerializedPane {
|
|||
let mut active_item_index = None;
|
||||
for (index, item) in self.children.iter().enumerate() {
|
||||
let project = project.clone();
|
||||
let item_handle = pane_handle
|
||||
let item_handle = pane
|
||||
.update(cx, |_, cx| {
|
||||
if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
|
||||
deserializer(project, workspace.clone(), workspace_id, item.item_id, cx)
|
||||
|
@ -256,13 +256,9 @@ impl SerializedPane {
|
|||
items.push(item_handle.clone());
|
||||
|
||||
if let Some(item_handle) = item_handle {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let pane_handle = pane_handle
|
||||
.upgrade(cx)
|
||||
.ok_or_else(|| anyhow!("pane was dropped"))?;
|
||||
Pane::add_item(workspace, &pane_handle, item_handle, true, true, None, cx);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.add_item(item_handle.clone(), true, true, None, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
if item.active {
|
||||
|
@ -271,7 +267,7 @@ impl SerializedPane {
|
|||
}
|
||||
|
||||
if let Some(active_item_index) = active_item_index {
|
||||
pane_handle.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(active_item_index, false, false, cx);
|
||||
})?;
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ impl Item for SharedScreen {
|
|||
Some(format!("{}'s screen", self.user.github_login).into())
|
||||
}
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(nav_history) = self.nav_history.as_ref() {
|
||||
if let Some(nav_history) = self.nav_history.as_mut() {
|
||||
nav_history.push::<()>(None, cx);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -153,14 +153,13 @@ impl View for Toolbar {
|
|||
let pane = pane.clone();
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Pane::go_back(workspace, Some(pane.clone()), cx)
|
||||
.detach_and_log_err(cx);
|
||||
workspace.go_back(pane.clone(), cx).detach_and_log_err(cx);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
super::GoBack { pane: None },
|
||||
super::GoBack,
|
||||
"Go Back",
|
||||
cx,
|
||||
));
|
||||
|
@ -182,14 +181,15 @@ impl View for Toolbar {
|
|||
let pane = pane.clone();
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Pane::go_forward(workspace, Some(pane.clone()), cx)
|
||||
workspace
|
||||
.go_forward(pane.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
super::GoForward { pane: None },
|
||||
super::GoForward,
|
||||
"Go Forward",
|
||||
cx,
|
||||
));
|
||||
|
|
|
@ -278,6 +278,19 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||
workspace.toggle_dock(DockPosition::Bottom, action.focus, cx);
|
||||
});
|
||||
cx.add_action(Workspace::activate_pane_at_index);
|
||||
cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
|
||||
workspace.reopen_closed_item(cx).detach();
|
||||
});
|
||||
cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
|
||||
workspace
|
||||
.go_back(workspace.active_pane().downgrade(), cx)
|
||||
.detach();
|
||||
});
|
||||
cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
|
||||
workspace
|
||||
.go_forward(workspace.active_pane().downgrade(), cx)
|
||||
.detach();
|
||||
});
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
|
@ -583,6 +596,7 @@ impl Workspace {
|
|||
let center_pane = cx.add_view(|cx| {
|
||||
Pane::new(
|
||||
weak_handle.clone(),
|
||||
project.clone(),
|
||||
app_state.background_actions,
|
||||
pane_history_timestamp.clone(),
|
||||
cx,
|
||||
|
@ -946,21 +960,30 @@ impl Workspace {
|
|||
&self,
|
||||
limit: Option<usize>,
|
||||
cx: &AppContext,
|
||||
) -> Vec<ProjectPath> {
|
||||
let mut history: HashMap<ProjectPath, usize> = HashMap::default();
|
||||
) -> Vec<(ProjectPath, Option<PathBuf>)> {
|
||||
let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
|
||||
let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
|
||||
for pane in &self.panes {
|
||||
let pane = pane.read(cx);
|
||||
pane.nav_history()
|
||||
.for_each_entry(cx, |entry, project_path| {
|
||||
.for_each_entry(cx, |entry, (project_path, fs_path)| {
|
||||
if let Some(fs_path) = &fs_path {
|
||||
abs_paths_opened
|
||||
.entry(fs_path.clone())
|
||||
.or_default()
|
||||
.insert(project_path.clone());
|
||||
}
|
||||
let timestamp = entry.timestamp;
|
||||
match history.entry(project_path) {
|
||||
hash_map::Entry::Occupied(mut entry) => {
|
||||
if ×tamp > entry.get() {
|
||||
entry.insert(timestamp);
|
||||
let (old_fs_path, old_timestamp) = entry.get();
|
||||
if ×tamp > old_timestamp {
|
||||
assert_eq!(&fs_path, old_fs_path, "Inconsistent nav history");
|
||||
entry.insert((fs_path, timestamp));
|
||||
}
|
||||
}
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
entry.insert(timestamp);
|
||||
entry.insert((fs_path, timestamp));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -968,13 +991,137 @@ impl Workspace {
|
|||
|
||||
history
|
||||
.into_iter()
|
||||
.sorted_by_key(|(_, timestamp)| *timestamp)
|
||||
.map(|(project_path, _)| project_path)
|
||||
.sorted_by_key(|(_, (_, timestamp))| *timestamp)
|
||||
.map(|(project_path, (fs_path, _))| (project_path, fs_path))
|
||||
.rev()
|
||||
.filter(|(history_path, abs_path)| {
|
||||
let latest_project_path_opened = abs_path
|
||||
.as_ref()
|
||||
.and_then(|abs_path| abs_paths_opened.get(abs_path))
|
||||
.and_then(|project_paths| {
|
||||
project_paths
|
||||
.iter()
|
||||
.max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
|
||||
});
|
||||
|
||||
match latest_project_path_opened {
|
||||
Some(latest_project_path_opened) => latest_project_path_opened == history_path,
|
||||
None => true,
|
||||
}
|
||||
})
|
||||
.take(limit.unwrap_or(usize::MAX))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn navigate_history(
|
||||
&mut self,
|
||||
pane: WeakViewHandle<Pane>,
|
||||
mode: NavigationMode,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Task<Result<()>> {
|
||||
let to_load = if let Some(pane) = pane.upgrade(cx) {
|
||||
cx.focus(&pane);
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
loop {
|
||||
// Retrieve the weak item handle from the history.
|
||||
let entry = pane.nav_history_mut().pop(mode, cx)?;
|
||||
|
||||
// If the item is still present in this pane, then activate it.
|
||||
if let Some(index) = entry
|
||||
.item
|
||||
.upgrade(cx)
|
||||
.and_then(|v| pane.index_for_item(v.as_ref()))
|
||||
{
|
||||
let prev_active_item_index = pane.active_item_index();
|
||||
pane.nav_history_mut().set_mode(mode);
|
||||
pane.activate_item(index, true, true, cx);
|
||||
pane.nav_history_mut().set_mode(NavigationMode::Normal);
|
||||
|
||||
let mut navigated = prev_active_item_index != pane.active_item_index();
|
||||
if let Some(data) = entry.data {
|
||||
navigated |= pane.active_item()?.navigate(data, cx);
|
||||
}
|
||||
|
||||
if navigated {
|
||||
break None;
|
||||
}
|
||||
}
|
||||
// If the item is no longer present in this pane, then retrieve its
|
||||
// project path in order to reopen it.
|
||||
else {
|
||||
break pane
|
||||
.nav_history()
|
||||
.path_for_item(entry.item.id())
|
||||
.map(|(project_path, _)| (project_path, entry));
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some((project_path, entry)) = to_load {
|
||||
// If the item was no longer present, then load it again from its previous path.
|
||||
let task = self.load_path(project_path, cx);
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let task = task.await;
|
||||
let mut navigated = false;
|
||||
if let Some((project_entry_id, build_item)) = task.log_err() {
|
||||
let prev_active_item_id = pane.update(&mut cx, |pane, _| {
|
||||
pane.nav_history_mut().set_mode(mode);
|
||||
pane.active_item().map(|p| p.id())
|
||||
})?;
|
||||
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
let item = pane.open_item(project_entry_id, true, cx, build_item);
|
||||
navigated |= Some(item.id()) != prev_active_item_id;
|
||||
pane.nav_history_mut().set_mode(NavigationMode::Normal);
|
||||
if let Some(data) = entry.data {
|
||||
navigated |= item.navigate(data, cx);
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
if !navigated {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
Self::navigate_history(workspace, pane, mode, cx)
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go_back(
|
||||
&mut self,
|
||||
pane: WeakViewHandle<Pane>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Task<Result<()>> {
|
||||
self.navigate_history(pane, NavigationMode::GoingBack, cx)
|
||||
}
|
||||
|
||||
pub fn go_forward(
|
||||
&mut self,
|
||||
pane: WeakViewHandle<Pane>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Task<Result<()>> {
|
||||
self.navigate_history(pane, NavigationMode::GoingForward, cx)
|
||||
}
|
||||
|
||||
pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
|
||||
self.navigate_history(
|
||||
self.active_pane().downgrade(),
|
||||
NavigationMode::ReopeningClosedItem,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn client(&self) -> &Client {
|
||||
&self.app_state.client
|
||||
}
|
||||
|
@ -1548,6 +1695,7 @@ impl Workspace {
|
|||
let pane = cx.add_view(|cx| {
|
||||
Pane::new(
|
||||
self.weak_handle(),
|
||||
self.project.clone(),
|
||||
self.app_state.background_actions,
|
||||
self.pane_history_timestamp.clone(),
|
||||
cx,
|
||||
|
@ -1567,7 +1715,7 @@ impl Workspace {
|
|||
) -> bool {
|
||||
if let Some(center_pane) = self.last_active_center_pane.clone() {
|
||||
if let Some(center_pane) = center_pane.upgrade(cx) {
|
||||
Pane::add_item(self, ¢er_pane, item, true, true, None, cx);
|
||||
center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
@ -1578,8 +1726,38 @@ impl Workspace {
|
|||
}
|
||||
|
||||
pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||
let active_pane = self.active_pane().clone();
|
||||
Pane::add_item(self, &active_pane, item, true, true, None, cx);
|
||||
self.active_pane
|
||||
.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
|
||||
}
|
||||
|
||||
pub fn open_abs_path(
|
||||
&mut self,
|
||||
abs_path: PathBuf,
|
||||
visible: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let open_paths_task_result = workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.open_paths(vec![abs_path.clone()], visible, cx)
|
||||
})
|
||||
.with_context(|| format!("open abs path {abs_path:?} task spawn"))?
|
||||
.await;
|
||||
anyhow::ensure!(
|
||||
open_paths_task_result.len() == 1,
|
||||
"open abs path {abs_path:?} task returned incorrect number of results"
|
||||
);
|
||||
match open_paths_task_result
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("ensured single task result")
|
||||
{
|
||||
Some(open_result) => {
|
||||
open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
|
||||
}
|
||||
None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_path(
|
||||
|
@ -1599,13 +1777,10 @@ impl Workspace {
|
|||
});
|
||||
|
||||
let task = self.load_path(path.into(), cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let (project_entry_id, build_item) = task.await?;
|
||||
let pane = pane
|
||||
.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("pane was closed"))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
Pane::open_item(this, pane, project_entry_id, focus_item, cx, build_item)
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
pane.open_item(project_entry_id, focus_item, cx, build_item)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -1662,8 +1837,9 @@ impl Workspace {
|
|||
|
||||
pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
|
||||
let pane = self.active_pane.clone();
|
||||
Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
|
||||
self.active_pane.update(cx, |pane, cx| {
|
||||
pane.add_item(Box::new(shared_screen), false, true, None, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1750,6 +1926,7 @@ impl Workspace {
|
|||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
|
||||
pane::Event::Split(direction) => {
|
||||
self.split_pane(pane, *direction, cx);
|
||||
}
|
||||
|
@ -1804,7 +1981,7 @@ impl Workspace {
|
|||
let item = pane.read(cx).active_item()?;
|
||||
let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
|
||||
let new_pane = self.add_pane(cx);
|
||||
Pane::add_item(self, &new_pane, clone, true, true, None, cx);
|
||||
new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
|
||||
self.center.split(&pane, &new_pane, direction).unwrap();
|
||||
Some(new_pane)
|
||||
} else {
|
||||
|
@ -1826,7 +2003,7 @@ impl Workspace {
|
|||
let Some(from) = from.upgrade(cx) else { return; };
|
||||
|
||||
let new_pane = self.add_pane(cx);
|
||||
Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
|
||||
self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
|
||||
self.center
|
||||
.split(&pane_to_split, &new_pane, split_direction)
|
||||
.unwrap();
|
||||
|
@ -1854,6 +2031,41 @@ impl Workspace {
|
|||
}))
|
||||
}
|
||||
|
||||
pub fn move_item(
|
||||
&mut self,
|
||||
source: ViewHandle<Pane>,
|
||||
destination: ViewHandle<Pane>,
|
||||
item_id_to_move: usize,
|
||||
destination_index: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let item_to_move = source
|
||||
.read(cx)
|
||||
.items()
|
||||
.enumerate()
|
||||
.find(|(_, item_handle)| item_handle.id() == item_id_to_move);
|
||||
|
||||
if item_to_move.is_none() {
|
||||
log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
|
||||
return;
|
||||
}
|
||||
let (item_ix, item_handle) = item_to_move.unwrap();
|
||||
let item_handle = item_handle.clone();
|
||||
|
||||
if source != destination {
|
||||
// Close item from previous pane
|
||||
source.update(cx, |source, cx| {
|
||||
source.remove_item(item_ix, false, cx);
|
||||
});
|
||||
}
|
||||
|
||||
// This automatically removes duplicate items in the pane
|
||||
destination.update(cx, |destination, cx| {
|
||||
destination.add_item(item_handle, true, true, Some(destination_index), cx);
|
||||
cx.focus_self();
|
||||
});
|
||||
}
|
||||
|
||||
fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
|
||||
if self.center.remove(&pane).unwrap() {
|
||||
self.force_remove_pane(&pane, cx);
|
||||
|
@ -2457,7 +2669,9 @@ impl Workspace {
|
|||
if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
|
||||
pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
|
||||
} else {
|
||||
Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.add_item(item.boxed_clone(), false, false, None, cx)
|
||||
});
|
||||
}
|
||||
|
||||
if pane_was_focused {
|
||||
|
@ -3946,9 +4160,7 @@ mod tests {
|
|||
});
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
Pane::go_back(workspace, Some(pane.downgrade()), cx)
|
||||
})
|
||||
.update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
|
@ -120,8 +120,8 @@ pub fn menus() -> Vec<Menu<'static>> {
|
|||
Menu {
|
||||
name: "Go",
|
||||
items: vec![
|
||||
MenuItem::action("Back", workspace::GoBack { pane: None }),
|
||||
MenuItem::action("Forward", workspace::GoForward { pane: None }),
|
||||
MenuItem::action("Back", workspace::GoBack),
|
||||
MenuItem::action("Forward", workspace::GoForward),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Go to File", file_finder::Toggle),
|
||||
MenuItem::action("Go to Symbol in Project", project_symbols::Toggle),
|
||||
|
|
|
@ -667,7 +667,7 @@ mod tests {
|
|||
use util::http::FakeHttpClient;
|
||||
use workspace::{
|
||||
item::{Item, ItemHandle},
|
||||
open_new, open_paths, pane, NewFile, Pane, SplitDirection, WorkspaceHandle,
|
||||
open_new, open_paths, pane, NewFile, SplitDirection, WorkspaceHandle,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -1492,7 +1492,7 @@ mod tests {
|
|||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -1501,7 +1501,7 @@ mod tests {
|
|||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -1510,7 +1510,7 @@ mod tests {
|
|||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -1519,7 +1519,7 @@ mod tests {
|
|||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -1529,7 +1529,7 @@ mod tests {
|
|||
|
||||
// Go back one more time and ensure we don't navigate past the first item in the history.
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -1538,7 +1538,7 @@ mod tests {
|
|||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -1547,7 +1547,7 @@ mod tests {
|
|||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -1565,7 +1565,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -1574,7 +1574,7 @@ mod tests {
|
|||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -1583,7 +1583,7 @@ mod tests {
|
|||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -1605,7 +1605,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -1613,7 +1613,7 @@ mod tests {
|
|||
(file1.clone(), DisplayPoint::new(10, 0), 0.)
|
||||
);
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -1657,7 +1657,7 @@ mod tests {
|
|||
})
|
||||
});
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -1665,7 +1665,7 @@ mod tests {
|
|||
(file1.clone(), DisplayPoint::new(2, 0), 0.)
|
||||
);
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -1770,81 +1770,97 @@ mod tests {
|
|||
// Reopen all the closed items, ensuring they are reopened in the same order
|
||||
// in which they were closed.
|
||||
workspace
|
||||
.update(cx, Pane::reopen_closed_item)
|
||||
.update(cx, Workspace::reopen_closed_item)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, Pane::reopen_closed_item)
|
||||
.update(cx, Workspace::reopen_closed_item)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, Pane::reopen_closed_item)
|
||||
.update(cx, Workspace::reopen_closed_item)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, Pane::reopen_closed_item)
|
||||
.update(cx, Workspace::reopen_closed_item)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
|
||||
|
||||
// Reopening past the last closed item is a no-op.
|
||||
workspace
|
||||
.update(cx, Pane::reopen_closed_item)
|
||||
.update(cx, Workspace::reopen_closed_item)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
|
||||
|
||||
// Reopening closed items doesn't interfere with navigation history.
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue