1mod feedback;
2pub mod languages;
3pub mod menus;
4pub mod paths;
5pub mod settings_file;
6#[cfg(any(test, feature = "test-support"))]
7pub mod test;
8
9use anyhow::{anyhow, Context, Result};
10use assets::Assets;
11use breadcrumbs::Breadcrumbs;
12pub use client;
13use collections::VecDeque;
14pub use contacts_panel;
15use contacts_panel::ContactsPanel;
16pub use editor;
17use editor::{Editor, MultiBuffer};
18use gpui::{
19 actions,
20 geometry::vector::vec2f,
21 impl_actions,
22 platform::{WindowBounds, WindowOptions},
23 AssetSource, AsyncAppContext, ViewContext,
24};
25use language::Rope;
26pub use lsp;
27pub use project::{self, fs};
28use project_panel::ProjectPanel;
29use search::{BufferSearchBar, ProjectSearchBar};
30use serde::Deserialize;
31use serde_json::to_string_pretty;
32use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
33use std::{env, path::Path, str, sync::Arc};
34use util::ResultExt;
35pub use workspace;
36use workspace::{sidebar::Side, AppState, Workspace};
37
38#[derive(Deserialize, Clone, PartialEq)]
39struct OpenBrowser {
40 url: Arc<str>,
41}
42
43impl_actions!(zed, [OpenBrowser]);
44
45actions!(
46 zed,
47 [
48 About,
49 Hide,
50 HideOthers,
51 ShowAll,
52 Minimize,
53 Zoom,
54 ToggleFullScreen,
55 Quit,
56 DebugElements,
57 OpenSettings,
58 OpenLog,
59 OpenKeymap,
60 OpenDefaultSettings,
61 OpenDefaultKeymap,
62 IncreaseBufferFontSize,
63 DecreaseBufferFontSize,
64 ResetBufferFontSize,
65 InstallCommandLineInterface,
66 ]
67);
68
69const MIN_FONT_SIZE: f32 = 6.0;
70
71pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
72 cx.add_action(about);
73 cx.add_global_action(|_: &Hide, cx: &mut gpui::MutableAppContext| {
74 cx.platform().hide();
75 });
76 cx.add_global_action(|_: &HideOthers, cx: &mut gpui::MutableAppContext| {
77 cx.platform().hide_other_apps();
78 });
79 cx.add_global_action(|_: &ShowAll, cx: &mut gpui::MutableAppContext| {
80 cx.platform().unhide_other_apps();
81 });
82 cx.add_action(
83 |_: &mut Workspace, _: &Minimize, cx: &mut ViewContext<Workspace>| {
84 cx.minimize_window();
85 },
86 );
87 cx.add_action(
88 |_: &mut Workspace, _: &Zoom, cx: &mut ViewContext<Workspace>| {
89 cx.zoom_window();
90 },
91 );
92 cx.add_action(
93 |_: &mut Workspace, _: &ToggleFullScreen, cx: &mut ViewContext<Workspace>| {
94 cx.toggle_full_screen();
95 },
96 );
97 cx.add_global_action(quit);
98 cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
99 cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
100 cx.update_global::<Settings, _, _>(|settings, cx| {
101 settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
102 if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() {
103 *terminal_font_size = (*terminal_font_size + 1.0).max(MIN_FONT_SIZE);
104 }
105 cx.refresh_windows();
106 });
107 });
108 cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
109 cx.update_global::<Settings, _, _>(|settings, cx| {
110 settings.buffer_font_size = (settings.buffer_font_size - 1.0).max(MIN_FONT_SIZE);
111 if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() {
112 *terminal_font_size = (*terminal_font_size - 1.0).max(MIN_FONT_SIZE);
113 }
114 cx.refresh_windows();
115 });
116 });
117 cx.add_global_action(move |_: &ResetBufferFontSize, cx| {
118 cx.update_global::<Settings, _, _>(|settings, cx| {
119 settings.buffer_font_size = settings.default_buffer_font_size;
120 settings.terminal_overrides.font_size = settings.terminal_defaults.font_size;
121 cx.refresh_windows();
122 });
123 });
124 cx.add_global_action(move |_: &InstallCommandLineInterface, cx| {
125 cx.spawn(|cx| async move { install_cli(&cx).await.context("error creating CLI symlink") })
126 .detach_and_log_err(cx);
127 });
128 cx.add_action({
129 let app_state = app_state.clone();
130 move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
131 open_config_file(&paths::SETTINGS, app_state.clone(), cx, || {
132 str::from_utf8(
133 Assets
134 .load("settings/initial_user_settings.json")
135 .unwrap()
136 .as_ref(),
137 )
138 .unwrap()
139 .into()
140 });
141 }
142 });
143 cx.add_action({
144 let app_state = app_state.clone();
145 move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext<Workspace>| {
146 open_log_file(workspace, app_state.clone(), cx);
147 }
148 });
149 cx.add_action({
150 let app_state = app_state.clone();
151 move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
152 open_config_file(&paths::KEYMAP, app_state.clone(), cx, Default::default);
153 }
154 });
155 cx.add_action({
156 let app_state = app_state.clone();
157 move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
158 open_bundled_config_file(
159 workspace,
160 app_state.clone(),
161 "keymaps/default.json",
162 "Default Key Bindings",
163 cx,
164 );
165 }
166 });
167 cx.add_action({
168 let app_state = app_state.clone();
169 move |workspace: &mut Workspace,
170 _: &OpenDefaultSettings,
171 cx: &mut ViewContext<Workspace>| {
172 open_bundled_config_file(
173 workspace,
174 app_state.clone(),
175 "settings/default.json",
176 "Default Settings",
177 cx,
178 );
179 }
180 });
181 cx.add_action(
182 |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
183 let content = to_string_pretty(&cx.debug_elements()).unwrap();
184 let project = workspace.project().clone();
185 let json_language = project.read(cx).languages().get_language("JSON").unwrap();
186 if project.read(cx).is_remote() {
187 cx.propagate_action();
188 } else if let Some(buffer) = project
189 .update(cx, |project, cx| {
190 project.create_buffer(&content, Some(json_language), cx)
191 })
192 .log_err()
193 {
194 workspace.add_item(
195 Box::new(
196 cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
197 ),
198 cx,
199 );
200 }
201 },
202 );
203 cx.add_action(
204 |workspace: &mut Workspace,
205 _: &project_panel::ToggleFocus,
206 cx: &mut ViewContext<Workspace>| {
207 workspace.toggle_sidebar_item_focus(Side::Left, 0, cx);
208 },
209 );
210 cx.add_action(
211 |workspace: &mut Workspace,
212 _: &contacts_panel::ToggleFocus,
213 cx: &mut ViewContext<Workspace>| {
214 workspace.toggle_sidebar_item_focus(Side::Right, 0, cx);
215 },
216 );
217
218 activity_indicator::init(cx);
219 settings::KeymapFileContent::load_defaults(cx);
220}
221
222pub fn initialize_workspace(
223 workspace: &mut Workspace,
224 app_state: &Arc<AppState>,
225 cx: &mut ViewContext<Workspace>,
226) {
227 cx.subscribe(&cx.handle(), {
228 move |_, _, event, cx| {
229 if let workspace::Event::PaneAdded(pane) = event {
230 pane.update(cx, |pane, cx| {
231 pane.toolbar().update(cx, |toolbar, cx| {
232 let breadcrumbs = cx.add_view(|_| Breadcrumbs::new());
233 toolbar.add_item(breadcrumbs, cx);
234 let buffer_search_bar = cx.add_view(BufferSearchBar::new);
235 toolbar.add_item(buffer_search_bar, cx);
236 let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
237 toolbar.add_item(project_search_bar, cx);
238 })
239 });
240 }
241 }
242 })
243 .detach();
244
245 cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
246
247 let theme_names = app_state.themes.list().collect();
248 let language_names = &languages::LANGUAGE_NAMES;
249
250 workspace.project().update(cx, |project, cx| {
251 let action_names = cx.all_action_names().collect::<Vec<_>>();
252 project.set_language_server_settings(serde_json::json!({
253 "json": {
254 "format": {
255 "enable": true,
256 },
257 "schemas": [
258 {
259 "fileMatch": [schema_file_match(&*paths::SETTINGS)],
260 "schema": settings_file_json_schema(theme_names, language_names),
261 },
262 {
263 "fileMatch": [schema_file_match(&*paths::KEYMAP)],
264 "schema": keymap_file_json_schema(&action_names),
265 }
266 ]
267 }
268 }));
269 });
270
271 let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
272 let contact_panel = cx.add_view(|cx| {
273 ContactsPanel::new(
274 app_state.user_store.clone(),
275 app_state.project_store.clone(),
276 workspace.weak_handle(),
277 cx,
278 )
279 });
280
281 workspace.left_sidebar().update(cx, |sidebar, cx| {
282 sidebar.add_item(
283 "icons/folder_tree_16.svg",
284 "Project Panel".to_string(),
285 project_panel,
286 cx,
287 )
288 });
289 workspace.right_sidebar().update(cx, |sidebar, cx| {
290 sidebar.add_item(
291 "icons/user_group_16.svg",
292 "Contacts Panel".to_string(),
293 contact_panel,
294 cx,
295 )
296 });
297
298 let diagnostic_summary =
299 cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
300 let activity_indicator =
301 activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
302 let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
303 let feedback_link = cx.add_view(|_| feedback::FeedbackLink);
304 workspace.status_bar().update(cx, |status_bar, cx| {
305 status_bar.add_left_item(diagnostic_summary, cx);
306 status_bar.add_left_item(activity_indicator, cx);
307 status_bar.add_right_item(cursor_position, cx);
308 status_bar.add_right_item(feedback_link, cx);
309 });
310
311 auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
312
313 cx.on_window_should_close(|workspace, cx| {
314 if let Some(task) = workspace.close(&Default::default(), cx) {
315 task.detach_and_log_err(cx);
316 }
317 false
318 });
319}
320
321pub fn build_window_options() -> WindowOptions<'static> {
322 WindowOptions {
323 bounds: WindowBounds::Maximized,
324 title: None,
325 titlebar_appears_transparent: true,
326 traffic_light_position: Some(vec2f(8., 8.)),
327 }
328}
329
330fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
331 let mut workspaces = cx
332 .window_ids()
333 .filter_map(|window_id| cx.root_view::<Workspace>(window_id))
334 .collect::<Vec<_>>();
335
336 // If multiple windows have unsaved changes, and need a save prompt,
337 // prompt in the active window before switching to a different window.
338 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
339
340 cx.spawn(|mut cx| async move {
341 // If the user cancels any save prompt, then keep the app open.
342 for workspace in workspaces {
343 if !workspace
344 .update(&mut cx, |workspace, cx| workspace.prepare_to_close(cx))
345 .await?
346 {
347 return Ok(());
348 }
349 }
350 cx.platform().quit();
351 anyhow::Ok(())
352 })
353 .detach_and_log_err(cx);
354}
355
356fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
357 cx.prompt(
358 gpui::PromptLevel::Info,
359 &format!("Zed {}", env!("CARGO_PKG_VERSION")),
360 &["OK"],
361 );
362}
363
364async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
365 let cli_path = cx.platform().path_for_auxiliary_executable("cli")?;
366 let link_path = Path::new("/usr/local/bin/zed");
367 let bin_dir_path = link_path.parent().unwrap();
368
369 // Don't re-create symlink if it points to the same CLI binary.
370 if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
371 return Ok(());
372 }
373
374 // If the symlink is not there or is outdated, first try replacing it
375 // without escalating.
376 smol::fs::remove_file(link_path).await.log_err();
377 if smol::fs::unix::symlink(&cli_path, link_path)
378 .await
379 .log_err()
380 .is_some()
381 {
382 return Ok(());
383 }
384
385 // The symlink could not be created, so use osascript with admin privileges
386 // to create it.
387 let status = smol::process::Command::new("osascript")
388 .args([
389 "-e",
390 &format!(
391 "do shell script \" \
392 mkdir -p \'{}\' && \
393 ln -sf \'{}\' \'{}\' \
394 \" with administrator privileges",
395 bin_dir_path.to_string_lossy(),
396 cli_path.to_string_lossy(),
397 link_path.to_string_lossy(),
398 ),
399 ])
400 .stdout(smol::process::Stdio::inherit())
401 .stderr(smol::process::Stdio::inherit())
402 .output()
403 .await?
404 .status;
405 if status.success() {
406 Ok(())
407 } else {
408 Err(anyhow!("error running osascript"))
409 }
410}
411
412fn open_config_file(
413 path: &'static Path,
414 app_state: Arc<AppState>,
415 cx: &mut ViewContext<Workspace>,
416 default_content: impl 'static + Send + FnOnce() -> Rope,
417) {
418 cx.spawn(|workspace, mut cx| async move {
419 let fs = &app_state.fs;
420 if !fs.is_file(path).await {
421 fs.create_file(path, Default::default()).await?;
422 fs.save(path, &default_content(), Default::default())
423 .await?;
424 }
425
426 workspace
427 .update(&mut cx, |workspace, cx| {
428 workspace.with_local_workspace(cx, app_state, |workspace, cx| {
429 workspace.open_paths(vec![path.to_path_buf()], false, cx)
430 })
431 })
432 .await;
433 Ok::<_, anyhow::Error>(())
434 })
435 .detach_and_log_err(cx)
436}
437
438fn open_log_file(
439 workspace: &mut Workspace,
440 app_state: Arc<AppState>,
441 cx: &mut ViewContext<Workspace>,
442) {
443 const MAX_LINES: usize = 1000;
444
445 workspace.with_local_workspace(cx, app_state.clone(), |_, cx| {
446 cx.spawn_weak(|workspace, mut cx| async move {
447 let (old_log, new_log) = futures::join!(
448 app_state.fs.load(&paths::OLD_LOG),
449 app_state.fs.load(&paths::LOG)
450 );
451
452 if let Some(workspace) = workspace.upgrade(&cx) {
453 let mut lines = VecDeque::with_capacity(MAX_LINES);
454 for line in old_log
455 .iter()
456 .flat_map(|log| log.lines())
457 .chain(new_log.iter().flat_map(|log| log.lines()))
458 {
459 if lines.len() == MAX_LINES {
460 lines.pop_front();
461 }
462 lines.push_back(line);
463 }
464 let log = lines
465 .into_iter()
466 .flat_map(|line| [line, "\n"])
467 .collect::<String>();
468
469 workspace.update(&mut cx, |workspace, cx| {
470 let project = workspace.project().clone();
471 let buffer = project
472 .update(cx, |project, cx| project.create_buffer("", None, cx))
473 .expect("creating buffers on a local workspace always succeeds");
474 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx));
475
476 let buffer = cx.add_model(|cx| {
477 MultiBuffer::singleton(buffer, cx).with_title("Log".into())
478 });
479 workspace.add_item(
480 Box::new(
481 cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx)),
482 ),
483 cx,
484 );
485 });
486 }
487 })
488 .detach();
489 });
490}
491
492fn open_bundled_config_file(
493 workspace: &mut Workspace,
494 app_state: Arc<AppState>,
495 asset_path: &'static str,
496 title: &str,
497 cx: &mut ViewContext<Workspace>,
498) {
499 workspace.with_local_workspace(cx, app_state, |workspace, cx| {
500 let project = workspace.project().clone();
501 let buffer = project.update(cx, |project, cx| {
502 let text = Assets::get(asset_path).unwrap().data;
503 let text = str::from_utf8(text.as_ref()).unwrap();
504 project
505 .create_buffer(text, project.languages().get_language("JSON"), cx)
506 .expect("creating buffers on a local workspace always succeeds")
507 });
508 let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
509 workspace.add_item(
510 Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), cx))),
511 cx,
512 );
513 });
514}
515
516fn schema_file_match(path: &Path) -> &Path {
517 path.strip_prefix(path.parent().unwrap().parent().unwrap())
518 .unwrap()
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524 use assets::Assets;
525 use editor::{Autoscroll, DisplayPoint, Editor};
526 use gpui::{
527 executor::Deterministic, AssetSource, MutableAppContext, TestAppContext, ViewHandle,
528 };
529 use project::{Project, ProjectPath};
530 use serde_json::json;
531 use std::{
532 collections::HashSet,
533 path::{Path, PathBuf},
534 };
535 use theme::ThemeRegistry;
536 use workspace::{
537 open_paths, pane, Item, ItemHandle, NewFile, Pane, SplitDirection, WorkspaceHandle,
538 };
539
540 #[gpui::test]
541 async fn test_open_paths_action(cx: &mut TestAppContext) {
542 let app_state = init(cx);
543 app_state
544 .fs
545 .as_fake()
546 .insert_tree(
547 "/root",
548 json!({
549 "a": {
550 "aa": null,
551 "ab": null,
552 },
553 "b": {
554 "ba": null,
555 "bb": null,
556 },
557 "c": {
558 "ca": null,
559 "cb": null,
560 },
561 }),
562 )
563 .await;
564
565 cx.update(|cx| {
566 open_paths(
567 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
568 &app_state,
569 cx,
570 )
571 })
572 .await;
573 assert_eq!(cx.window_ids().len(), 1);
574
575 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
576 .await;
577 assert_eq!(cx.window_ids().len(), 1);
578 let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
579 workspace_1.update(cx, |workspace, cx| {
580 assert_eq!(workspace.worktrees(cx).count(), 2);
581 assert!(workspace.left_sidebar().read(cx).is_open());
582 assert!(workspace.active_pane().is_focused(cx));
583 });
584
585 cx.update(|cx| {
586 open_paths(
587 &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
588 &app_state,
589 cx,
590 )
591 })
592 .await;
593 assert_eq!(cx.window_ids().len(), 2);
594 }
595
596 #[gpui::test]
597 async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
598 let app_state = init(cx);
599 app_state
600 .fs
601 .as_fake()
602 .insert_tree("/root", json!({"a": "hey"}))
603 .await;
604
605 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
606 .await;
607 assert_eq!(cx.window_ids().len(), 1);
608
609 // When opening the workspace, the window is not in a edited state.
610 let workspace = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
611 let editor = workspace.read_with(cx, |workspace, cx| {
612 workspace
613 .active_item(cx)
614 .unwrap()
615 .downcast::<Editor>()
616 .unwrap()
617 });
618 assert!(!cx.is_window_edited(workspace.window_id()));
619
620 // Editing a buffer marks the window as edited.
621 editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
622 assert!(cx.is_window_edited(workspace.window_id()));
623
624 // Undoing the edit restores the window's edited state.
625 editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
626 assert!(!cx.is_window_edited(workspace.window_id()));
627
628 // Redoing the edit marks the window as edited again.
629 editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
630 assert!(cx.is_window_edited(workspace.window_id()));
631
632 // Closing the item restores the window's edited state.
633 let close = workspace.update(cx, |workspace, cx| {
634 drop(editor);
635 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
636 });
637 executor.run_until_parked();
638 cx.simulate_prompt_answer(workspace.window_id(), 1);
639 close.await.unwrap();
640 assert!(!cx.is_window_edited(workspace.window_id()));
641
642 // Opening the buffer again doesn't impact the window's edited state.
643 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
644 .await;
645 let editor = workspace.read_with(cx, |workspace, cx| {
646 workspace
647 .active_item(cx)
648 .unwrap()
649 .downcast::<Editor>()
650 .unwrap()
651 });
652 assert!(!cx.is_window_edited(workspace.window_id()));
653
654 // Editing the buffer marks the window as edited.
655 editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
656 assert!(cx.is_window_edited(workspace.window_id()));
657
658 // Ensure closing the window via the mouse gets preempted due to the
659 // buffer having unsaved changes.
660 assert!(!cx.simulate_window_close(workspace.window_id()));
661 executor.run_until_parked();
662 assert_eq!(cx.window_ids().len(), 1);
663
664 // The window is successfully closed after the user dismisses the prompt.
665 cx.simulate_prompt_answer(workspace.window_id(), 1);
666 executor.run_until_parked();
667 assert_eq!(cx.window_ids().len(), 0);
668 }
669
670 #[gpui::test]
671 async fn test_new_empty_workspace(cx: &mut TestAppContext) {
672 let app_state = init(cx);
673 cx.dispatch_global_action(workspace::NewFile);
674 let window_id = *cx.window_ids().first().unwrap();
675 let workspace = cx.root_view::<Workspace>(window_id).unwrap();
676 let editor = workspace.update(cx, |workspace, cx| {
677 workspace
678 .active_item(cx)
679 .unwrap()
680 .downcast::<editor::Editor>()
681 .unwrap()
682 });
683
684 editor.update(cx, |editor, cx| {
685 assert!(editor.text(cx).is_empty());
686 });
687
688 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
689 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
690 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
691 save_task.await.unwrap();
692 editor.read_with(cx, |editor, cx| {
693 assert!(!editor.is_dirty(cx));
694 assert_eq!(editor.title(cx), "the-new-name");
695 });
696 }
697
698 #[gpui::test]
699 async fn test_open_entry(cx: &mut TestAppContext) {
700 let app_state = init(cx);
701 app_state
702 .fs
703 .as_fake()
704 .insert_tree(
705 "/root",
706 json!({
707 "a": {
708 "file1": "contents 1",
709 "file2": "contents 2",
710 "file3": "contents 3",
711 },
712 }),
713 )
714 .await;
715
716 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
717 let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
718
719 let entries = cx.read(|cx| workspace.file_project_paths(cx));
720 let file1 = entries[0].clone();
721 let file2 = entries[1].clone();
722 let file3 = entries[2].clone();
723
724 // Open the first entry
725 let entry_1 = workspace
726 .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
727 .await
728 .unwrap();
729 cx.read(|cx| {
730 let pane = workspace.read(cx).active_pane().read(cx);
731 assert_eq!(
732 pane.active_item().unwrap().project_path(cx),
733 Some(file1.clone())
734 );
735 assert_eq!(pane.items().count(), 1);
736 });
737
738 // Open the second entry
739 workspace
740 .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
741 .await
742 .unwrap();
743 cx.read(|cx| {
744 let pane = workspace.read(cx).active_pane().read(cx);
745 assert_eq!(
746 pane.active_item().unwrap().project_path(cx),
747 Some(file2.clone())
748 );
749 assert_eq!(pane.items().count(), 2);
750 });
751
752 // Open the first entry again. The existing pane item is activated.
753 let entry_1b = workspace
754 .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
755 .await
756 .unwrap();
757 assert_eq!(entry_1.id(), entry_1b.id());
758
759 cx.read(|cx| {
760 let pane = workspace.read(cx).active_pane().read(cx);
761 assert_eq!(
762 pane.active_item().unwrap().project_path(cx),
763 Some(file1.clone())
764 );
765 assert_eq!(pane.items().count(), 2);
766 });
767
768 // Split the pane with the first entry, then open the second entry again.
769 workspace
770 .update(cx, |w, cx| {
771 w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
772 w.open_path(file2.clone(), true, cx)
773 })
774 .await
775 .unwrap();
776
777 workspace.read_with(cx, |w, cx| {
778 assert_eq!(
779 w.active_pane()
780 .read(cx)
781 .active_item()
782 .unwrap()
783 .project_path(cx.as_ref()),
784 Some(file2.clone())
785 );
786 });
787
788 // Open the third entry twice concurrently. Only one pane item is added.
789 let (t1, t2) = workspace.update(cx, |w, cx| {
790 (
791 w.open_path(file3.clone(), true, cx),
792 w.open_path(file3.clone(), true, cx),
793 )
794 });
795 t1.await.unwrap();
796 t2.await.unwrap();
797 cx.read(|cx| {
798 let pane = workspace.read(cx).active_pane().read(cx);
799 assert_eq!(
800 pane.active_item().unwrap().project_path(cx),
801 Some(file3.clone())
802 );
803 let pane_entries = pane
804 .items()
805 .map(|i| i.project_path(cx).unwrap())
806 .collect::<Vec<_>>();
807 assert_eq!(pane_entries, &[file1, file2, file3]);
808 });
809 }
810
811 #[gpui::test]
812 async fn test_open_paths(cx: &mut TestAppContext) {
813 let app_state = init(cx);
814
815 app_state
816 .fs
817 .as_fake()
818 .insert_tree(
819 "/",
820 json!({
821 "dir1": {
822 "a.txt": ""
823 },
824 "dir2": {
825 "b.txt": ""
826 },
827 "dir3": {
828 "c.txt": ""
829 },
830 "d.txt": ""
831 }),
832 )
833 .await;
834
835 let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
836 let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
837
838 // Open a file within an existing worktree.
839 cx.update(|cx| {
840 workspace.update(cx, |view, cx| {
841 view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
842 })
843 })
844 .await;
845 cx.read(|cx| {
846 assert_eq!(
847 workspace
848 .read(cx)
849 .active_pane()
850 .read(cx)
851 .active_item()
852 .unwrap()
853 .to_any()
854 .downcast::<Editor>()
855 .unwrap()
856 .read(cx)
857 .title(cx),
858 "a.txt"
859 );
860 });
861
862 // Open a file outside of any existing worktree.
863 cx.update(|cx| {
864 workspace.update(cx, |view, cx| {
865 view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
866 })
867 })
868 .await;
869 cx.read(|cx| {
870 let worktree_roots = workspace
871 .read(cx)
872 .worktrees(cx)
873 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
874 .collect::<HashSet<_>>();
875 assert_eq!(
876 worktree_roots,
877 vec!["/dir1", "/dir2/b.txt"]
878 .into_iter()
879 .map(Path::new)
880 .collect(),
881 );
882 assert_eq!(
883 workspace
884 .read(cx)
885 .active_pane()
886 .read(cx)
887 .active_item()
888 .unwrap()
889 .to_any()
890 .downcast::<Editor>()
891 .unwrap()
892 .read(cx)
893 .title(cx),
894 "b.txt"
895 );
896 });
897
898 // Ensure opening a directory and one of its children only adds one worktree.
899 cx.update(|cx| {
900 workspace.update(cx, |view, cx| {
901 view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
902 })
903 })
904 .await;
905 cx.read(|cx| {
906 let worktree_roots = workspace
907 .read(cx)
908 .worktrees(cx)
909 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
910 .collect::<HashSet<_>>();
911 assert_eq!(
912 worktree_roots,
913 vec!["/dir1", "/dir2/b.txt", "/dir3"]
914 .into_iter()
915 .map(Path::new)
916 .collect(),
917 );
918 assert_eq!(
919 workspace
920 .read(cx)
921 .active_pane()
922 .read(cx)
923 .active_item()
924 .unwrap()
925 .to_any()
926 .downcast::<Editor>()
927 .unwrap()
928 .read(cx)
929 .title(cx),
930 "c.txt"
931 );
932 });
933
934 // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
935 cx.update(|cx| {
936 workspace.update(cx, |view, cx| {
937 view.open_paths(vec!["/d.txt".into()], false, cx)
938 })
939 })
940 .await;
941 cx.read(|cx| {
942 let worktree_roots = workspace
943 .read(cx)
944 .worktrees(cx)
945 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
946 .collect::<HashSet<_>>();
947 assert_eq!(
948 worktree_roots,
949 vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
950 .into_iter()
951 .map(Path::new)
952 .collect(),
953 );
954
955 let visible_worktree_roots = workspace
956 .read(cx)
957 .visible_worktrees(cx)
958 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
959 .collect::<HashSet<_>>();
960 assert_eq!(
961 visible_worktree_roots,
962 vec!["/dir1", "/dir2/b.txt", "/dir3"]
963 .into_iter()
964 .map(Path::new)
965 .collect(),
966 );
967
968 assert_eq!(
969 workspace
970 .read(cx)
971 .active_pane()
972 .read(cx)
973 .active_item()
974 .unwrap()
975 .to_any()
976 .downcast::<Editor>()
977 .unwrap()
978 .read(cx)
979 .title(cx),
980 "d.txt"
981 );
982 });
983 }
984
985 #[gpui::test]
986 async fn test_save_conflicting_item(cx: &mut TestAppContext) {
987 let app_state = init(cx);
988 app_state
989 .fs
990 .as_fake()
991 .insert_tree("/root", json!({ "a.txt": "" }))
992 .await;
993
994 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
995 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
996
997 // Open a file within an existing worktree.
998 cx.update(|cx| {
999 workspace.update(cx, |view, cx| {
1000 view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
1001 })
1002 })
1003 .await;
1004 let editor = cx.read(|cx| {
1005 let pane = workspace.read(cx).active_pane().read(cx);
1006 let item = pane.active_item().unwrap();
1007 item.downcast::<Editor>().unwrap()
1008 });
1009
1010 cx.update(|cx| editor.update(cx, |editor, cx| editor.handle_input("x", cx)));
1011 app_state
1012 .fs
1013 .as_fake()
1014 .insert_file("/root/a.txt", "changed".to_string())
1015 .await;
1016 editor
1017 .condition(cx, |editor, cx| editor.has_conflict(cx))
1018 .await;
1019 cx.read(|cx| assert!(editor.is_dirty(cx)));
1020
1021 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1022 cx.simulate_prompt_answer(window_id, 0);
1023 save_task.await.unwrap();
1024 editor.read_with(cx, |editor, cx| {
1025 assert!(!editor.is_dirty(cx));
1026 assert!(!editor.has_conflict(cx));
1027 });
1028 }
1029
1030 #[gpui::test]
1031 async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
1032 let app_state = init(cx);
1033 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1034
1035 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1036 project.update(cx, |project, _| project.languages().add(rust_lang()));
1037 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
1038 let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
1039
1040 // Create a new untitled buffer
1041 cx.dispatch_action(window_id, NewFile);
1042 let editor = workspace.read_with(cx, |workspace, cx| {
1043 workspace
1044 .active_item(cx)
1045 .unwrap()
1046 .downcast::<Editor>()
1047 .unwrap()
1048 });
1049
1050 editor.update(cx, |editor, cx| {
1051 assert!(!editor.is_dirty(cx));
1052 assert_eq!(editor.title(cx), "untitled");
1053 assert!(Arc::ptr_eq(
1054 editor.language_at(0, cx).unwrap(),
1055 &languages::PLAIN_TEXT
1056 ));
1057 editor.handle_input("hi", cx);
1058 assert!(editor.is_dirty(cx));
1059 });
1060
1061 // Save the buffer. This prompts for a filename.
1062 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1063 cx.simulate_new_path_selection(|parent_dir| {
1064 assert_eq!(parent_dir, Path::new("/root"));
1065 Some(parent_dir.join("the-new-name.rs"))
1066 });
1067 cx.read(|cx| {
1068 assert!(editor.is_dirty(cx));
1069 assert_eq!(editor.read(cx).title(cx), "untitled");
1070 });
1071
1072 // When the save completes, the buffer's title is updated and the language is assigned based
1073 // on the path.
1074 save_task.await.unwrap();
1075 editor.read_with(cx, |editor, cx| {
1076 assert!(!editor.is_dirty(cx));
1077 assert_eq!(editor.title(cx), "the-new-name.rs");
1078 assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
1079 });
1080
1081 // Edit the file and save it again. This time, there is no filename prompt.
1082 editor.update(cx, |editor, cx| {
1083 editor.handle_input(" there", cx);
1084 assert!(editor.is_dirty(cx.as_ref()));
1085 });
1086 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1087 save_task.await.unwrap();
1088 assert!(!cx.did_prompt_for_new_path());
1089 editor.read_with(cx, |editor, cx| {
1090 assert!(!editor.is_dirty(cx));
1091 assert_eq!(editor.title(cx), "the-new-name.rs")
1092 });
1093
1094 // Open the same newly-created file in another pane item. The new editor should reuse
1095 // the same buffer.
1096 cx.dispatch_action(window_id, NewFile);
1097 workspace
1098 .update(cx, |workspace, cx| {
1099 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
1100 workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), true, cx)
1101 })
1102 .await
1103 .unwrap();
1104 let editor2 = workspace.update(cx, |workspace, cx| {
1105 workspace
1106 .active_item(cx)
1107 .unwrap()
1108 .downcast::<Editor>()
1109 .unwrap()
1110 });
1111 cx.read(|cx| {
1112 assert_eq!(
1113 editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
1114 editor.read(cx).buffer().read(cx).as_singleton().unwrap()
1115 );
1116 })
1117 }
1118
1119 #[gpui::test]
1120 async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
1121 let app_state = init(cx);
1122 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1123
1124 let project = Project::test(app_state.fs.clone(), [], cx).await;
1125 project.update(cx, |project, _| project.languages().add(rust_lang()));
1126 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
1127
1128 // Create a new untitled buffer
1129 cx.dispatch_action(window_id, NewFile);
1130 let editor = workspace.read_with(cx, |workspace, cx| {
1131 workspace
1132 .active_item(cx)
1133 .unwrap()
1134 .downcast::<Editor>()
1135 .unwrap()
1136 });
1137
1138 editor.update(cx, |editor, cx| {
1139 assert!(Arc::ptr_eq(
1140 editor.language_at(0, cx).unwrap(),
1141 &languages::PLAIN_TEXT
1142 ));
1143 editor.handle_input("hi", cx);
1144 assert!(editor.is_dirty(cx.as_ref()));
1145 });
1146
1147 // Save the buffer. This prompts for a filename.
1148 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1149 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
1150 save_task.await.unwrap();
1151 // The buffer is not dirty anymore and the language is assigned based on the path.
1152 editor.read_with(cx, |editor, cx| {
1153 assert!(!editor.is_dirty(cx));
1154 assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
1155 });
1156 }
1157
1158 #[gpui::test]
1159 async fn test_pane_actions(cx: &mut TestAppContext) {
1160 init(cx);
1161
1162 let app_state = cx.update(AppState::test);
1163 app_state
1164 .fs
1165 .as_fake()
1166 .insert_tree(
1167 "/root",
1168 json!({
1169 "a": {
1170 "file1": "contents 1",
1171 "file2": "contents 2",
1172 "file3": "contents 3",
1173 },
1174 }),
1175 )
1176 .await;
1177
1178 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1179 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
1180
1181 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1182 let file1 = entries[0].clone();
1183
1184 let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
1185
1186 workspace
1187 .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
1188 .await
1189 .unwrap();
1190
1191 let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
1192 let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
1193 assert_eq!(editor.project_path(cx), Some(file1.clone()));
1194 let buffer = editor.update(cx, |editor, cx| {
1195 editor.insert("dirt", cx);
1196 editor.buffer().downgrade()
1197 });
1198 (editor.downgrade(), buffer)
1199 });
1200
1201 cx.dispatch_action(window_id, pane::SplitRight);
1202 let editor_2 = cx.update(|cx| {
1203 let pane_2 = workspace.read(cx).active_pane().clone();
1204 assert_ne!(pane_1, pane_2);
1205
1206 let pane2_item = pane_2.read(cx).active_item().unwrap();
1207 assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone()));
1208
1209 pane2_item.downcast::<Editor>().unwrap().downgrade()
1210 });
1211 cx.dispatch_action(window_id, workspace::CloseActiveItem);
1212
1213 cx.foreground().run_until_parked();
1214 workspace.read_with(cx, |workspace, _| {
1215 assert_eq!(workspace.panes().len(), 1);
1216 assert_eq!(workspace.active_pane(), &pane_1);
1217 });
1218
1219 cx.dispatch_action(window_id, workspace::CloseActiveItem);
1220 cx.foreground().run_until_parked();
1221 cx.simulate_prompt_answer(window_id, 1);
1222 cx.foreground().run_until_parked();
1223
1224 workspace.read_with(cx, |workspace, cx| {
1225 assert!(workspace.active_item(cx).is_none());
1226 });
1227
1228 cx.assert_dropped(editor_1);
1229 cx.assert_dropped(editor_2);
1230 cx.assert_dropped(buffer);
1231 }
1232
1233 #[gpui::test]
1234 async fn test_navigation(cx: &mut TestAppContext) {
1235 let app_state = init(cx);
1236 app_state
1237 .fs
1238 .as_fake()
1239 .insert_tree(
1240 "/root",
1241 json!({
1242 "a": {
1243 "file1": "contents 1\n".repeat(20),
1244 "file2": "contents 2\n".repeat(20),
1245 "file3": "contents 3\n".repeat(20),
1246 },
1247 }),
1248 )
1249 .await;
1250
1251 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1252 let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
1253
1254 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1255 let file1 = entries[0].clone();
1256 let file2 = entries[1].clone();
1257 let file3 = entries[2].clone();
1258
1259 let editor1 = workspace
1260 .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
1261 .await
1262 .unwrap()
1263 .downcast::<Editor>()
1264 .unwrap();
1265 editor1.update(cx, |editor, cx| {
1266 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
1267 s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
1268 });
1269 });
1270 let editor2 = workspace
1271 .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
1272 .await
1273 .unwrap()
1274 .downcast::<Editor>()
1275 .unwrap();
1276 let editor3 = workspace
1277 .update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
1278 .await
1279 .unwrap()
1280 .downcast::<Editor>()
1281 .unwrap();
1282
1283 editor3
1284 .update(cx, |editor, cx| {
1285 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
1286 s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
1287 });
1288 editor.newline(&Default::default(), cx);
1289 editor.newline(&Default::default(), cx);
1290 editor.move_down(&Default::default(), cx);
1291 editor.move_down(&Default::default(), cx);
1292 editor.save(project.clone(), cx)
1293 })
1294 .await
1295 .unwrap();
1296 editor3.update(cx, |editor, cx| {
1297 editor.set_scroll_position(vec2f(0., 12.5), cx)
1298 });
1299 assert_eq!(
1300 active_location(&workspace, cx),
1301 (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1302 );
1303
1304 workspace
1305 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1306 .await;
1307 assert_eq!(
1308 active_location(&workspace, cx),
1309 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1310 );
1311
1312 workspace
1313 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1314 .await;
1315 assert_eq!(
1316 active_location(&workspace, cx),
1317 (file2.clone(), DisplayPoint::new(0, 0), 0.)
1318 );
1319
1320 workspace
1321 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1322 .await;
1323 assert_eq!(
1324 active_location(&workspace, cx),
1325 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1326 );
1327
1328 workspace
1329 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1330 .await;
1331 assert_eq!(
1332 active_location(&workspace, cx),
1333 (file1.clone(), DisplayPoint::new(0, 0), 0.)
1334 );
1335
1336 // Go back one more time and ensure we don't navigate past the first item in the history.
1337 workspace
1338 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1339 .await;
1340 assert_eq!(
1341 active_location(&workspace, cx),
1342 (file1.clone(), DisplayPoint::new(0, 0), 0.)
1343 );
1344
1345 workspace
1346 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1347 .await;
1348 assert_eq!(
1349 active_location(&workspace, cx),
1350 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1351 );
1352
1353 workspace
1354 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1355 .await;
1356 assert_eq!(
1357 active_location(&workspace, cx),
1358 (file2.clone(), DisplayPoint::new(0, 0), 0.)
1359 );
1360
1361 // Go forward to an item that has been closed, ensuring it gets re-opened at the same
1362 // location.
1363 workspace
1364 .update(cx, |workspace, cx| {
1365 let editor3_id = editor3.id();
1366 drop(editor3);
1367 Pane::close_item(workspace, workspace.active_pane().clone(), editor3_id, cx)
1368 })
1369 .await
1370 .unwrap();
1371 workspace
1372 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1373 .await;
1374 assert_eq!(
1375 active_location(&workspace, cx),
1376 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1377 );
1378
1379 workspace
1380 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1381 .await;
1382 assert_eq!(
1383 active_location(&workspace, cx),
1384 (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1385 );
1386
1387 workspace
1388 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1389 .await;
1390 assert_eq!(
1391 active_location(&workspace, cx),
1392 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1393 );
1394
1395 // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
1396 workspace
1397 .update(cx, |workspace, cx| {
1398 let editor2_id = editor2.id();
1399 drop(editor2);
1400 Pane::close_item(workspace, workspace.active_pane().clone(), editor2_id, cx)
1401 })
1402 .await
1403 .unwrap();
1404 app_state
1405 .fs
1406 .remove_file(Path::new("/root/a/file2"), Default::default())
1407 .await
1408 .unwrap();
1409 workspace
1410 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1411 .await;
1412 assert_eq!(
1413 active_location(&workspace, cx),
1414 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1415 );
1416 workspace
1417 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1418 .await;
1419 assert_eq!(
1420 active_location(&workspace, cx),
1421 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1422 );
1423
1424 // Modify file to collapse multiple nav history entries into the same location.
1425 // Ensure we don't visit the same location twice when navigating.
1426 editor1.update(cx, |editor, cx| {
1427 editor.change_selections(None, cx, |s| {
1428 s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
1429 })
1430 });
1431
1432 for _ in 0..5 {
1433 editor1.update(cx, |editor, cx| {
1434 editor.change_selections(None, cx, |s| {
1435 s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
1436 });
1437 });
1438 editor1.update(cx, |editor, cx| {
1439 editor.change_selections(None, cx, |s| {
1440 s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
1441 })
1442 });
1443 }
1444
1445 editor1.update(cx, |editor, cx| {
1446 editor.transact(cx, |editor, cx| {
1447 editor.change_selections(None, cx, |s| {
1448 s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
1449 });
1450 editor.insert("", cx);
1451 })
1452 });
1453
1454 editor1.update(cx, |editor, cx| {
1455 editor.change_selections(None, cx, |s| {
1456 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1457 })
1458 });
1459 workspace
1460 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1461 .await;
1462 assert_eq!(
1463 active_location(&workspace, cx),
1464 (file1.clone(), DisplayPoint::new(2, 0), 0.)
1465 );
1466 workspace
1467 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1468 .await;
1469 assert_eq!(
1470 active_location(&workspace, cx),
1471 (file1.clone(), DisplayPoint::new(3, 0), 0.)
1472 );
1473
1474 fn active_location(
1475 workspace: &ViewHandle<Workspace>,
1476 cx: &mut TestAppContext,
1477 ) -> (ProjectPath, DisplayPoint, f32) {
1478 workspace.update(cx, |workspace, cx| {
1479 let item = workspace.active_item(cx).unwrap();
1480 let editor = item.downcast::<Editor>().unwrap();
1481 let (selections, scroll_position) = editor.update(cx, |editor, cx| {
1482 (
1483 editor.selections.display_ranges(cx),
1484 editor.scroll_position(cx),
1485 )
1486 });
1487 (
1488 item.project_path(cx).unwrap(),
1489 selections[0].start,
1490 scroll_position.y(),
1491 )
1492 })
1493 }
1494 }
1495
1496 #[gpui::test]
1497 async fn test_reopening_closed_items(cx: &mut TestAppContext) {
1498 let app_state = init(cx);
1499 app_state
1500 .fs
1501 .as_fake()
1502 .insert_tree(
1503 "/root",
1504 json!({
1505 "a": {
1506 "file1": "",
1507 "file2": "",
1508 "file3": "",
1509 "file4": "",
1510 },
1511 }),
1512 )
1513 .await;
1514
1515 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1516 let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
1517 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1518
1519 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1520 let file1 = entries[0].clone();
1521 let file2 = entries[1].clone();
1522 let file3 = entries[2].clone();
1523 let file4 = entries[3].clone();
1524
1525 let file1_item_id = workspace
1526 .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
1527 .await
1528 .unwrap()
1529 .id();
1530 let file2_item_id = workspace
1531 .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
1532 .await
1533 .unwrap()
1534 .id();
1535 let file3_item_id = workspace
1536 .update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
1537 .await
1538 .unwrap()
1539 .id();
1540 let file4_item_id = workspace
1541 .update(cx, |w, cx| w.open_path(file4.clone(), true, cx))
1542 .await
1543 .unwrap()
1544 .id();
1545 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1546
1547 // Close all the pane items in some arbitrary order.
1548 workspace
1549 .update(cx, |workspace, cx| {
1550 Pane::close_item(workspace, pane.clone(), file1_item_id, cx)
1551 })
1552 .await
1553 .unwrap();
1554 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1555
1556 workspace
1557 .update(cx, |workspace, cx| {
1558 Pane::close_item(workspace, pane.clone(), file4_item_id, cx)
1559 })
1560 .await
1561 .unwrap();
1562 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1563
1564 workspace
1565 .update(cx, |workspace, cx| {
1566 Pane::close_item(workspace, pane.clone(), file2_item_id, cx)
1567 })
1568 .await
1569 .unwrap();
1570 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1571
1572 workspace
1573 .update(cx, |workspace, cx| {
1574 Pane::close_item(workspace, pane.clone(), file3_item_id, cx)
1575 })
1576 .await
1577 .unwrap();
1578 assert_eq!(active_path(&workspace, cx), None);
1579
1580 // Reopen all the closed items, ensuring they are reopened in the same order
1581 // in which they were closed.
1582 workspace.update(cx, Pane::reopen_closed_item).await;
1583 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1584
1585 workspace.update(cx, Pane::reopen_closed_item).await;
1586 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1587
1588 workspace.update(cx, Pane::reopen_closed_item).await;
1589 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1590
1591 workspace.update(cx, Pane::reopen_closed_item).await;
1592 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1593
1594 // Reopening past the last closed item is a no-op.
1595 workspace.update(cx, Pane::reopen_closed_item).await;
1596 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1597
1598 // Reopening closed items doesn't interfere with navigation history.
1599 workspace
1600 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1601 .await;
1602 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1603
1604 workspace
1605 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1606 .await;
1607 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1608
1609 workspace
1610 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1611 .await;
1612 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1613
1614 workspace
1615 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1616 .await;
1617 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1618
1619 workspace
1620 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1621 .await;
1622 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1623
1624 workspace
1625 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1626 .await;
1627 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1628
1629 workspace
1630 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1631 .await;
1632 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1633
1634 workspace
1635 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1636 .await;
1637 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1638
1639 fn active_path(
1640 workspace: &ViewHandle<Workspace>,
1641 cx: &TestAppContext,
1642 ) -> Option<ProjectPath> {
1643 workspace.read_with(cx, |workspace, cx| {
1644 let item = workspace.active_item(cx)?;
1645 item.project_path(cx)
1646 })
1647 }
1648 }
1649
1650 #[gpui::test]
1651 fn test_bundled_settings_and_themes(cx: &mut MutableAppContext) {
1652 cx.platform()
1653 .fonts()
1654 .add_fonts(&[
1655 Assets
1656 .load("fonts/zed-sans/zed-sans-extended.ttf")
1657 .unwrap()
1658 .to_vec()
1659 .into(),
1660 Assets
1661 .load("fonts/zed-mono/zed-mono-extended.ttf")
1662 .unwrap()
1663 .to_vec()
1664 .into(),
1665 ])
1666 .unwrap();
1667 let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
1668 let settings = Settings::defaults(Assets, cx.font_cache(), &themes);
1669
1670 let mut has_default_theme = false;
1671 for theme_name in themes.list() {
1672 let theme = themes.get(&theme_name).unwrap();
1673 if theme.name == settings.theme.name {
1674 has_default_theme = true;
1675 }
1676 assert_eq!(theme.name, theme_name);
1677 }
1678 assert!(has_default_theme);
1679 }
1680
1681 fn init(cx: &mut TestAppContext) -> Arc<AppState> {
1682 cx.foreground().forbid_parking();
1683 cx.update(|cx| {
1684 let mut app_state = AppState::test(cx);
1685 let state = Arc::get_mut(&mut app_state).unwrap();
1686 state.initialize_workspace = initialize_workspace;
1687 state.build_window_options = build_window_options;
1688 workspace::init(app_state.clone(), cx);
1689 editor::init(cx);
1690 pane::init(cx);
1691 app_state
1692 })
1693 }
1694
1695 fn rust_lang() -> Arc<language::Language> {
1696 Arc::new(language::Language::new(
1697 language::LanguageConfig {
1698 name: "Rust".into(),
1699 path_suffixes: vec!["rs".to_string()],
1700 ..Default::default()
1701 },
1702 Some(tree_sitter_rust::language()),
1703 ))
1704 }
1705}