Merge pull request #1023 from zed-industries/app-menu-improvements

Correctly populate application menus' keystrokes and enabled status
This commit is contained in:
Max Brunsfeld 2022-05-20 10:22:20 -07:00 committed by GitHub
commit c4fc3d9c7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 759 additions and 643 deletions

1
Cargo.lock generated
View File

@ -907,6 +907,7 @@ dependencies = [
"fuzzy",
"gpui",
"picker",
"project",
"serde_json",
"settings",
"theme",

View File

@ -18,7 +18,10 @@
"cmd-s": "workspace::Save",
"cmd-=": "zed::IncreaseBufferFontSize",
"cmd--": "zed::DecreaseBufferFontSize",
"cmd-,": "zed::OpenSettings"
"cmd-,": "zed::OpenSettings",
"cmd-q": "zed::Quit",
"cmd-n": "workspace::OpenNew",
"cmd-o": "workspace::Open"
}
},
{

View File

@ -1630,7 +1630,7 @@ mod tests {
use gpui::{
executor::{self, Deterministic},
geometry::vector::vec2f,
ModelHandle, TestAppContext, ViewHandle,
ModelHandle, Task, TestAppContext, ViewHandle,
};
use language::{
range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
@ -1662,7 +1662,7 @@ mod tests {
time::Duration,
};
use theme::ThemeRegistry;
use workspace::{Item, SplitDirection, ToggleFollow, Workspace, WorkspaceParams};
use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
#[cfg(test)]
#[ctor::ctor]
@ -4322,13 +4322,7 @@ mod tests {
// Join the project as client B.
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
let mut params = cx_b.update(WorkspaceParams::test);
params.languages = lang_registry.clone();
params.project = project_b.clone();
params.client = client_b.client.clone();
params.user_store = client_b.user_store.clone();
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), true, cx)
@ -4563,13 +4557,7 @@ mod tests {
// Join the worktree as client B.
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
let mut params = cx_b.update(WorkspaceParams::test);
params.languages = lang_registry.clone();
params.project = project_b.clone();
params.client = client_b.client.clone();
params.user_store = client_b.user_store.clone();
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "one.rs"), true, cx)
@ -6602,13 +6590,21 @@ mod tests {
})
});
Channel::init(&client);
Project::init(&client);
cx.update(|cx| {
workspace::init(&client, cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
themes: ThemeRegistry::new((), cx.font_cache()),
fs: FakeFs::new(cx.background()),
build_window_options: || Default::default(),
initialize_workspace: |_, _, _| unimplemented!(),
});
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
Channel::init(&client);
Project::init(&client);
cx.update(|cx| workspace::init(app_state.clone(), cx));
client
.authenticate_and_connect(false, &cx.to_async())
.await
@ -6846,23 +6842,7 @@ mod tests {
cx: &mut TestAppContext,
) -> ViewHandle<Workspace> {
let (window_id, _) = cx.add_window(|_| EmptyView);
cx.add_view(window_id, |cx| {
let fs = project.read(cx).fs().clone();
Workspace::new(
&WorkspaceParams {
fs,
project: project.clone(),
user_store: self.user_store.clone(),
languages: self.language_registry.clone(),
themes: ThemeRegistry::new((), cx.font_cache().clone()),
channel_list: cx.add_model(|cx| {
ChannelList::new(self.user_store.clone(), self.client.clone(), cx)
}),
client: self.client.clone(),
},
cx,
)
})
cx.add_view(window_id, |cx| Workspace::new(project.clone(), cx))
}
async fn simulate_host(

View File

@ -12,6 +12,7 @@ editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
picker = { path = "../picker" }
project = { path = "../project" }
settings = { path = "../settings" }
util = { path = "../util" }
theme = { path = "../theme" }
@ -20,6 +21,7 @@ workspace = { path = "../workspace" }
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
serde_json = { version = "1.0.64", features = ["preserve_order"] }
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"

View File

@ -299,7 +299,8 @@ mod tests {
use super::*;
use editor::Editor;
use gpui::TestAppContext;
use workspace::{Workspace, WorkspaceParams};
use project::Project;
use workspace::{AppState, Workspace};
#[test]
fn test_humanize_action_name() {
@ -319,15 +320,16 @@ mod tests {
#[gpui::test]
async fn test_command_palette(cx: &mut TestAppContext) {
let params = cx.update(WorkspaceParams::test);
let app_state = cx.update(AppState::test);
cx.update(|cx| {
editor::init(cx);
workspace::init(&params.client, cx);
workspace::init(app_state.clone(), cx);
init(cx);
});
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
let project = Project::test(app_state.fs.clone(), [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
let editor = cx.add_view(window_id, |cx| {
let mut editor = Editor::single_line(None, cx);
editor.set_text("abc", cx);

View File

@ -23,7 +23,7 @@ use theme::IconButton;
use workspace::{
menu::{Confirm, SelectNext, SelectPrev},
sidebar::SidebarItem,
AppState, JoinProject, Workspace,
JoinProject, Workspace,
};
impl_actions!(
@ -60,7 +60,6 @@ pub struct ContactsPanel {
filter_editor: ViewHandle<Editor>,
collapsed_sections: Vec<Section>,
selection: Option<usize>,
app_state: Arc<AppState>,
_maintain_contacts: Subscription,
}
@ -92,7 +91,7 @@ pub fn init(cx: &mut MutableAppContext) {
impl ContactsPanel {
pub fn new(
app_state: Arc<AppState>,
user_store: ModelHandle<UserStore>,
workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
@ -152,8 +151,8 @@ impl ContactsPanel {
}
});
cx.subscribe(&app_state.user_store, {
let user_store = app_state.user_store.downgrade();
cx.subscribe(&user_store, {
let user_store = user_store.downgrade();
move |_, _, event, cx| {
if let Some((workspace, user_store)) =
workspace.upgrade(cx).zip(user_store.upgrade(cx))
@ -175,7 +174,6 @@ impl ContactsPanel {
let mut this = Self {
list_state: ListState::new(0, Orientation::Top, 1000., {
let this = cx.weak_handle();
let app_state = app_state.clone();
move |ix, cx| {
let this = this.upgrade(cx).unwrap();
let this = this.read(cx);
@ -222,7 +220,6 @@ impl ContactsPanel {
contact.clone(),
current_user_id,
*project_ix,
app_state.clone(),
theme,
is_last_project_for_contact,
is_selected,
@ -237,10 +234,8 @@ impl ContactsPanel {
entries: Default::default(),
match_candidates: Default::default(),
filter_editor,
_maintain_contacts: cx
.observe(&app_state.user_store, |this, _, cx| this.update_entries(cx)),
user_store: app_state.user_store.clone(),
app_state,
_maintain_contacts: cx.observe(&user_store, |this, _, cx| this.update_entries(cx)),
user_store,
};
this.update_entries(cx);
this
@ -339,7 +334,6 @@ impl ContactsPanel {
contact: Arc<Contact>,
current_user_id: Option<u64>,
project_index: usize,
app_state: Arc<AppState>,
theme: &theme::ContactsPanel,
is_last_project: bool,
is_selected: bool,
@ -444,7 +438,6 @@ impl ContactsPanel {
cx.dispatch_global_action(JoinProject {
contact: contact.clone(),
project_index,
app_state: app_state.clone(),
});
}
})
@ -770,7 +763,6 @@ impl ContactsPanel {
.dispatch_global_action(JoinProject {
contact: contact.clone(),
project_index: *project_index,
app_state: self.app_state.clone(),
}),
_ => {}
}
@ -916,19 +908,20 @@ impl PartialEq for ContactEntry {
#[cfg(test)]
mod tests {
use super::*;
use client::{proto, test::FakeServer, ChannelList, Client};
use client::{proto, test::FakeServer, Client};
use gpui::TestAppContext;
use language::LanguageRegistry;
use project::Project;
use theme::ThemeRegistry;
use workspace::WorkspaceParams;
use workspace::AppState;
#[gpui::test]
async fn test_contact_panel(cx: &mut TestAppContext) {
let (app_state, server) = init(cx).await;
let workspace_params = cx.update(WorkspaceParams::test);
let workspace = cx.add_view(0, |cx| Workspace::new(&workspace_params, cx));
let project = Project::test(app_state.fs.clone(), [], cx).await;
let workspace = cx.add_view(0, |cx| Workspace::new(project, cx));
let panel = cx.add_view(0, |cx| {
ContactsPanel::new(app_state.clone(), workspace.downgrade(), cx)
ContactsPanel::new(app_state.user_store.clone(), workspace.downgrade(), cx)
});
let get_users_request = server.receive::<proto::GetUsers>().await.unwrap();
@ -1110,13 +1103,6 @@ mod tests {
let mut client = Client::new(http_client.clone());
let server = FakeServer::for_client(100, &mut client, &cx).await;
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let channel_list =
cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx));
let get_channels = server.receive::<proto::GetChannels>().await.unwrap();
server
.respond(get_channels.receipt(), Default::default())
.await;
(
Arc::new(AppState {
@ -1125,9 +1111,8 @@ mod tests {
client,
user_store: user_store.clone(),
fs,
channel_list,
build_window_options: || unimplemented!(),
build_workspace: |_, _, _| unimplemented!(),
build_window_options: || Default::default(),
initialize_workspace: |_, _, _| {},
}),
server,
)

View File

@ -707,49 +707,42 @@ mod tests {
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
use serde_json::json;
use unindent::Unindent as _;
use workspace::WorkspaceParams;
use workspace::AppState;
#[gpui::test]
async fn test_diagnostics(cx: &mut TestAppContext) {
let params = cx.update(WorkspaceParams::test);
let project = params.project.clone();
let workspace = cx.add_view(0, |cx| Workspace::new(&params, cx));
params
let app_state = cx.update(AppState::test);
app_state
.fs
.as_fake()
.insert_tree(
"/test",
json!({
"consts.rs": "
const a: i32 = 'a';
const b: i32 = c;
"
const a: i32 = 'a';
const b: i32 = c;
"
.unindent(),
"main.rs": "
fn main() {
let x = vec![];
let y = vec![];
a(x);
b(y);
// comment 1
// comment 2
c(y);
d(x);
}
"
fn main() {
let x = vec![];
let y = vec![];
a(x);
b(y);
// comment 1
// comment 2
c(y);
d(x);
}
"
.unindent(),
}),
)
.await;
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/test", true, cx)
})
.await
.unwrap();
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
let workspace = cx.add_view(0, |cx| Workspace::new(project.clone(), cx));
// Create some diagnostics
project.update(cx, |project, cx| {

View File

@ -8849,7 +8849,7 @@ mod tests {
let fs = FakeFs::new(cx.background().clone());
fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs, ["/file.rs"], cx).await;
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
@ -8971,7 +8971,7 @@ mod tests {
let fs = FakeFs::new(cx.background().clone());
fs.insert_file("/file.rs", text).await;
let project = Project::test(fs, ["/file.rs"], cx).await;
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))

View File

@ -258,9 +258,10 @@ mod tests {
use super::*;
use editor::{Editor, Input};
use serde_json::json;
use std::path::PathBuf;
use workspace::menu::{Confirm, SelectNext};
use workspace::{Workspace, WorkspaceParams};
use workspace::{
menu::{Confirm, SelectNext},
AppState, Workspace,
};
#[ctor::ctor]
fn init_logger() {
@ -271,13 +272,13 @@ mod tests {
#[gpui::test]
async fn test_matching_paths(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
let app_state = cx.update(|cx| {
super::init(cx);
editor::init(cx);
AppState::test(cx)
});
let params = cx.update(WorkspaceParams::test);
params
app_state
.fs
.as_fake()
.insert_tree(
@ -291,16 +292,8 @@ mod tests {
)
.await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
params
.project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
cx.dispatch_action(window_id, Toggle);
let finder = cx.read(|cx| {
@ -341,32 +334,26 @@ mod tests {
#[gpui::test]
async fn test_matching_cancellation(cx: &mut gpui::TestAppContext) {
let params = cx.update(WorkspaceParams::test);
let fs = params.fs.as_fake();
fs.insert_tree(
"/dir",
json!({
"hello": "",
"goodbye": "",
"halogen-light": "",
"happiness": "",
"height": "",
"hi": "",
"hiccup": "",
}),
)
.await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
params
.project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/dir", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
let app_state = cx.update(AppState::test);
app_state
.fs
.as_fake()
.insert_tree(
"/dir",
json!({
"hello": "",
"goodbye": "",
"halogen-light": "",
"happiness": "",
"height": "",
"hi": "",
"hiccup": "",
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
@ -406,23 +393,20 @@ mod tests {
#[gpui::test]
async fn test_single_file_worktrees(cx: &mut gpui::TestAppContext) {
let params = cx.update(WorkspaceParams::test);
params
let app_state = cx.update(AppState::test);
app_state
.fs
.as_fake()
.insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
.await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
params
.project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root/the-parent-dir/the-file", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let project = Project::test(
app_state.fs.clone(),
["/root/the-parent-dir/the-file".as_ref()],
cx,
)
.await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
@ -451,10 +435,12 @@ mod tests {
finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 0));
}
#[gpui::test(retries = 5)]
#[gpui::test]
async fn test_multiple_matches_with_same_relative_path(cx: &mut gpui::TestAppContext) {
let params = cx.update(WorkspaceParams::test);
params
cx.foreground().forbid_parking();
let app_state = cx.update(AppState::test);
app_state
.fs
.as_fake()
.insert_tree(
@ -466,19 +452,13 @@ mod tests {
)
.await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
workspace
.update(cx, |workspace, cx| {
workspace.open_paths(
vec![PathBuf::from("/root/dir1"), PathBuf::from("/root/dir2")],
cx,
)
})
.await;
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let project = Project::test(
app_state.fs.clone(),
["/root/dir1".as_ref(), "/root/dir2".as_ref()],
cx,
)
.await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));

View File

@ -154,7 +154,6 @@ pub struct Menu<'a> {
pub enum MenuItem<'a> {
Action {
name: &'a str,
keystroke: Option<&'a str>,
action: Box<dyn Action>,
},
Separator,
@ -193,6 +192,20 @@ impl App {
cx.borrow_mut().quit();
}
}));
foreground_platform.on_will_open_menu(Box::new({
let cx = app.0.clone();
move || {
let mut cx = cx.borrow_mut();
cx.keystroke_matcher.clear_pending();
}
}));
foreground_platform.on_validate_menu_command(Box::new({
let cx = app.0.clone();
move |action| {
let cx = cx.borrow_mut();
!cx.keystroke_matcher.has_pending_keystrokes() && cx.is_action_available(action)
}
}));
foreground_platform.on_menu_command(Box::new({
let cx = app.0.clone();
move |action| {
@ -1070,7 +1083,8 @@ impl MutableAppContext {
}
pub fn set_menus(&mut self, menus: Vec<Menu>) {
self.foreground_platform.set_menus(menus);
self.foreground_platform
.set_menus(menus, &self.keystroke_matcher);
}
fn prompt(
@ -1364,6 +1378,26 @@ impl MutableAppContext {
})
}
pub fn is_action_available(&self, action: &dyn Action) -> bool {
let action_type = action.as_any().type_id();
if let Some(window_id) = self.cx.platform.key_window_id() {
if let Some((presenter, _)) = self.presenters_and_platform_windows.get(&window_id) {
let dispatch_path = presenter.borrow().dispatch_path(&self.cx);
for view_id in dispatch_path {
if let Some(view) = self.views.get(&(window_id, view_id)) {
let view_type = view.as_any().type_id();
if let Some(actions) = self.actions.get(&view_type) {
if actions.contains_key(&action_type) {
return true;
}
}
}
}
}
}
self.global_actions.contains_key(&action_type)
}
pub fn dispatch_action_at(&mut self, window_id: usize, view_id: usize, action: &dyn Action) {
let presenter = self
.presenters_and_platform_windows

View File

@ -215,12 +215,12 @@ where
self.autoscroll(scroll_max, size.y(), item_height);
let start = cmp::min(
((self.scroll_top() - self.padding_top) / item_height) as usize,
((self.scroll_top() - self.padding_top) / item_height.max(1.)) as usize,
self.item_count,
);
let end = cmp::min(
self.item_count,
start + (size.y() / item_height).ceil() as usize + 1,
start + (size.y() / item_height.max(1.)).ceil() as usize + 1,
);
if (start..end).contains(&sample_item_ix) {

View File

@ -123,6 +123,10 @@ impl Matcher {
self.pending.clear();
}
pub fn has_pending_keystrokes(&self) -> bool {
!self.pending.is_empty()
}
pub fn push_keystroke(
&mut self,
keystroke: Keystroke,

View File

@ -14,6 +14,7 @@ use crate::{
rect::{RectF, RectI},
vector::Vector2F,
},
keymap,
text_layout::{LineLayout, RunStyle},
Action, ClipboardItem, Menu, Scene,
};
@ -72,7 +73,9 @@ pub(crate) trait ForegroundPlatform {
fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>);
fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn set_menus(&self, menus: Vec<Menu>);
fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
fn on_will_open_menu(&self, callback: Box<dyn FnMut()>);
fn set_menus(&self, menus: Vec<Menu>, matcher: &keymap::Matcher);
fn prompt_for_paths(
&self,
options: PathPromptOptions,

View File

@ -8,7 +8,35 @@ use cocoa::{
base::{id, nil, YES},
foundation::NSString as _,
};
use std::{ffi::CStr, os::raw::c_char};
use std::{borrow::Cow, ffi::CStr, os::raw::c_char};
pub fn key_to_native(key: &str) -> Cow<str> {
use cocoa::appkit::*;
let code = match key {
"backspace" => 0x7F,
"up" => NSUpArrowFunctionKey,
"down" => NSDownArrowFunctionKey,
"left" => NSLeftArrowFunctionKey,
"right" => NSRightArrowFunctionKey,
"pageup" => NSPageUpFunctionKey,
"pagedown" => NSPageDownFunctionKey,
"delete" => NSDeleteFunctionKey,
"f1" => NSF1FunctionKey,
"f2" => NSF2FunctionKey,
"f3" => NSF3FunctionKey,
"f4" => NSF4FunctionKey,
"f5" => NSF5FunctionKey,
"f6" => NSF6FunctionKey,
"f7" => NSF7FunctionKey,
"f8" => NSF8FunctionKey,
"f9" => NSF9FunctionKey,
"f10" => NSF10FunctionKey,
"f11" => NSF11FunctionKey,
"f12" => NSF12FunctionKey,
_ => return Cow::Borrowed(key),
};
Cow::Owned(String::from_utf16(&[code]).unwrap())
}
impl Event {
pub unsafe fn from_native(native_event: id, window_height: Option<f32>) -> Option<Self> {

View File

@ -1,7 +1,6 @@
use super::{BoolExt as _, Dispatcher, FontSystem, Window};
use super::{event::key_to_native, BoolExt as _, Dispatcher, FontSystem, Window};
use crate::{
executor,
keymap::Keystroke,
executor, keymap,
platform::{self, CursorStyle},
Action, ClipboardItem, Event, Menu, MenuItem,
};
@ -90,6 +89,14 @@ unsafe fn build_classes() {
sel!(handleGPUIMenuItem:),
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(validateMenuItem:),
validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool,
);
decl.add_method(
sel!(menuWillOpen:),
menu_will_open as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(application:openURLs:),
open_urls as extern "C" fn(&mut Object, Sel, id, id),
@ -108,14 +115,22 @@ pub struct MacForegroundPlatformState {
quit: Option<Box<dyn FnMut()>>,
event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
will_open_menu: Option<Box<dyn FnMut()>>,
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
finish_launching: Option<Box<dyn FnOnce() -> ()>>,
menu_actions: Vec<Box<dyn Action>>,
}
impl MacForegroundPlatform {
unsafe fn create_menu_bar(&self, menus: Vec<Menu>) -> id {
unsafe fn create_menu_bar(
&self,
menus: Vec<Menu>,
delegate: id,
keystroke_matcher: &keymap::Matcher,
) -> id {
let menu_bar = NSMenu::new(nil).autorelease();
menu_bar.setDelegate_(delegate);
let mut state = self.0.borrow_mut();
state.menu_actions.clear();
@ -126,6 +141,7 @@ impl MacForegroundPlatform {
let menu_name = menu_config.name;
menu.setTitle_(ns_string(menu_name));
menu.setDelegate_(delegate);
for item_config in menu_config.items {
let item;
@ -134,19 +150,18 @@ impl MacForegroundPlatform {
MenuItem::Separator => {
item = NSMenuItem::separatorItem(nil);
}
MenuItem::Action {
name,
keystroke,
action,
} => {
if let Some(keystroke) = keystroke {
let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
panic!(
"Invalid keystroke for menu item {}:{} - {:?}",
menu_name, name, err
)
});
MenuItem::Action { name, action } => {
let mut keystroke = None;
if let Some(binding) = keystroke_matcher
.bindings_for_action_type(action.as_any().type_id())
.next()
{
if binding.keystrokes().len() == 1 {
keystroke = binding.keystrokes().first()
}
}
if let Some(keystroke) = keystroke {
let mut mask = NSEventModifierFlags::empty();
for (modifier, flag) in &[
(keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
@ -162,7 +177,7 @@ impl MacForegroundPlatform {
.initWithTitle_action_keyEquivalent_(
ns_string(name),
selector("handleGPUIMenuItem:"),
ns_string(&keystroke.key),
ns_string(key_to_native(&keystroke.key).as_ref()),
)
.autorelease();
item.setKeyEquivalentModifierMask_(mask);
@ -239,10 +254,18 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
self.0.borrow_mut().menu_command = Some(callback);
}
fn set_menus(&self, menus: Vec<Menu>) {
fn on_will_open_menu(&self, callback: Box<dyn FnMut()>) {
self.0.borrow_mut().will_open_menu = Some(callback);
}
fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
self.0.borrow_mut().validate_menu_command = Some(callback);
}
fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &keymap::Matcher) {
unsafe {
let app: id = msg_send![APP_CLASS, sharedApplication];
app.setMainMenu_(self.create_menu_bar(menus));
app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), keystroke_matcher));
}
}
@ -740,6 +763,34 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
}
}
extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool {
unsafe {
let mut result = false;
let platform = get_foreground_platform(this);
let mut platform = platform.0.borrow_mut();
if let Some(mut callback) = platform.validate_menu_command.take() {
let tag: NSInteger = msg_send![item, tag];
let index = tag as usize;
if let Some(action) = platform.menu_actions.get(index) {
result = callback(action.as_ref());
}
platform.validate_menu_command = Some(callback);
}
result
}
}
extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {
unsafe {
let platform = get_foreground_platform(this);
let mut platform = platform.0.borrow_mut();
if let Some(mut callback) = platform.will_open_menu.take() {
callback();
platform.will_open_menu = Some(callback);
}
}
}
unsafe fn ns_string(string: &str) -> id {
NSString::alloc(nil).init_str(string).autorelease()
}

View File

@ -1,7 +1,7 @@
use super::{AppVersion, CursorStyle, WindowBounds};
use crate::{
geometry::vector::{vec2f, Vector2F},
Action, ClipboardItem,
keymap, Action, ClipboardItem,
};
use anyhow::{anyhow, Result};
use parking_lot::Mutex;
@ -73,8 +73,9 @@ impl super::ForegroundPlatform for ForegroundPlatform {
}
fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
fn set_menus(&self, _: Vec<crate::Menu>) {}
fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {}
fn on_will_open_menu(&self, _: Box<dyn FnMut()>) {}
fn set_menus(&self, _: Vec<crate::Menu>, _: &keymap::Matcher) {}
fn prompt_for_paths(
&self,

View File

@ -497,7 +497,7 @@ impl Project {
#[cfg(any(test, feature = "test-support"))]
pub async fn test(
fs: Arc<dyn Fs>,
root_paths: impl IntoIterator<Item = impl AsRef<Path>>,
root_paths: impl IntoIterator<Item = &Path>,
cx: &mut gpui::TestAppContext,
) -> ModelHandle<Project> {
let languages = Arc::new(LanguageRegistry::test());
@ -528,6 +528,14 @@ impl Project {
&self.languages
}
pub fn client(&self) -> Arc<Client> {
self.client.clone()
}
pub fn user_store(&self) -> ModelHandle<UserStore> {
self.user_store.clone()
}
#[cfg(any(test, feature = "test-support"))]
pub fn check_invariants(&self, cx: &AppContext) {
if self.is_local() {
@ -5294,7 +5302,7 @@ mod tests {
)
.unwrap();
let project = Project::test(Arc::new(RealFs), [root_link_path], cx).await;
let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await;
project.read_with(cx, |project, cx| {
let tree = project.worktrees(cx).next().unwrap().read(cx);
@ -5378,7 +5386,7 @@ mod tests {
)
.await;
let project = Project::test(fs.clone(), ["/the-root"], cx).await;
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
project.update(cx, |project, _| {
project.languages.add(Arc::new(rust_language));
project.languages.add(Arc::new(json_language));
@ -5714,7 +5722,7 @@ mod tests {
)
.await;
let project = Project::test(fs, ["/dir/a.rs", "/dir/b.rs"], cx).await;
let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await;
let buffer_a = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@ -5825,7 +5833,7 @@ mod tests {
)
.await;
let project = Project::test(fs, ["/dir"], cx).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let worktree_id =
project.read_with(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
@ -5947,7 +5955,7 @@ mod tests {
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
let project = Project::test(fs, ["/dir"], cx).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let buffer = project
@ -6016,7 +6024,7 @@ mod tests {
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "a.rs": text })).await;
let project = Project::test(fs, ["/dir"], cx).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let buffer = project
@ -6285,7 +6293,7 @@ mod tests {
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "a.rs": text })).await;
let project = Project::test(fs, ["/dir"], cx).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
.await
@ -6376,7 +6384,7 @@ mod tests {
)
.await;
let project = Project::test(fs, ["/dir"], cx).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@ -6530,7 +6538,7 @@ mod tests {
)
.await;
let project = Project::test(fs, ["/dir"], cx).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
.await
@ -6686,7 +6694,7 @@ mod tests {
)
.await;
let project = Project::test(fs, ["/dir/b.rs"], cx).await;
let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let buffer = project
@ -6780,7 +6788,7 @@ mod tests {
)
.await;
let project = Project::test(fs, ["/dir"], cx).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
@ -6838,7 +6846,7 @@ mod tests {
)
.await;
let project = Project::test(fs, ["/dir"], cx).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
@ -6944,7 +6952,7 @@ mod tests {
)
.await;
let project = Project::test(fs.clone(), ["/dir"], cx).await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
.await
@ -6973,7 +6981,7 @@ mod tests {
)
.await;
let project = Project::test(fs.clone(), ["/dir/file1"], cx).await;
let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await;
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
.await
@ -6995,7 +7003,7 @@ mod tests {
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({})).await;
let project = Project::test(fs.clone(), ["/dir"], cx).await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let buffer = project.update(cx, |project, cx| {
project.create_buffer("", None, cx).unwrap()
});
@ -7182,7 +7190,7 @@ mod tests {
)
.await;
let project = Project::test(fs.clone(), ["/dir"], cx).await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
// Spawn multiple tasks to open paths, repeating some paths.
let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
@ -7227,7 +7235,7 @@ mod tests {
)
.await;
let project = Project::test(fs.clone(), ["/dir"], cx).await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let buffer1 = project
.update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
@ -7359,7 +7367,7 @@ mod tests {
}),
)
.await;
let project = Project::test(fs.clone(), ["/dir"], cx).await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
.await
@ -7444,7 +7452,7 @@ mod tests {
)
.await;
let project = Project::test(fs.clone(), ["/the-dir"], cx).await;
let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
.await
@ -7708,7 +7716,7 @@ mod tests {
)
.await;
let project = Project::test(fs.clone(), ["/dir"], cx).await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let buffer = project
.update(cx, |project, cx| {
@ -7827,7 +7835,7 @@ mod tests {
}),
)
.await;
let project = Project::test(fs.clone(), ["/dir"], cx).await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
assert_eq!(
search(&project, SearchQuery::text("TWO", false, true), cx)
.await

View File

@ -913,11 +913,14 @@ mod tests {
use project::FakeFs;
use serde_json::json;
use std::{collections::HashSet, path::Path};
use workspace::WorkspaceParams;
#[gpui::test]
async fn test_visible_list(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking();
cx.update(|cx| {
let settings = Settings::test(cx);
cx.set_global(settings);
});
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@ -956,9 +959,8 @@ mod tests {
)
.await;
let project = Project::test(fs.clone(), ["/root1", "/root2"], cx).await;
let params = cx.update(WorkspaceParams::test);
let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
assert_eq!(
visible_entries_as_strings(&panel, 0..50, cx),
@ -1005,6 +1007,10 @@ mod tests {
#[gpui::test(iterations = 30)]
async fn test_editing_files(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking();
cx.update(|cx| {
let settings = Settings::test(cx);
cx.set_global(settings);
});
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@ -1043,9 +1049,8 @@ mod tests {
)
.await;
let project = Project::test(fs.clone(), ["/root1", "/root2"], cx).await;
let params = cx.update(WorkspaceParams::test);
let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
select_path(&panel, "root1", cx);

View File

@ -295,7 +295,7 @@ mod tests {
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "test.rs": "" })).await;
let project = Project::test(fs.clone(), ["/dir"], cx).await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let _buffer = project

View File

@ -848,7 +848,7 @@ mod tests {
}),
)
.await;
let project = Project::test(fs.clone(), ["/dir"], cx).await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
let search_view = cx.add_view(Default::default(), |cx| {
ProjectSearchView::new(search.clone(), cx)

View File

@ -7,7 +7,7 @@ use picker::{Picker, PickerDelegate};
use settings::Settings;
use std::sync::Arc;
use theme::{Theme, ThemeRegistry};
use workspace::Workspace;
use workspace::{AppState, Workspace};
pub struct ThemeSelector {
registry: Arc<ThemeRegistry>,
@ -21,9 +21,14 @@ pub struct ThemeSelector {
actions!(theme_selector, [Toggle, Reload]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ThemeSelector::toggle);
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
Picker::<ThemeSelector>::init(cx);
cx.add_action({
let theme_registry = app_state.themes.clone();
move |workspace, _: &Toggle, cx| {
ThemeSelector::toggle(workspace, theme_registry.clone(), cx)
}
});
}
pub enum Event {
@ -63,8 +68,11 @@ impl ThemeSelector {
this
}
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
let themes = workspace.themes();
fn toggle(
workspace: &mut Workspace,
themes: Arc<ThemeRegistry>,
cx: &mut ViewContext<Workspace>,
) {
workspace.toggle_modal(cx, |_, cx| {
let this = cx.add_view(|cx| Self::new(themes, cx));
cx.subscribe(&this, Self::on_event).detach();

View File

@ -7,11 +7,12 @@ use editor::{display_map::ToDisplayPoint, Autoscroll};
use gpui::{json::json, keymap::Keystroke, ViewHandle};
use indoc::indoc;
use language::Selection;
use project::Project;
use util::{
set_eq,
test::{marked_text, marked_text_ranges_by, SetEqError},
};
use workspace::{WorkspaceHandle, WorkspaceParams};
use workspace::{AppState, WorkspaceHandle};
use crate::{state::Operator, *};
@ -30,7 +31,8 @@ impl<'a> VimTestContext<'a> {
settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
});
let params = cx.update(WorkspaceParams::test);
let params = cx.update(AppState::test);
let project = Project::test(params.fs.clone(), [], cx).await;
cx.update(|cx| {
cx.update_global(|settings: &mut Settings, _| {
@ -44,9 +46,8 @@ impl<'a> VimTestContext<'a> {
.insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
.await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
params
.project
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})

View File

@ -920,7 +920,7 @@ impl NavHistory {
#[cfg(test)]
mod tests {
use super::*;
use crate::WorkspaceParams;
use crate::AppState;
use gpui::{ModelHandle, TestAppContext, ViewContext};
use project::Project;
use std::sync::atomic::AtomicUsize;
@ -929,8 +929,9 @@ mod tests {
async fn test_close_items(cx: &mut TestAppContext) {
cx.foreground().forbid_parking();
let params = cx.update(WorkspaceParams::test);
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
let app_state = cx.update(AppState::test);
let project = Project::test(app_state.fs.clone(), None, cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
let item1 = cx.add_view(window_id, |_| {
let mut item = TestItem::new();
item.is_dirty = true;
@ -1019,8 +1020,9 @@ mod tests {
async fn test_prompting_only_on_last_item_for_entry(cx: &mut TestAppContext) {
cx.foreground().forbid_parking();
let params = cx.update(WorkspaceParams::test);
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
let app_state = cx.update(AppState::test);
let project = Project::test(app_state.fs.clone(), [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
let item = cx.add_view(window_id, |_| {
let mut item = TestItem::new();
item.is_dirty = true;

View File

@ -1,6 +1,6 @@
use crate::{
sidebar::{Side, ToggleSidebarItem},
AppState, ToggleFollow,
AppState, ToggleFollow, Workspace,
};
use anyhow::Result;
use client::{proto, Client, Contact};
@ -77,86 +77,87 @@ impl WaitingRoom {
) -> Self {
let project_id = contact.projects[project_index].id;
let client = app_state.client.clone();
let _join_task = cx.spawn_weak({
let contact = contact.clone();
|this, mut cx| async move {
let project = Project::remote(
project_id,
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
&mut cx,
)
.await;
let _join_task =
cx.spawn_weak({
let contact = contact.clone();
|this, mut cx| async move {
let project = Project::remote(
project_id,
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
&mut cx,
)
.await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
this.waiting = false;
match project {
Ok(project) => {
cx.replace_root_view(|cx| {
let mut workspace = (app_state.build_workspace)(
project.clone(),
&app_state,
cx,
);
workspace.toggle_sidebar_item(
&ToggleSidebarItem {
side: Side::Left,
item_index: 0,
},
cx,
);
if let Some((host_peer_id, _)) = project
.read(cx)
.collaborators()
.iter()
.find(|(_, collaborator)| collaborator.replica_id == 0)
{
if let Some(follow) = workspace
.toggle_follow(&ToggleFollow(*host_peer_id), cx)
{
follow.detach_and_log_err(cx);
}
}
workspace
});
}
Err(error @ _) => {
let login = &contact.user.github_login;
let message = match error {
project::JoinProjectError::HostDeclined => {
format!("@{} declined your request.", login)
}
project::JoinProjectError::HostClosedProject => {
format!(
"@{} closed their copy of {}.",
login,
humanize_list(
&contact.projects[project_index]
.worktree_root_names
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
this.waiting = false;
match project {
Ok(project) => {
cx.replace_root_view(|cx| {
let mut workspace = Workspace::new(project, cx);
(app_state.initialize_workspace)(
&mut workspace,
&app_state,
cx,
);
workspace.toggle_sidebar_item(
&ToggleSidebarItem {
side: Side::Left,
item_index: 0,
},
cx,
);
if let Some((host_peer_id, _)) =
workspace.project.read(cx).collaborators().iter().find(
|(_, collaborator)| collaborator.replica_id == 0,
)
)
}
project::JoinProjectError::HostWentOffline => {
format!("@{} went offline.", login)
}
project::JoinProjectError::Other(error) => {
log::error!("error joining project: {}", error);
"An error occurred.".to_string()
}
};
this.message = message;
cx.notify();
{
if let Some(follow) = workspace
.toggle_follow(&ToggleFollow(*host_peer_id), cx)
{
follow.detach_and_log_err(cx);
}
}
workspace
});
}
Err(error @ _) => {
let login = &contact.user.github_login;
let message = match error {
project::JoinProjectError::HostDeclined => {
format!("@{} declined your request.", login)
}
project::JoinProjectError::HostClosedProject => {
format!(
"@{} closed their copy of {}.",
login,
humanize_list(
&contact.projects[project_index]
.worktree_root_names
)
)
}
project::JoinProjectError::HostWentOffline => {
format!("@{} went offline.", login)
}
project::JoinProjectError::Other(error) => {
log::error!("error joining project: {}", error);
"An error occurred.".to_string()
}
};
this.message = message;
cx.notify();
}
}
}
})
}
})
}
Ok(())
}
});
Ok(())
}
});
Self {
project_id,

View File

@ -9,8 +9,7 @@ mod waiting_room;
use anyhow::{anyhow, Context, Result};
use client::{
proto, Authenticate, ChannelList, Client, Contact, PeerId, Subscription, TypedEnvelope, User,
UserStore,
proto, Authenticate, Client, Contact, PeerId, Subscription, TypedEnvelope, User, UserStore,
};
use clock::ReplicaId;
use collections::{hash_map, HashMap, HashSet};
@ -75,6 +74,8 @@ type FollowableItemBuilders = HashMap<
actions!(
workspace,
[
Open,
OpenNew,
Unfollow,
Save,
ActivatePreviousPane,
@ -83,16 +84,9 @@ actions!(
]
);
#[derive(Clone)]
pub struct Open(pub Arc<AppState>);
#[derive(Clone)]
pub struct OpenNew(pub Arc<AppState>);
#[derive(Clone)]
pub struct OpenPaths {
pub paths: Vec<PathBuf>,
pub app_state: Arc<AppState>,
}
#[derive(Clone)]
@ -102,31 +96,37 @@ pub struct ToggleFollow(pub PeerId);
pub struct JoinProject {
pub contact: Arc<Contact>,
pub project_index: usize,
pub app_state: Arc<AppState>,
}
impl_internal_actions!(
workspace,
[Open, OpenNew, OpenPaths, ToggleFollow, JoinProject]
);
impl_internal_actions!(workspace, [OpenPaths, ToggleFollow, JoinProject]);
pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
pane::init(cx);
cx.add_global_action(open);
cx.add_global_action(move |action: &OpenPaths, cx: &mut MutableAppContext| {
open_paths(&action.paths, &action.app_state, cx).detach();
cx.add_global_action({
let app_state = Arc::downgrade(&app_state);
move |action: &OpenPaths, cx: &mut MutableAppContext| {
if let Some(app_state) = app_state.upgrade() {
open_paths(&action.paths, &app_state, cx).detach();
}
}
});
cx.add_global_action(move |action: &OpenNew, cx: &mut MutableAppContext| {
open_new(&action.0, cx)
cx.add_global_action({
let app_state = Arc::downgrade(&app_state);
move |_: &OpenNew, cx: &mut MutableAppContext| {
if let Some(app_state) = app_state.upgrade() {
open_new(&app_state, cx)
}
}
});
cx.add_global_action(move |action: &JoinProject, cx: &mut MutableAppContext| {
join_project(
action.contact.clone(),
action.project_index,
&action.app_state,
cx,
);
cx.add_global_action({
let app_state = Arc::downgrade(&app_state);
move |action: &JoinProject, cx: &mut MutableAppContext| {
if let Some(app_state) = app_state.upgrade() {
join_project(action.contact.clone(), action.project_index, &app_state, cx);
}
}
});
cx.add_async_action(Workspace::toggle_follow);
@ -151,6 +151,7 @@ pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
workspace.activate_next_pane(cx)
});
let client = &app_state.client;
client.add_view_request_handler(Workspace::handle_follow);
client.add_view_message_handler(Workspace::handle_unfollow);
client.add_view_message_handler(Workspace::handle_update_followers);
@ -188,10 +189,8 @@ pub struct AppState {
pub client: Arc<client::Client>,
pub user_store: ModelHandle<client::UserStore>,
pub fs: Arc<dyn fs::Fs>,
pub channel_list: ModelHandle<client::ChannelList>,
pub build_window_options: fn() -> WindowOptions<'static>,
pub build_workspace:
fn(ModelHandle<Project>, &Arc<AppState>, &mut ViewContext<Workspace>) -> Workspace,
pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
}
pub trait Item: View {
@ -636,20 +635,9 @@ impl Into<AnyViewHandle> for &dyn NotificationHandle {
}
}
#[derive(Clone)]
pub struct WorkspaceParams {
pub project: ModelHandle<Project>,
pub client: Arc<Client>,
pub fs: Arc<dyn Fs>,
pub languages: Arc<LanguageRegistry>,
pub themes: Arc<ThemeRegistry>,
pub user_store: ModelHandle<UserStore>,
pub channel_list: ModelHandle<ChannelList>,
}
impl WorkspaceParams {
impl AppState {
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut MutableAppContext) -> Self {
pub fn test(cx: &mut MutableAppContext) -> Arc<Self> {
let settings = Settings::test(cx);
cx.set_global(settings);
@ -658,42 +646,16 @@ impl WorkspaceParams {
let http_client = client::test::FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let project = Project::local(
client.clone(),
user_store.clone(),
languages.clone(),
fs.clone(),
cx,
);
Self {
project,
channel_list: cx
.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
let themes = ThemeRegistry::new((), cx.font_cache().clone());
Arc::new(Self {
client,
themes: ThemeRegistry::new((), cx.font_cache().clone()),
themes,
fs,
languages,
user_store,
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn local(app_state: &Arc<AppState>, cx: &mut MutableAppContext) -> Self {
Self {
project: Project::local(
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx,
),
client: app_state.client.clone(),
fs: app_state.fs.clone(),
themes: app_state.themes.clone(),
languages: app_state.languages.clone(),
user_store: app_state.user_store.clone(),
channel_list: app_state.channel_list.clone(),
}
initialize_workspace: |_, _, _| {},
build_window_options: || Default::default(),
})
}
}
@ -708,7 +670,6 @@ pub struct Workspace {
user_store: ModelHandle<client::UserStore>,
remote_entity_subscription: Option<Subscription>,
fs: Arc<dyn Fs>,
themes: Arc<ThemeRegistry>,
modal: Option<AnyViewHandle>,
center: PaneGroup,
left_sidebar: ViewHandle<Sidebar>,
@ -744,8 +705,8 @@ enum FollowerItem {
}
impl Workspace {
pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> Self {
cx.observe(&params.project, |_, project, cx| {
pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
cx.observe(&project, |_, project, cx| {
if project.read(cx).is_read_only() {
cx.blur();
}
@ -753,7 +714,7 @@ impl Workspace {
})
.detach();
cx.subscribe(&params.project, move |this, project, event, cx| {
cx.subscribe(&project, move |this, project, event, cx| {
match event {
project::Event::RemoteIdChanged(remote_id) => {
this.project_remote_id_changed(*remote_id, cx);
@ -785,8 +746,11 @@ impl Workspace {
cx.focus(&pane);
cx.emit(Event::PaneAdded(pane.clone()));
let mut current_user = params.user_store.read(cx).watch_current_user().clone();
let mut connection_status = params.client.status().clone();
let fs = project.read(cx).fs().clone();
let user_store = project.read(cx).user_store();
let client = project.read(cx).client();
let mut current_user = user_store.read(cx).watch_current_user().clone();
let mut connection_status = client.status().clone();
let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
current_user.recv().await;
connection_status.recv().await;
@ -826,14 +790,13 @@ impl Workspace {
active_pane: pane.clone(),
status_bar,
notifications: Default::default(),
client: params.client.clone(),
client,
remote_entity_subscription: None,
user_store: params.user_store.clone(),
fs: params.fs.clone(),
themes: params.themes.clone(),
user_store,
fs,
left_sidebar,
right_sidebar,
project: params.project.clone(),
project,
leader_state: Default::default(),
follower_states_by_leader: Default::default(),
last_leaders_by_pane: Default::default(),
@ -867,10 +830,6 @@ impl Workspace {
&self.project
}
pub fn themes(&self) -> Arc<ThemeRegistry> {
self.themes.clone()
}
pub fn worktrees<'a>(
&self,
cx: &'a AppContext,
@ -2203,8 +2162,7 @@ impl std::fmt::Debug for OpenPaths {
}
}
fn open(action: &Open, cx: &mut MutableAppContext) {
let app_state = action.0.clone();
fn open(_: &Open, cx: &mut MutableAppContext) {
let mut paths = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: true,
@ -2212,7 +2170,7 @@ fn open(action: &Open, cx: &mut MutableAppContext) {
});
cx.spawn(|mut cx| async move {
if let Some(paths) = paths.recv().await.flatten() {
cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths, app_state }));
cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
}
})
.detach();
@ -2260,14 +2218,17 @@ pub fn open_paths(
.contains(&false);
cx.add_window((app_state.build_window_options)(), |cx| {
let project = Project::local(
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
let mut workspace = Workspace::new(
Project::local(
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx,
),
cx,
);
let mut workspace = (app_state.build_workspace)(project, &app_state, cx);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
if contains_directory {
workspace.toggle_sidebar_item(
&ToggleSidebarItem {
@ -2313,14 +2274,18 @@ pub fn join_project(
fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
let project = Project::local(
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
let mut workspace = Workspace::new(
Project::local(
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx,
),
cx,
);
(app_state.build_workspace)(project, &app_state, cx)
(app_state.initialize_workspace)(&mut workspace, app_state, cx);
workspace
});
cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone()));
cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew);
}

View File

@ -12,7 +12,7 @@ use cli::{
use client::{
self,
http::{self, HttpClient},
ChannelList, UserStore, ZED_SECRET_CLIENT_TOKEN,
UserStore, ZED_SECRET_CLIENT_TOKEN,
};
use fs::OpenOptions;
use futures::{
@ -40,9 +40,9 @@ use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
use util::{ResultExt, TryFutureExt};
use workspace::{self, AppState, OpenNew, OpenPaths};
use zed::{
self, build_window_options, build_workspace,
self, build_window_options,
fs::RealFs,
languages, menus,
initialize_workspace, languages, menus,
settings_file::{settings_from_files, watch_keymap_file, WatchedJsonFile},
};
@ -133,15 +133,12 @@ fn main() {
let client = client::Client::new(http.clone());
let mut languages = languages::build_language_registry(login_shell_env_loaded);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
let channel_list =
cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx));
auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
project::Project::init(&client);
client::Channel::init(&client);
client::init(client.clone(), cx);
command_palette::init(cx);
workspace::init(&client, cx);
editor::init(cx);
go_to_line::init(cx);
file_finder::init(cx);
@ -192,33 +189,33 @@ fn main() {
let app_state = Arc::new(AppState {
languages,
themes,
channel_list,
client: client.clone(),
user_store,
fs,
build_window_options,
build_workspace,
initialize_workspace,
});
workspace::init(app_state.clone(), cx);
journal::init(app_state.clone(), cx);
theme_selector::init(cx);
theme_selector::init(app_state.clone(), cx);
zed::init(&app_state, cx);
cx.set_menus(menus::menus(&app_state.clone()));
cx.set_menus(menus::menus());
if stdout_is_a_pty() {
cx.platform().activate(true);
let paths = collect_path_args();
if paths.is_empty() {
cx.dispatch_global_action(OpenNew(app_state.clone()));
cx.dispatch_global_action(OpenNew);
} else {
cx.dispatch_global_action(OpenPaths { paths, app_state });
cx.dispatch_global_action(OpenPaths { paths });
}
} else {
if let Ok(Some(connection)) = cli_connections_rx.try_next() {
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
.detach();
} else {
cx.dispatch_global_action(OpenNew(app_state.clone()));
cx.dispatch_global_action(OpenNew);
}
cx.spawn(|cx| async move {
while let Some(connection) = cli_connections_rx.next().await {

View File

@ -1,33 +1,27 @@
use crate::AppState;
use gpui::{Menu, MenuItem};
use std::sync::Arc;
#[cfg(target_os = "macos")]
pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
pub fn menus() -> Vec<Menu<'static>> {
vec![
Menu {
name: "Zed",
items: vec![
MenuItem::Action {
name: "About Zed…",
keystroke: None,
action: Box::new(super::About),
},
MenuItem::Action {
name: "Check for Updates",
keystroke: None,
action: Box::new(auto_update::Check),
},
MenuItem::Separator,
MenuItem::Action {
name: "Install CLI",
keystroke: None,
action: Box::new(super::InstallCommandLineInterface),
},
MenuItem::Separator,
MenuItem::Action {
name: "Quit",
keystroke: Some("cmd-q"),
action: Box::new(super::Quit),
},
],
@ -37,14 +31,20 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
items: vec![
MenuItem::Action {
name: "New",
keystroke: Some("cmd-n"),
action: Box::new(workspace::OpenNew(state.clone())),
action: Box::new(workspace::OpenNew),
},
MenuItem::Separator,
MenuItem::Action {
name: "Open…",
keystroke: Some("cmd-o"),
action: Box::new(workspace::Open(state.clone())),
action: Box::new(workspace::Open),
},
MenuItem::Action {
name: "Save",
action: Box::new(workspace::Save),
},
MenuItem::Action {
name: "Close Editor",
action: Box::new(workspace::CloseActiveItem),
},
],
},
@ -53,30 +53,160 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
items: vec![
MenuItem::Action {
name: "Undo",
keystroke: Some("cmd-z"),
action: Box::new(editor::Undo),
},
MenuItem::Action {
name: "Redo",
keystroke: Some("cmd-Z"),
action: Box::new(editor::Redo),
},
MenuItem::Separator,
MenuItem::Action {
name: "Cut",
keystroke: Some("cmd-x"),
action: Box::new(editor::Cut),
},
MenuItem::Action {
name: "Copy",
keystroke: Some("cmd-c"),
action: Box::new(editor::Copy),
},
MenuItem::Action {
name: "Paste",
keystroke: Some("cmd-v"),
action: Box::new(editor::Paste),
},
MenuItem::Separator,
MenuItem::Action {
name: "Find",
action: Box::new(search::buffer_search::Deploy { focus: true }),
},
MenuItem::Action {
name: "Find In Project",
action: Box::new(search::project_search::Deploy),
},
MenuItem::Separator,
MenuItem::Action {
name: "Toggle Line Comment",
action: Box::new(editor::ToggleComments),
},
],
},
Menu {
name: "Selection",
items: vec![
MenuItem::Action {
name: "Select All",
action: Box::new(editor::SelectAll),
},
MenuItem::Action {
name: "Expand Selection",
action: Box::new(editor::SelectLargerSyntaxNode),
},
MenuItem::Action {
name: "Shrink Selection",
action: Box::new(editor::SelectSmallerSyntaxNode),
},
MenuItem::Separator,
MenuItem::Action {
name: "Add Cursor Above",
action: Box::new(editor::AddSelectionAbove),
},
MenuItem::Action {
name: "Add Cursor Below",
action: Box::new(editor::AddSelectionBelow),
},
MenuItem::Action {
name: "Select Next Occurrence",
action: Box::new(editor::SelectNext {
replace_newest: false,
}),
},
MenuItem::Separator,
MenuItem::Action {
name: "Move Line Up",
action: Box::new(editor::MoveLineUp),
},
MenuItem::Action {
name: "Move Line Down",
action: Box::new(editor::MoveLineDown),
},
MenuItem::Action {
name: "Duplicate Selection",
action: Box::new(editor::DuplicateLine),
},
],
},
Menu {
name: "View",
items: vec![
MenuItem::Action {
name: "Zoom In",
action: Box::new(super::IncreaseBufferFontSize),
},
MenuItem::Action {
name: "Zoom Out",
action: Box::new(super::DecreaseBufferFontSize),
},
MenuItem::Separator,
MenuItem::Action {
name: "Project Browser",
action: Box::new(workspace::sidebar::ToggleSidebarItemFocus {
side: workspace::sidebar::Side::Left,
item_index: 0,
}),
},
MenuItem::Action {
name: "Command Palette",
action: Box::new(command_palette::Toggle),
},
MenuItem::Action {
name: "Diagnostics",
action: Box::new(diagnostics::Deploy),
},
],
},
Menu {
name: "Go",
items: vec![
MenuItem::Action {
name: "Back",
action: Box::new(workspace::GoBack { pane: None }),
},
MenuItem::Action {
name: "Forward",
action: Box::new(workspace::GoForward { pane: None }),
},
MenuItem::Separator,
MenuItem::Action {
name: "Go to File",
action: Box::new(file_finder::Toggle),
},
MenuItem::Action {
name: "Go to Symbol in Project",
action: Box::new(project_symbols::Toggle),
},
MenuItem::Action {
name: "Go to Symbol in Editor",
action: Box::new(outline::Toggle),
},
MenuItem::Action {
name: "Go to Definition",
action: Box::new(editor::GoToDefinition),
},
MenuItem::Action {
name: "Go to References",
action: Box::new(editor::FindAllReferences),
},
MenuItem::Action {
name: "Go to Line/Column",
action: Box::new(go_to_line::Toggle),
},
MenuItem::Separator,
MenuItem::Action {
name: "Next Problem",
action: Box::new(editor::GoToNextDiagnostic),
},
MenuItem::Action {
name: "Previous Problem",
action: Box::new(editor::GoToPrevDiagnostic),
},
],
},
]

View File

@ -1,13 +1,3 @@
use crate::{build_window_options, build_workspace, AppState};
use assets::Assets;
use client::{test::FakeHttpClient, ChannelList, Client, UserStore};
use gpui::MutableAppContext;
use language::LanguageRegistry;
use project::fs::FakeFs;
use settings::Settings;
use std::sync::Arc;
use theme::ThemeRegistry;
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
@ -15,32 +5,3 @@ fn init_logger() {
env_logger::init();
}
}
pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
let settings = Settings::test(cx);
editor::init(cx);
cx.set_global(settings);
let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
let languages = LanguageRegistry::test();
languages.add(Arc::new(language::Language::new(
language::LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
)));
Arc::new(AppState {
themes,
languages: Arc::new(languages),
channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
client,
user_store,
fs: FakeFs::new(cx.background().clone()),
build_window_options,
build_workspace,
})
}

View File

@ -15,7 +15,7 @@ use gpui::{
actions,
geometry::vector::vec2f,
platform::{WindowBounds, WindowOptions},
AsyncAppContext, ModelHandle, ViewContext,
AsyncAppContext, ViewContext,
};
use lazy_static::lazy_static;
pub use lsp;
@ -31,7 +31,7 @@ use std::{
};
use util::ResultExt;
pub use workspace;
use workspace::{AppState, Workspace, WorkspaceParams};
use workspace::{AppState, Workspace};
actions!(
zed,
@ -115,13 +115,13 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
settings::KeymapFileContent::load_defaults(cx);
}
pub fn build_workspace(
project: ModelHandle<Project>,
pub fn initialize_workspace(
workspace: &mut Workspace,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) -> Workspace {
) {
cx.subscribe(&cx.handle(), {
let project = project.clone();
let project = workspace.project().clone();
move |_, _, event, cx| {
if let workspace::Event::PaneAdded(pane) = event {
pane.update(cx, |pane, cx| {
@ -139,22 +139,12 @@ pub fn build_workspace(
})
.detach();
let workspace_params = WorkspaceParams {
project,
client: app_state.client.clone(),
fs: app_state.fs.clone(),
languages: app_state.languages.clone(),
themes: app_state.themes.clone(),
user_store: app_state.user_store.clone(),
channel_list: app_state.channel_list.clone(),
};
let workspace = Workspace::new(&workspace_params, cx);
let project = workspace.project().clone();
cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
let theme_names = app_state.themes.list().collect();
let language_names = app_state.languages.language_names();
project.update(cx, |project, cx| {
workspace.project().update(cx, |project, cx| {
let action_names = cx.all_action_names().collect::<Vec<_>>();
project.set_language_server_settings(serde_json::json!({
"json": {
@ -172,9 +162,10 @@ pub fn build_workspace(
}));
});
let project_panel = ProjectPanel::new(project, cx);
let contact_panel =
cx.add_view(|cx| ContactsPanel::new(app_state.clone(), workspace.weak_handle(), cx));
let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
let contact_panel = cx.add_view(|cx| {
ContactsPanel::new(app_state.user_store.clone(), workspace.weak_handle(), cx)
});
workspace.left_sidebar().update(cx, |sidebar, cx| {
sidebar.add_item("icons/folder-tree-solid-14.svg", project_panel.into(), cx)
@ -196,8 +187,6 @@ pub fn build_workspace(
status_bar.add_right_item(cursor_position, cx);
status_bar.add_right_item(auto_update, cx);
});
workspace
}
pub fn build_window_options() -> WindowOptions<'static> {
@ -287,14 +276,18 @@ fn open_config_file(
workspace.open_paths(vec![path.to_path_buf()], cx)
} else {
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
let project = Project::local(
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
let mut workspace = Workspace::new(
Project::local(
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx,
),
cx,
);
(app_state.build_workspace)(project, &app_state, cx)
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
workspace
});
workspace.update(cx, |workspace, cx| {
workspace.open_paths(vec![path.to_path_buf()], cx)
@ -313,43 +306,45 @@ mod tests {
use assets::Assets;
use editor::{Autoscroll, DisplayPoint, Editor};
use gpui::{AssetSource, MutableAppContext, TestAppContext, ViewHandle};
use project::{Fs, ProjectPath};
use project::ProjectPath;
use serde_json::json;
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use test::test_app_state;
use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
use util::test::temp_tree;
use workspace::{
open_paths, pane, Item, ItemHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle,
};
#[gpui::test]
async fn test_open_paths_action(cx: &mut TestAppContext) {
let app_state = cx.update(test_app_state);
let dir = temp_tree(json!({
"a": {
"aa": null,
"ab": null,
},
"b": {
"ba": null,
"bb": null,
},
"c": {
"ca": null,
"cb": null,
},
}));
let app_state = init(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"a": {
"aa": null,
"ab": null,
},
"b": {
"ba": null,
"bb": null,
},
"c": {
"ca": null,
"cb": null,
},
}),
)
.await;
cx.update(|cx| {
open_paths(
&[
dir.path().join("a").to_path_buf(),
dir.path().join("b").to_path_buf(),
],
&[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
&app_state,
cx,
)
@ -357,7 +352,7 @@ mod tests {
.await;
assert_eq!(cx.window_ids().len(), 1);
cx.update(|cx| open_paths(&[dir.path().join("a").to_path_buf()], &app_state, cx))
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
.await;
assert_eq!(cx.window_ids().len(), 1);
let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
@ -369,10 +364,7 @@ mod tests {
cx.update(|cx| {
open_paths(
&[
dir.path().join("b").to_path_buf(),
dir.path().join("c").to_path_buf(),
],
&[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
&app_state,
cx,
)
@ -383,11 +375,8 @@ mod tests {
#[gpui::test]
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
let app_state = cx.update(test_app_state);
cx.update(|cx| {
workspace::init(&app_state.client, cx);
});
cx.dispatch_global_action(workspace::OpenNew(app_state.clone()));
let app_state = init(cx);
cx.dispatch_global_action(workspace::OpenNew);
let window_id = *cx.window_ids().first().unwrap();
let workspace = cx.root_view::<Workspace>(window_id).unwrap();
let editor = workspace.update(cx, |workspace, cx| {
@ -414,7 +403,7 @@ mod tests {
#[gpui::test]
async fn test_open_entry(cx: &mut TestAppContext) {
let app_state = cx.update(test_app_state);
let app_state = init(cx);
app_state
.fs
.as_fake()
@ -429,18 +418,10 @@ mod tests {
}),
)
.await;
let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
params
.project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
let entries = cx.read(|cx| workspace.file_project_paths(cx));
let file1 = entries[0].clone();
let file2 = entries[1].clone();
@ -535,7 +516,8 @@ mod tests {
#[gpui::test]
async fn test_open_paths(cx: &mut TestAppContext) {
let app_state = cx.update(test_app_state);
let app_state = init(cx);
let fs = app_state.fs.as_fake();
fs.insert_dir("/dir1").await;
fs.insert_dir("/dir2").await;
@ -544,17 +526,8 @@ mod tests {
fs.insert_file("/dir2/b.txt", "".into()).await;
fs.insert_file("/dir3/c.txt", "".into()).await;
let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
params
.project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/dir1", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
// Open a file within an existing worktree.
cx.update(|cx| {
@ -655,19 +628,15 @@ mod tests {
#[gpui::test]
async fn test_save_conflicting_item(cx: &mut TestAppContext) {
let app_state = cx.update(test_app_state);
let fs = app_state.fs.as_fake();
fs.insert_tree("/root", json!({ "a.txt": "" })).await;
let app_state = init(cx);
app_state
.fs
.as_fake()
.insert_tree("/root", json!({ "a.txt": "" }))
.await;
let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
params
.project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await
.unwrap();
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
// Open a file within an existing worktree.
cx.update(|cx| {
@ -687,7 +656,11 @@ mod tests {
editor.handle_input(&editor::Input("x".into()), cx)
})
});
fs.insert_file("/root/a.txt", "changed".to_string()).await;
app_state
.fs
.as_fake()
.insert_file("/root/a.txt", "changed".to_string())
.await;
editor
.condition(&cx, |editor, cx| editor.has_conflict(cx))
.await;
@ -704,21 +677,16 @@ mod tests {
#[gpui::test]
async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
let app_state = cx.update(test_app_state);
let app_state = init(cx);
app_state.fs.as_fake().insert_dir("/root").await;
let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
params
.project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await
.unwrap();
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(rust_lang()));
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
// Create a new untitled buffer
cx.dispatch_action(window_id, OpenNew(app_state.clone()));
cx.dispatch_action(window_id, OpenNew);
let editor = workspace.read_with(cx, |workspace, cx| {
workspace
.active_item(cx)
@ -773,18 +741,11 @@ mod tests {
// Open the same newly-created file in another pane item. The new editor should reuse
// the same buffer.
cx.dispatch_action(window_id, OpenNew(app_state.clone()));
cx.dispatch_action(window_id, OpenNew);
workspace
.update(cx, |workspace, cx| {
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
workspace.open_path(
ProjectPath {
worktree_id: worktree.read(cx).id(),
path: Path::new("the-new-name.rs").into(),
},
true,
cx,
)
workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), true, cx)
})
.await
.unwrap();
@ -805,13 +766,15 @@ mod tests {
#[gpui::test]
async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
let app_state = cx.update(test_app_state);
let app_state = init(cx);
app_state.fs.as_fake().insert_dir("/root").await;
let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
let project = Project::test(app_state.fs.clone(), [], cx).await;
project.update(cx, |project, _| project.languages().add(rust_lang()));
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
// Create a new untitled buffer
cx.dispatch_action(window_id, OpenNew(app_state.clone()));
cx.dispatch_action(window_id, OpenNew);
let editor = workspace.read_with(cx, |workspace, cx| {
workspace
.active_item(cx)
@ -842,10 +805,9 @@ mod tests {
#[gpui::test]
async fn test_pane_actions(cx: &mut TestAppContext) {
cx.foreground().forbid_parking();
init(cx);
cx.update(|cx| pane::init(cx));
let app_state = cx.update(test_app_state);
let app_state = cx.update(AppState::test);
app_state
.fs
.as_fake()
@ -861,17 +823,9 @@ mod tests {
)
.await;
let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
params
.project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
let entries = cx.read(|cx| workspace.file_project_paths(cx));
let file1 = entries[0].clone();
@ -926,7 +880,7 @@ mod tests {
#[gpui::test]
async fn test_navigation(cx: &mut TestAppContext) {
let app_state = cx.update(test_app_state);
let app_state = init(cx);
app_state
.fs
.as_fake()
@ -941,17 +895,10 @@ mod tests {
}),
)
.await;
let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
params
.project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
let entries = cx.read(|cx| workspace.file_project_paths(cx));
let file1 = entries[0].clone();
let file2 = entries[1].clone();
@ -990,7 +937,7 @@ mod tests {
editor.newline(&Default::default(), cx);
editor.move_down(&Default::default(), cx);
editor.move_down(&Default::default(), cx);
editor.save(params.project.clone(), cx)
editor.save(project.clone(), cx)
})
.await
.unwrap();
@ -1104,7 +1051,6 @@ mod tests {
.unwrap();
app_state
.fs
.as_fake()
.remove_file(Path::new("/root/a/file2"), Default::default())
.await
.unwrap();
@ -1219,4 +1165,29 @@ mod tests {
}
assert!(has_default_theme);
}
fn init(cx: &mut TestAppContext) -> Arc<AppState> {
cx.foreground().forbid_parking();
cx.update(|cx| {
let mut app_state = AppState::test(cx);
let state = Arc::get_mut(&mut app_state).unwrap();
state.initialize_workspace = initialize_workspace;
state.build_window_options = build_window_options;
workspace::init(app_state.clone(), cx);
editor::init(cx);
pane::init(cx);
app_state
})
}
fn rust_lang() -> Arc<language::Language> {
Arc::new(language::Language::new(
language::LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
))
}
}