1pub mod assets;
2pub mod language;
3pub mod menus;
4#[cfg(any(test, feature = "test-support"))]
5pub mod test;
6
7use self::language::LanguageRegistry;
8use chat_panel::ChatPanel;
9pub use client;
10pub use editor;
11use gpui::{
12 action,
13 geometry::{rect::RectF, vector::vec2f},
14 keymap::Binding,
15 platform::WindowOptions,
16 ModelHandle, MutableAppContext, PathPromptOptions, Task, ViewContext,
17};
18use parking_lot::Mutex;
19pub use people_panel;
20use people_panel::PeoplePanel;
21use postage::watch;
22pub use project::{self, fs};
23use project_panel::ProjectPanel;
24use std::{path::PathBuf, sync::Arc};
25use theme::ThemeRegistry;
26use theme_selector::ThemeSelectorParams;
27pub use workspace;
28use workspace::{Settings, Workspace, WorkspaceParams};
29
30action!(About);
31action!(Open, Arc<AppState>);
32action!(OpenPaths, OpenParams);
33action!(Quit);
34action!(AdjustBufferFontSize, f32);
35
36const MIN_FONT_SIZE: f32 = 6.0;
37
38pub struct AppState {
39 pub settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
40 pub settings: watch::Receiver<Settings>,
41 pub languages: Arc<LanguageRegistry>,
42 pub themes: Arc<ThemeRegistry>,
43 pub client: Arc<client::Client>,
44 pub user_store: ModelHandle<client::UserStore>,
45 pub fs: Arc<dyn fs::Fs>,
46 pub channel_list: ModelHandle<client::ChannelList>,
47}
48
49#[derive(Clone)]
50pub struct OpenParams {
51 pub paths: Vec<PathBuf>,
52 pub app_state: Arc<AppState>,
53}
54
55pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
56 cx.add_global_action(open);
57 cx.add_global_action(|action: &OpenPaths, cx: &mut MutableAppContext| {
58 open_paths(action, cx).detach()
59 });
60 cx.add_global_action(open_new);
61 cx.add_global_action(quit);
62
63 cx.add_global_action({
64 let settings_tx = app_state.settings_tx.clone();
65
66 move |action: &AdjustBufferFontSize, cx| {
67 let mut settings_tx = settings_tx.lock();
68 let new_size = (settings_tx.borrow().buffer_font_size + action.0).max(MIN_FONT_SIZE);
69 settings_tx.borrow_mut().buffer_font_size = new_size;
70 cx.refresh_windows();
71 }
72 });
73
74 cx.add_bindings(vec![
75 Binding::new("cmd-=", AdjustBufferFontSize(1.), None),
76 Binding::new("cmd--", AdjustBufferFontSize(-1.), None),
77 ])
78}
79
80fn open(action: &Open, cx: &mut MutableAppContext) {
81 let app_state = action.0.clone();
82 cx.prompt_for_paths(
83 PathPromptOptions {
84 files: true,
85 directories: true,
86 multiple: true,
87 },
88 move |paths, cx| {
89 if let Some(paths) = paths {
90 cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state }));
91 }
92 },
93 );
94}
95
96fn open_paths(action: &OpenPaths, cx: &mut MutableAppContext) -> Task<()> {
97 log::info!("open paths {:?}", action.0.paths);
98
99 // Open paths in existing workspace if possible
100 for window_id in cx.window_ids().collect::<Vec<_>>() {
101 if let Some(handle) = cx.root_view::<Workspace>(window_id) {
102 let task = handle.update(cx, |view, cx| {
103 if view.contains_paths(&action.0.paths, cx.as_ref()) {
104 log::info!("open paths on existing workspace");
105 Some(view.open_paths(&action.0.paths, cx))
106 } else {
107 None
108 }
109 });
110
111 if let Some(task) = task {
112 return task;
113 }
114 }
115 }
116
117 log::info!("open new workspace");
118
119 // Add a new workspace if necessary
120 let app_state = &action.0.app_state;
121 let (_, workspace) = cx.add_window(window_options(), |cx| {
122 build_workspace(&WorkspaceParams::from(app_state.as_ref()), cx)
123 });
124 workspace.update(cx, |workspace, cx| {
125 workspace.open_paths(&action.0.paths, cx)
126 })
127}
128
129fn open_new(action: &workspace::OpenNew, cx: &mut MutableAppContext) {
130 cx.add_window(window_options(), |cx| {
131 let mut workspace = build_workspace(&action.0, cx);
132 workspace.open_new_file(&action, cx);
133 workspace
134 });
135}
136
137fn build_workspace(params: &WorkspaceParams, cx: &mut ViewContext<Workspace>) -> Workspace {
138 let mut workspace = Workspace::new(params, cx);
139 let project = workspace.project().clone();
140 workspace.left_sidebar_mut().add_item(
141 "icons/folder-tree-16.svg",
142 ProjectPanel::new(project, params.settings.clone(), cx).into(),
143 );
144 workspace.right_sidebar_mut().add_item(
145 "icons/user-16.svg",
146 cx.add_view(|cx| PeoplePanel::new(params.user_store.clone(), params.settings.clone(), cx))
147 .into(),
148 );
149 workspace.right_sidebar_mut().add_item(
150 "icons/comment-16.svg",
151 cx.add_view(|cx| {
152 ChatPanel::new(
153 params.client.clone(),
154 params.channel_list.clone(),
155 params.settings.clone(),
156 cx,
157 )
158 })
159 .into(),
160 );
161 workspace
162}
163
164fn window_options() -> WindowOptions<'static> {
165 WindowOptions {
166 bounds: RectF::new(vec2f(0., 0.), vec2f(1024., 768.)),
167 title: None,
168 titlebar_appears_transparent: true,
169 traffic_light_position: Some(vec2f(8., 8.)),
170 }
171}
172
173fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
174 cx.platform().quit();
175}
176
177impl<'a> From<&'a AppState> for WorkspaceParams {
178 fn from(state: &'a AppState) -> Self {
179 Self {
180 client: state.client.clone(),
181 fs: state.fs.clone(),
182 languages: state.languages.clone(),
183 settings: state.settings.clone(),
184 user_store: state.user_store.clone(),
185 channel_list: state.channel_list.clone(),
186 }
187 }
188}
189
190impl<'a> From<&'a AppState> for ThemeSelectorParams {
191 fn from(state: &'a AppState) -> Self {
192 Self {
193 settings_tx: state.settings_tx.clone(),
194 settings: state.settings.clone(),
195 themes: state.themes.clone(),
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use serde_json::json;
204 use test::test_app_state;
205 use theme::DEFAULT_THEME_NAME;
206 use util::test::temp_tree;
207 use workspace::ItemView;
208
209 #[gpui::test]
210 async fn test_open_paths_action(mut cx: gpui::TestAppContext) {
211 let app_state = cx.update(test_app_state);
212 let dir = temp_tree(json!({
213 "a": {
214 "aa": null,
215 "ab": null,
216 },
217 "b": {
218 "ba": null,
219 "bb": null,
220 },
221 "c": {
222 "ca": null,
223 "cb": null,
224 },
225 }));
226
227 cx.update(|cx| {
228 open_paths(
229 &OpenPaths(OpenParams {
230 paths: vec![
231 dir.path().join("a").to_path_buf(),
232 dir.path().join("b").to_path_buf(),
233 ],
234 app_state: app_state.clone(),
235 }),
236 cx,
237 )
238 })
239 .await;
240 assert_eq!(cx.window_ids().len(), 1);
241
242 cx.update(|cx| {
243 open_paths(
244 &OpenPaths(OpenParams {
245 paths: vec![dir.path().join("a").to_path_buf()],
246 app_state: app_state.clone(),
247 }),
248 cx,
249 )
250 })
251 .await;
252 assert_eq!(cx.window_ids().len(), 1);
253 let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
254 workspace_1.read_with(&cx, |workspace, cx| {
255 assert_eq!(workspace.worktrees(cx).len(), 2)
256 });
257
258 cx.update(|cx| {
259 open_paths(
260 &OpenPaths(OpenParams {
261 paths: vec![
262 dir.path().join("b").to_path_buf(),
263 dir.path().join("c").to_path_buf(),
264 ],
265 app_state: app_state.clone(),
266 }),
267 cx,
268 )
269 })
270 .await;
271 assert_eq!(cx.window_ids().len(), 2);
272 }
273
274 #[gpui::test]
275 async fn test_new_empty_workspace(mut cx: gpui::TestAppContext) {
276 let app_state = cx.update(test_app_state);
277 cx.update(|cx| init(&app_state, cx));
278 cx.dispatch_global_action(workspace::OpenNew(app_state.as_ref().into()));
279 let window_id = *cx.window_ids().first().unwrap();
280 let workspace = cx.root_view::<Workspace>(window_id).unwrap();
281 let editor = workspace.update(&mut cx, |workspace, cx| {
282 workspace
283 .active_item(cx)
284 .unwrap()
285 .to_any()
286 .downcast::<editor::Editor>()
287 .unwrap()
288 });
289
290 editor.update(&mut cx, |editor, cx| {
291 assert!(editor.text(cx).is_empty());
292 });
293
294 workspace.update(&mut cx, |workspace, cx| {
295 workspace.save_active_item(&workspace::Save, cx)
296 });
297
298 app_state.fs.as_fake().insert_dir("/root").await.unwrap();
299 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
300
301 editor
302 .condition(&cx, |editor, cx| editor.title(cx) == "the-new-name")
303 .await;
304 editor.update(&mut cx, |editor, cx| {
305 assert!(!editor.is_dirty(cx));
306 });
307 }
308
309 #[gpui::test]
310 fn test_bundled_themes(cx: &mut MutableAppContext) {
311 let app_state = test_app_state(cx);
312 let mut has_default_theme = false;
313 for theme_name in app_state.themes.list() {
314 let theme = app_state.themes.get(&theme_name).unwrap();
315 if theme.name == DEFAULT_THEME_NAME {
316 has_default_theme = true;
317 }
318 assert_eq!(theme.name, theme_name);
319 }
320 assert!(has_default_theme);
321 }
322}