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