lib.rs

  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}