1mod app_menus;
2pub mod inline_completion_registry;
3#[cfg(target_os = "linux")]
4pub(crate) mod linux_prompts;
5#[cfg(not(target_os = "linux"))]
6pub(crate) mod only_instance;
7mod open_listener;
8
9pub use app_menus::*;
10use breadcrumbs::Breadcrumbs;
11use client::ZED_URL_SCHEME;
12use collections::VecDeque;
13use editor::{scroll::Autoscroll, Editor, MultiBuffer};
14use gpui::{
15 actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, MenuItem, PromptLevel,
16 ReadGlobal, TitlebarOptions, View, ViewContext, VisualContext, WindowKind, WindowOptions,
17};
18pub use open_listener::*;
19
20use anyhow::Context as _;
21use assets::Assets;
22use futures::{channel::mpsc, select_biased, StreamExt};
23use outline_panel::OutlinePanel;
24use project::TaskSourceKind;
25use project_panel::ProjectPanel;
26use quick_action_bar::QuickActionBar;
27use release_channel::{AppCommitSha, ReleaseChannel};
28use rope::Rope;
29use search::project_search::ProjectSearchBar;
30use settings::{
31 initial_local_settings_content, initial_tasks_content, watch_config_file, KeymapFile, Settings,
32 SettingsStore, DEFAULT_KEYMAP_PATH,
33};
34use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
35use task::static_source::{StaticSource, TrackedFile};
36use theme::ActiveTheme;
37use workspace::notifications::NotificationId;
38
39use paths::{local_settings_file_relative_path, local_tasks_file_relative_path};
40use terminal_view::terminal_panel::{self, TerminalPanel};
41use util::{asset_str, ResultExt};
42use uuid::Uuid;
43use vim::VimModeSetting;
44use welcome::{BaseKeymap, MultibufferHint};
45use workspace::{
46 create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
47 open_new, AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings,
48};
49use workspace::{notifications::DetachAndPromptErr, Pane};
50use zed_actions::{OpenBrowser, OpenSettings, OpenZedUrl, Quit};
51
52actions!(
53 zed,
54 [
55 DebugElements,
56 Hide,
57 HideOthers,
58 Minimize,
59 OpenDefaultKeymap,
60 OpenDefaultSettings,
61 OpenLocalSettings,
62 OpenLocalTasks,
63 OpenTasks,
64 ResetDatabase,
65 ShowAll,
66 ToggleFullScreen,
67 Zoom,
68 TestPanic,
69 ]
70);
71
72pub fn init(cx: &mut AppContext) {
73 #[cfg(target_os = "macos")]
74 cx.on_action(|_: &Hide, cx| cx.hide());
75 #[cfg(target_os = "macos")]
76 cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps());
77 #[cfg(target_os = "macos")]
78 cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps());
79 cx.on_action(quit);
80
81 if ReleaseChannel::global(cx) == ReleaseChannel::Dev {
82 cx.on_action(test_panic);
83 }
84}
85
86pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut AppContext) -> WindowOptions {
87 let display = display_uuid.and_then(|uuid| {
88 cx.displays()
89 .into_iter()
90 .find(|display| display.uuid().ok() == Some(uuid))
91 });
92 let app_id = ReleaseChannel::global(cx).app_id();
93 let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
94 Ok(val) if val == "server" => gpui::WindowDecorations::Server,
95 Ok(val) if val == "client" => gpui::WindowDecorations::Client,
96 _ => gpui::WindowDecorations::Client,
97 };
98
99 WindowOptions {
100 titlebar: Some(TitlebarOptions {
101 title: None,
102 appears_transparent: true,
103 traffic_light_position: Some(point(px(9.0), px(9.0))),
104 }),
105 window_bounds: None,
106 focus: false,
107 show: false,
108 kind: WindowKind::Normal,
109 is_movable: true,
110 display_id: display.map(|display| display.id()),
111 window_background: cx.theme().window_background_appearance(),
112 app_id: Some(app_id.to_owned()),
113 window_decorations: Some(window_decorations),
114 window_min_size: Some(gpui::Size {
115 width: px(360.0),
116 height: px(240.0),
117 }),
118 }
119}
120
121pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
122 cx.observe_new_views(move |workspace: &mut Workspace, cx| {
123 let workspace_handle = cx.view().clone();
124 let center_pane = workspace.active_pane().clone();
125 initialize_pane(workspace, ¢er_pane, cx);
126 cx.subscribe(&workspace_handle, {
127 move |workspace, _, event, cx| match event {
128 workspace::Event::PaneAdded(pane) => {
129 initialize_pane(workspace, pane, cx);
130 }
131 workspace::Event::OpenBundledFile {
132 text,
133 title,
134 language,
135 } => open_bundled_file(workspace, text.clone(), title, language, cx),
136 _ => {}
137 }
138 })
139 .detach();
140
141 let inline_completion_button = cx.new_view(|cx| {
142 inline_completion_button::InlineCompletionButton::new(app_state.fs.clone(), cx)
143 });
144
145 let diagnostic_summary =
146 cx.new_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
147 let activity_indicator =
148 activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
149 let active_buffer_language =
150 cx.new_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
151 let vim_mode_indicator = cx.new_view(|cx| vim::ModeIndicator::new(cx));
152 let cursor_position =
153 cx.new_view(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
154 workspace.status_bar().update(cx, |status_bar, cx| {
155 status_bar.add_left_item(diagnostic_summary, cx);
156 status_bar.add_left_item(activity_indicator, cx);
157 status_bar.add_right_item(inline_completion_button, cx);
158 status_bar.add_right_item(active_buffer_language, cx);
159 status_bar.add_right_item(vim_mode_indicator, cx);
160 status_bar.add_right_item(cursor_position, cx);
161 });
162
163 auto_update::notify_of_any_new_update(cx);
164
165 let handle = cx.view().downgrade();
166 cx.on_window_should_close(move |cx| {
167 handle
168 .update(cx, |workspace, cx| {
169 // We'll handle closing asynchronously
170 workspace.close_window(&Default::default(), cx);
171 false
172 })
173 .unwrap_or(true)
174 });
175
176 let project = workspace.project().clone();
177 if project.update(cx, |project, cx| {
178 project.is_local() || project.ssh_connection_string(cx).is_some()
179 }) {
180 project.update(cx, |project, cx| {
181 let fs = app_state.fs.clone();
182 project.task_inventory().update(cx, |inventory, cx| {
183 let tasks_file_rx =
184 watch_config_file(&cx.background_executor(), fs, paths::tasks_file().clone());
185 inventory.add_source(
186 TaskSourceKind::AbsPath {
187 id_base: "global_tasks".into(),
188 abs_path: paths::tasks_file().clone(),
189 },
190 |tx, cx| StaticSource::new(TrackedFile::new(tasks_file_rx, tx, cx)),
191 cx,
192 );
193 })
194 });
195 }
196
197 cx.spawn(|workspace_handle, mut cx| async move {
198 let assistant_panel =
199 assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone());
200
201 let runtime_panel = repl::RuntimePanel::load(workspace_handle.clone(), cx.clone());
202
203 let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
204 let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
205 let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
206 let channels_panel =
207 collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
208 let chat_panel =
209 collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
210 let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
211 workspace_handle.clone(),
212 cx.clone(),
213 );
214
215 let (
216 project_panel,
217 outline_panel,
218 terminal_panel,
219 assistant_panel,
220 runtime_panel,
221 channels_panel,
222 chat_panel,
223 notification_panel,
224 ) = futures::try_join!(
225 project_panel,
226 outline_panel,
227 terminal_panel,
228 assistant_panel,
229 runtime_panel,
230 channels_panel,
231 chat_panel,
232 notification_panel,
233 )?;
234
235 workspace_handle.update(&mut cx, |workspace, cx| {
236 workspace.add_panel(assistant_panel, cx);
237 workspace.add_panel(runtime_panel, cx);
238 workspace.add_panel(project_panel, cx);
239 workspace.add_panel(outline_panel, cx);
240 workspace.add_panel(terminal_panel, cx);
241 workspace.add_panel(channels_panel, cx);
242 workspace.add_panel(chat_panel, cx);
243 workspace.add_panel(notification_panel, cx);
244 cx.focus_self();
245 })
246 })
247 .detach();
248
249 workspace
250 .register_action(about)
251 .register_action(|_, _: &Minimize, cx| {
252 cx.minimize_window();
253 })
254 .register_action(|_, _: &Zoom, cx| {
255 cx.zoom_window();
256 })
257 .register_action(|_, _: &ToggleFullScreen, cx| {
258 cx.toggle_fullscreen();
259 })
260 .register_action(|_, action: &OpenZedUrl, cx| {
261 OpenListener::global(cx).open_urls(vec![action.url.clone()])
262 })
263 .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url))
264 .register_action(move |_, _: &zed_actions::IncreaseBufferFontSize, cx| {
265 theme::adjust_buffer_font_size(cx, |size| *size += px(1.0))
266 })
267 .register_action(move |_, _: &zed_actions::DecreaseBufferFontSize, cx| {
268 theme::adjust_buffer_font_size(cx, |size| *size -= px(1.0))
269 })
270 .register_action(move |_, _: &zed_actions::ResetBufferFontSize, cx| {
271 theme::reset_buffer_font_size(cx)
272 })
273 .register_action(move |_, _: &zed_actions::IncreaseUiFontSize, cx| {
274 theme::adjust_ui_font_size(cx, |size| *size += px(1.0))
275 })
276 .register_action(move |_, _: &zed_actions::DecreaseUiFontSize, cx| {
277 theme::adjust_ui_font_size(cx, |size| *size -= px(1.0))
278 })
279 .register_action(move |_, _: &zed_actions::ResetUiFontSize, cx| {
280 theme::reset_ui_font_size(cx)
281 })
282 .register_action(move |_, _: &zed_actions::IncreaseBufferFontSize, cx| {
283 theme::adjust_buffer_font_size(cx, |size| *size += px(1.0))
284 })
285 .register_action(move |_, _: &zed_actions::DecreaseBufferFontSize, cx| {
286 theme::adjust_buffer_font_size(cx, |size| *size -= px(1.0))
287 })
288 .register_action(move |_, _: &zed_actions::ResetBufferFontSize, cx| {
289 theme::reset_buffer_font_size(cx)
290 })
291 .register_action(|_, _: &install_cli::Install, cx| {
292 cx.spawn(|workspace, mut cx| async move {
293 if cfg!(target_os = "linux") {
294 let prompt = cx.prompt(
295 PromptLevel::Warning,
296 "CLI should already be installed",
297 Some("If you installed Zed from our official release add ~/.local/bin to your PATH.\n\nIf you installed Zed from a different source like your package manager, then you may need to create an alias/symlink manually.\n\nDepending on your package manager, the CLI might be named zeditor, zedit, zed-editor or something else."),
298 &["Ok"],
299 );
300 cx.background_executor().spawn(prompt).detach();
301 return Ok(());
302 }
303 let path = install_cli::install_cli(cx.deref())
304 .await
305 .context("error creating CLI symlink")?;
306
307 workspace.update(&mut cx, |workspace, cx| {
308 struct InstalledZedCli;
309
310 workspace.show_toast(
311 Toast::new(
312 NotificationId::unique::<InstalledZedCli>(),
313 format!(
314 "Installed `zed` to {}. You can launch {} from your terminal.",
315 path.to_string_lossy(),
316 ReleaseChannel::global(cx).display_name()
317 ),
318 ),
319 cx,
320 )
321 })?;
322 register_zed_scheme(&cx).await.log_err();
323 Ok(())
324 })
325 .detach_and_prompt_err("Error installing zed cli", cx, |_, _| None);
326 })
327 .register_action(|_, _: &install_cli::RegisterZedScheme, cx| {
328 cx.spawn(|workspace, mut cx| async move {
329 register_zed_scheme(&cx).await?;
330 workspace.update(&mut cx, |workspace, cx| {
331 struct RegisterZedScheme;
332
333 workspace.show_toast(
334 Toast::new(
335 NotificationId::unique::<RegisterZedScheme>(),
336 format!(
337 "zed:// links will now open in {}.",
338 ReleaseChannel::global(cx).display_name()
339 ),
340 ),
341 cx,
342 )
343 })?;
344 Ok(())
345 })
346 .detach_and_prompt_err(
347 "Error registering zed:// scheme",
348 cx,
349 |_, _| None,
350 );
351 })
352 .register_action(|workspace, _: &OpenLog, cx| {
353 open_log_file(workspace, cx);
354 })
355 .register_action(|workspace, _: &zed_actions::OpenLicenses, cx| {
356 open_bundled_file(
357 workspace,
358 asset_str::<Assets>("licenses.md"),
359 "Open Source License Attribution",
360 "Markdown",
361 cx,
362 );
363 })
364 .register_action(
365 move |workspace: &mut Workspace,
366 _: &zed_actions::OpenTelemetryLog,
367 cx: &mut ViewContext<Workspace>| {
368 open_telemetry_log_file(workspace, cx);
369 },
370 )
371 .register_action(
372 move |_: &mut Workspace,
373 _: &zed_actions::OpenKeymap,
374 cx: &mut ViewContext<Workspace>| {
375 open_settings_file(&paths::keymap_file(), || settings::initial_keymap_content().as_ref().into(), cx);
376 },
377 )
378 .register_action(
379 move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
380 open_settings_file(
381 paths::settings_file(),
382 || settings::initial_user_settings_content().as_ref().into(),
383 cx,
384 );
385 },
386 )
387 .register_action(
388 move |_: &mut Workspace, _: &OpenTasks, cx: &mut ViewContext<Workspace>| {
389 open_settings_file(
390 paths::tasks_file(),
391 || settings::initial_tasks_content().as_ref().into(),
392 cx,
393 );
394 },
395 )
396 .register_action(open_local_settings_file)
397 .register_action(open_local_tasks_file)
398 .register_action(
399 move |workspace: &mut Workspace,
400 _: &OpenDefaultKeymap,
401 cx: &mut ViewContext<Workspace>| {
402 open_bundled_file(
403 workspace,
404 settings::default_keymap(),
405 "Default Key Bindings",
406 "JSON",
407 cx,
408 );
409 },
410 )
411 .register_action(
412 move |workspace: &mut Workspace,
413 _: &OpenDefaultSettings,
414 cx: &mut ViewContext<Workspace>| {
415 open_bundled_file(
416 workspace,
417 settings::default_settings(),
418 "Default Settings",
419 "JSON",
420 cx,
421 );
422 },
423 )
424 .register_action(
425 |workspace: &mut Workspace,
426 _: &project_panel::ToggleFocus,
427 cx: &mut ViewContext<Workspace>| {
428 workspace.toggle_panel_focus::<ProjectPanel>(cx);
429 },
430 )
431 .register_action(
432 |workspace: &mut Workspace,
433 _: &outline_panel::ToggleFocus,
434 cx: &mut ViewContext<Workspace>| {
435 workspace.toggle_panel_focus::<OutlinePanel>(cx);
436 },
437 )
438 .register_action(
439 |workspace: &mut Workspace,
440 _: &collab_ui::collab_panel::ToggleFocus,
441 cx: &mut ViewContext<Workspace>| {
442 workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
443 },
444 )
445 .register_action(
446 |workspace: &mut Workspace,
447 _: &collab_ui::chat_panel::ToggleFocus,
448 cx: &mut ViewContext<Workspace>| {
449 workspace.toggle_panel_focus::<collab_ui::chat_panel::ChatPanel>(cx);
450 },
451 )
452 .register_action(
453 |workspace: &mut Workspace,
454 _: &collab_ui::notification_panel::ToggleFocus,
455 cx: &mut ViewContext<Workspace>| {
456 workspace
457 .toggle_panel_focus::<collab_ui::notification_panel::NotificationPanel>(cx);
458 },
459 )
460 .register_action(
461 |workspace: &mut Workspace,
462 _: &terminal_panel::ToggleFocus,
463 cx: &mut ViewContext<Workspace>| {
464 workspace.toggle_panel_focus::<TerminalPanel>(cx);
465 },
466 )
467 .register_action({
468 let app_state = Arc::downgrade(&app_state);
469 move |_, _: &NewWindow, cx| {
470 if let Some(app_state) = app_state.upgrade() {
471 open_new(app_state, cx, |workspace, cx| {
472 Editor::new_file(workspace, &Default::default(), cx)
473 })
474 .detach();
475 }
476 }
477 })
478 .register_action({
479 let app_state = Arc::downgrade(&app_state);
480 move |_, _: &NewFile, cx| {
481 if let Some(app_state) = app_state.upgrade() {
482 open_new(app_state, cx, |workspace, cx| {
483 Editor::new_file(workspace, &Default::default(), cx)
484 })
485 .detach();
486 }
487 }
488 });
489
490 workspace.focus_handle(cx).focus(cx);
491 })
492 .detach();
493}
494
495fn initialize_pane(workspace: &mut Workspace, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
496 pane.update(cx, |pane, cx| {
497 pane.toolbar().update(cx, |toolbar, cx| {
498 let multibuffer_hint = cx.new_view(|_| MultibufferHint::new());
499 toolbar.add_item(multibuffer_hint, cx);
500 let breadcrumbs = cx.new_view(|_| Breadcrumbs::new());
501 toolbar.add_item(breadcrumbs, cx);
502 let buffer_search_bar = cx.new_view(search::BufferSearchBar::new);
503 toolbar.add_item(buffer_search_bar.clone(), cx);
504
505 let quick_action_bar =
506 cx.new_view(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
507 toolbar.add_item(quick_action_bar, cx);
508 let diagnostic_editor_controls = cx.new_view(|_| diagnostics::ToolbarControls::new());
509 toolbar.add_item(diagnostic_editor_controls, cx);
510 let project_search_bar = cx.new_view(|_| ProjectSearchBar::new());
511 toolbar.add_item(project_search_bar, cx);
512 let lsp_log_item = cx.new_view(|_| language_tools::LspLogToolbarItemView::new());
513 toolbar.add_item(lsp_log_item, cx);
514 let syntax_tree_item =
515 cx.new_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
516 toolbar.add_item(syntax_tree_item, cx);
517 })
518 });
519}
520
521fn about(_: &mut Workspace, _: &zed_actions::About, cx: &mut gpui::ViewContext<Workspace>) {
522 let release_channel = ReleaseChannel::global(cx).display_name();
523 let version = env!("CARGO_PKG_VERSION");
524 let message = format!("{release_channel} {version}");
525 let detail = AppCommitSha::try_global(cx).map(|sha| sha.0.clone());
526
527 let prompt = cx.prompt(PromptLevel::Info, &message, detail.as_deref(), &["OK"]);
528 cx.foreground_executor()
529 .spawn(async {
530 prompt.await.ok();
531 })
532 .detach();
533}
534
535fn test_panic(_: &TestPanic, _: &mut AppContext) {
536 panic!("Ran the TestPanic action")
537}
538
539fn quit(_: &Quit, cx: &mut AppContext) {
540 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
541 cx.spawn(|mut cx| async move {
542 let mut workspace_windows = cx.update(|cx| {
543 cx.windows()
544 .into_iter()
545 .filter_map(|window| window.downcast::<Workspace>())
546 .collect::<Vec<_>>()
547 })?;
548
549 // If multiple windows have unsaved changes, and need a save prompt,
550 // prompt in the active window before switching to a different window.
551 cx.update(|mut cx| {
552 workspace_windows.sort_by_key(|window| window.is_active(&mut cx) == Some(false));
553 })
554 .log_err();
555
556 if let (true, Some(workspace)) = (should_confirm, workspace_windows.first().copied()) {
557 let answer = workspace
558 .update(&mut cx, |_, cx| {
559 cx.prompt(
560 PromptLevel::Info,
561 "Are you sure you want to quit?",
562 None,
563 &["Quit", "Cancel"],
564 )
565 })
566 .log_err();
567
568 if let Some(answer) = answer {
569 let answer = answer.await.ok();
570 if answer != Some(0) {
571 return Ok(());
572 }
573 }
574 }
575
576 // If the user cancels any save prompt, then keep the app open.
577 for window in workspace_windows {
578 if let Some(should_close) = window
579 .update(&mut cx, |workspace, cx| {
580 workspace.prepare_to_close(true, cx)
581 })
582 .log_err()
583 {
584 if !should_close.await? {
585 return Ok(());
586 }
587 }
588 }
589 cx.update(|cx| cx.quit())?;
590 anyhow::Ok(())
591 })
592 .detach_and_log_err(cx);
593}
594
595fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
596 const MAX_LINES: usize = 1000;
597 workspace
598 .with_local_workspace(cx, move |workspace, cx| {
599 let fs = workspace.app_state().fs.clone();
600 cx.spawn(|workspace, mut cx| async move {
601 let (old_log, new_log) =
602 futures::join!(fs.load(paths::old_log_file()), fs.load(paths::log_file()));
603 let log = match (old_log, new_log) {
604 (Err(_), Err(_)) => None,
605 (old_log, new_log) => {
606 let mut lines = VecDeque::with_capacity(MAX_LINES);
607 for line in old_log
608 .iter()
609 .flat_map(|log| log.lines())
610 .chain(new_log.iter().flat_map(|log| log.lines()))
611 {
612 if lines.len() == MAX_LINES {
613 lines.pop_front();
614 }
615 lines.push_back(line);
616 }
617 Some(
618 lines
619 .into_iter()
620 .flat_map(|line| [line, "\n"])
621 .collect::<String>(),
622 )
623 }
624 };
625
626 workspace
627 .update(&mut cx, |workspace, cx| {
628 let Some(log) = log else {
629 struct OpenLogError;
630
631 workspace.show_notification(
632 NotificationId::unique::<OpenLogError>(),
633 cx,
634 |cx| {
635 cx.new_view(|_| {
636 MessageNotification::new(format!(
637 "Unable to access/open log file at path {:?}",
638 paths::log_file().as_path()
639 ))
640 })
641 },
642 );
643 return;
644 };
645 let project = workspace.project().clone();
646 let buffer = project.update(cx, |project, cx| {
647 project.create_local_buffer(&log, None, cx)
648 });
649
650 let buffer = cx.new_model(|cx| {
651 MultiBuffer::singleton(buffer, cx).with_title("Log".into())
652 });
653 let editor = cx.new_view(|cx| {
654 let mut editor =
655 Editor::for_multibuffer(buffer, Some(project), true, cx);
656 editor.set_breadcrumb_header(format!(
657 "Last {} lines in {}",
658 MAX_LINES,
659 paths::log_file().display()
660 ));
661 editor
662 });
663
664 editor.update(cx, |editor, cx| {
665 let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
666 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
667 s.select_ranges(Some(
668 last_multi_buffer_offset..last_multi_buffer_offset,
669 ));
670 })
671 });
672
673 workspace.add_item_to_active_pane(Box::new(editor), None, cx);
674 })
675 .log_err();
676 })
677 .detach();
678 })
679 .detach();
680}
681
682pub fn handle_keymap_file_changes(
683 mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
684 cx: &mut AppContext,
685) {
686 BaseKeymap::register(cx);
687 VimModeSetting::register(cx);
688
689 let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
690 let mut old_base_keymap = *BaseKeymap::get_global(cx);
691 let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
692 cx.observe_global::<SettingsStore>(move |cx| {
693 let new_base_keymap = *BaseKeymap::get_global(cx);
694 let new_vim_enabled = VimModeSetting::get_global(cx).0;
695
696 if new_base_keymap != old_base_keymap || new_vim_enabled != old_vim_enabled {
697 old_base_keymap = new_base_keymap;
698 old_vim_enabled = new_vim_enabled;
699 base_keymap_tx.unbounded_send(()).unwrap();
700 }
701 })
702 .detach();
703
704 load_default_keymap(cx);
705
706 cx.spawn(move |cx| async move {
707 let mut user_keymap = KeymapFile::default();
708 loop {
709 select_biased! {
710 _ = base_keymap_rx.next() => {}
711 user_keymap_content = user_keymap_file_rx.next() => {
712 if let Some(user_keymap_content) = user_keymap_content {
713 if let Some(keymap_content) = KeymapFile::parse(&user_keymap_content).log_err() {
714 user_keymap = keymap_content;
715 } else {
716 continue
717 }
718 }
719 }
720 }
721 cx.update(|cx| reload_keymaps(cx, &user_keymap)).ok();
722 }
723 })
724 .detach();
725}
726
727fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
728 cx.clear_key_bindings();
729 load_default_keymap(cx);
730 keymap_content.clone().add_to_cx(cx).log_err();
731 cx.set_menus(app_menus());
732 cx.set_dock_menu(vec![MenuItem::action("New Window", workspace::NewWindow)])
733}
734
735pub fn load_default_keymap(cx: &mut AppContext) {
736 let base_keymap = *BaseKeymap::get_global(cx);
737 if base_keymap == BaseKeymap::None {
738 return;
739 }
740
741 KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
742 if VimModeSetting::get_global(cx).0 {
743 KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
744 }
745
746 if let Some(asset_path) = base_keymap.asset_path() {
747 KeymapFile::load_asset(asset_path, cx).unwrap();
748 }
749}
750
751fn open_local_settings_file(
752 workspace: &mut Workspace,
753 _: &OpenLocalSettings,
754 cx: &mut ViewContext<Workspace>,
755) {
756 open_local_file(
757 workspace,
758 local_settings_file_relative_path(),
759 initial_local_settings_content(),
760 cx,
761 )
762}
763
764fn open_local_tasks_file(
765 workspace: &mut Workspace,
766 _: &OpenLocalTasks,
767 cx: &mut ViewContext<Workspace>,
768) {
769 open_local_file(
770 workspace,
771 local_tasks_file_relative_path(),
772 initial_tasks_content(),
773 cx,
774 )
775}
776
777fn open_local_file(
778 workspace: &mut Workspace,
779 settings_relative_path: &'static Path,
780 initial_contents: Cow<'static, str>,
781 cx: &mut ViewContext<Workspace>,
782) {
783 let project = workspace.project().clone();
784 let worktree = project
785 .read(cx)
786 .visible_worktrees(cx)
787 .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
788 if let Some(worktree) = worktree {
789 let tree_id = worktree.read(cx).id();
790 cx.spawn(|workspace, mut cx| async move {
791 if let Some(dir_path) = settings_relative_path.parent() {
792 if worktree.update(&mut cx, |tree, _| tree.entry_for_path(dir_path).is_none())? {
793 project
794 .update(&mut cx, |project, cx| {
795 project.create_entry((tree_id, dir_path), true, cx)
796 })?
797 .await
798 .context("worktree was removed")?;
799 }
800 }
801
802 if worktree.update(&mut cx, |tree, _| {
803 tree.entry_for_path(settings_relative_path).is_none()
804 })? {
805 project
806 .update(&mut cx, |project, cx| {
807 project.create_entry((tree_id, settings_relative_path), false, cx)
808 })?
809 .await
810 .context("worktree was removed")?;
811 }
812
813 let editor = workspace
814 .update(&mut cx, |workspace, cx| {
815 workspace.open_path((tree_id, settings_relative_path), None, true, cx)
816 })?
817 .await?
818 .downcast::<Editor>()
819 .context("unexpected item type: expected editor item")?;
820
821 editor
822 .downgrade()
823 .update(&mut cx, |editor, cx| {
824 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
825 if buffer.read(cx).is_empty() {
826 buffer.update(cx, |buffer, cx| {
827 buffer.edit([(0..0, initial_contents)], None, cx)
828 });
829 }
830 }
831 })
832 .ok();
833
834 anyhow::Ok(())
835 })
836 .detach();
837 } else {
838 struct NoOpenFolders;
839
840 workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
841 cx.new_view(|_| MessageNotification::new("This project has no folders open."))
842 })
843 }
844}
845
846fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
847 workspace.with_local_workspace(cx, move |workspace, cx| {
848 let app_state = workspace.app_state().clone();
849 cx.spawn(|workspace, mut cx| async move {
850 async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
851 let path = app_state.client.telemetry().log_file_path()?;
852 app_state.fs.load(&path).await.log_err()
853 }
854
855 let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
856
857 const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
858 let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
859 if let Some(newline_offset) = log[start_offset..].find('\n') {
860 start_offset += newline_offset + 1;
861 }
862 let log_suffix = &log[start_offset..];
863 let json = app_state.languages.language_for_name("JSON").await.log_err();
864
865 workspace.update(&mut cx, |workspace, cx| {
866 let project = workspace.project().clone();
867 let buffer = project
868 .update(cx, |project, cx| project.create_local_buffer("", None, cx));
869 buffer.update(cx, |buffer, cx| {
870 buffer.set_language(json, cx);
871 buffer.edit(
872 [(
873 0..0,
874 concat!(
875 "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
876 "// Telemetry can be disabled via the `settings.json` file.\n",
877 "// Here is the data that has been reported for the current session:\n",
878 "\n"
879 ),
880 )],
881 None,
882 cx,
883 );
884 buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
885 });
886
887 let buffer = cx.new_model(|cx| {
888 MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
889 });
890 workspace.add_item_to_active_pane(
891 Box::new(cx.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), true, cx))),
892 None,cx,
893 );
894 }).log_err()?;
895
896 Some(())
897 })
898 .detach();
899 }).detach();
900}
901
902fn open_bundled_file(
903 workspace: &mut Workspace,
904 text: Cow<'static, str>,
905 title: &'static str,
906 language: &'static str,
907 cx: &mut ViewContext<Workspace>,
908) {
909 let language = workspace.app_state().languages.language_for_name(language);
910 cx.spawn(|workspace, mut cx| async move {
911 let language = language.await.log_err();
912 workspace
913 .update(&mut cx, |workspace, cx| {
914 workspace.with_local_workspace(cx, |workspace, cx| {
915 let project = workspace.project();
916 let buffer = project.update(cx, move |project, cx| {
917 project.create_local_buffer(text.as_ref(), language, cx)
918 });
919 let buffer = cx.new_model(|cx| {
920 MultiBuffer::singleton(buffer, cx).with_title(title.into())
921 });
922 workspace.add_item_to_active_pane(
923 Box::new(cx.new_view(|cx| {
924 Editor::for_multibuffer(buffer, Some(project.clone()), true, cx)
925 })),
926 None,
927 cx,
928 );
929 })
930 })?
931 .await
932 })
933 .detach_and_log_err(cx);
934}
935
936fn open_settings_file(
937 abs_path: &'static Path,
938 default_content: impl FnOnce() -> Rope + Send + 'static,
939 cx: &mut ViewContext<Workspace>,
940) {
941 cx.spawn(|workspace, mut cx| async move {
942 let (worktree_creation_task, settings_open_task) =
943 workspace.update(&mut cx, |workspace, cx| {
944 let worktree_creation_task = workspace.project().update(cx, |project, cx| {
945 // Set up a dedicated worktree for settings, since otherwise we're dropping and re-starting LSP servers for each file inside on every settings file close/open
946 // TODO: Do note that all other external files (e.g. drag and drop from OS) still have their worktrees released on file close, causing LSP servers' restarts.
947 project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
948 });
949 let settings_open_task = create_and_open_local_file(&abs_path, cx, default_content);
950 (worktree_creation_task, settings_open_task)
951 })?;
952
953 let _ = worktree_creation_task.await?;
954 let _ = settings_open_task.await?;
955 anyhow::Ok(())
956 })
957 .detach_and_log_err(cx);
958}
959
960#[cfg(test)]
961mod tests {
962 use super::*;
963 use anyhow::anyhow;
964 use assets::Assets;
965 use collections::{HashMap, HashSet};
966 use editor::{display_map::DisplayRow, scroll::Autoscroll, DisplayPoint, Editor};
967 use gpui::{
968 actions, Action, AnyWindowHandle, AppContext, AssetSource, BorrowAppContext, Entity,
969 SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle,
970 };
971 use language::{LanguageMatcher, LanguageRegistry};
972 use project::{project_settings::ProjectSettings, Project, ProjectPath, WorktreeSettings};
973 use serde_json::json;
974 use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
975 use std::{
976 path::{Path, PathBuf},
977 time::Duration,
978 };
979 use task::{RevealStrategy, SpawnInTerminal};
980 use theme::{ThemeRegistry, ThemeSettings};
981 use workspace::{
982 item::{Item, ItemHandle},
983 open_new, open_paths, pane, NewFile, OpenVisible, SaveIntent, SplitDirection,
984 WorkspaceHandle,
985 };
986
987 #[gpui::test]
988 async fn test_open_non_existing_file(cx: &mut TestAppContext) {
989 let app_state = init_test(cx);
990 app_state
991 .fs
992 .as_fake()
993 .insert_tree(
994 "/root",
995 json!({
996 "a": {
997 },
998 }),
999 )
1000 .await;
1001
1002 cx.update(|cx| {
1003 open_paths(
1004 &[PathBuf::from("/root/a/new")],
1005 app_state.clone(),
1006 workspace::OpenOptions::default(),
1007 cx,
1008 )
1009 })
1010 .await
1011 .unwrap();
1012 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1013
1014 let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
1015 workspace
1016 .update(cx, |workspace, cx| {
1017 assert!(workspace.active_item_as::<Editor>(cx).is_some())
1018 })
1019 .unwrap();
1020 }
1021
1022 #[gpui::test]
1023 async fn test_open_paths_action(cx: &mut TestAppContext) {
1024 let app_state = init_test(cx);
1025 app_state
1026 .fs
1027 .as_fake()
1028 .insert_tree(
1029 "/root",
1030 json!({
1031 "a": {
1032 "aa": null,
1033 "ab": null,
1034 },
1035 "b": {
1036 "ba": null,
1037 "bb": null,
1038 },
1039 "c": {
1040 "ca": null,
1041 "cb": null,
1042 },
1043 "d": {
1044 "da": null,
1045 "db": null,
1046 },
1047 "e": {
1048 "ea": null,
1049 "eb": null,
1050 }
1051 }),
1052 )
1053 .await;
1054
1055 cx.update(|cx| {
1056 open_paths(
1057 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
1058 app_state.clone(),
1059 workspace::OpenOptions::default(),
1060 cx,
1061 )
1062 })
1063 .await
1064 .unwrap();
1065 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1066
1067 cx.update(|cx| {
1068 open_paths(
1069 &[PathBuf::from("/root/a")],
1070 app_state.clone(),
1071 workspace::OpenOptions::default(),
1072 cx,
1073 )
1074 })
1075 .await
1076 .unwrap();
1077 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1078 let workspace_1 = cx
1079 .read(|cx| cx.windows()[0].downcast::<Workspace>())
1080 .unwrap();
1081 cx.run_until_parked();
1082 workspace_1
1083 .update(cx, |workspace, cx| {
1084 assert_eq!(workspace.worktrees(cx).count(), 2);
1085 assert!(workspace.left_dock().read(cx).is_open());
1086 assert!(workspace
1087 .active_pane()
1088 .read(cx)
1089 .focus_handle(cx)
1090 .is_focused(cx));
1091 })
1092 .unwrap();
1093
1094 cx.update(|cx| {
1095 open_paths(
1096 &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
1097 app_state.clone(),
1098 workspace::OpenOptions::default(),
1099 cx,
1100 )
1101 })
1102 .await
1103 .unwrap();
1104 assert_eq!(cx.read(|cx| cx.windows().len()), 2);
1105
1106 // Replace existing windows
1107 let window = cx
1108 .update(|cx| cx.windows()[0].downcast::<Workspace>())
1109 .unwrap();
1110 cx.update(|cx| {
1111 open_paths(
1112 &[PathBuf::from("/root/e")],
1113 app_state,
1114 workspace::OpenOptions {
1115 replace_window: Some(window),
1116 ..Default::default()
1117 },
1118 cx,
1119 )
1120 })
1121 .await
1122 .unwrap();
1123 cx.background_executor.run_until_parked();
1124 assert_eq!(cx.read(|cx| cx.windows().len()), 2);
1125 let workspace_1 = cx
1126 .update(|cx| cx.windows()[0].downcast::<Workspace>())
1127 .unwrap();
1128 workspace_1
1129 .update(cx, |workspace, cx| {
1130 assert_eq!(
1131 workspace
1132 .worktrees(cx)
1133 .map(|w| w.read(cx).abs_path())
1134 .collect::<Vec<_>>(),
1135 &[Path::new("/root/e").into()]
1136 );
1137 assert!(workspace.left_dock().read(cx).is_open());
1138 assert!(workspace.active_pane().focus_handle(cx).is_focused(cx));
1139 })
1140 .unwrap();
1141 }
1142
1143 #[gpui::test]
1144 async fn test_open_add_new(cx: &mut TestAppContext) {
1145 let app_state = init_test(cx);
1146 app_state
1147 .fs
1148 .as_fake()
1149 .insert_tree("/root", json!({"a": "hey", "b": "", "dir": {"c": "f"}}))
1150 .await;
1151
1152 cx.update(|cx| {
1153 open_paths(
1154 &[PathBuf::from("/root/dir")],
1155 app_state.clone(),
1156 workspace::OpenOptions::default(),
1157 cx,
1158 )
1159 })
1160 .await
1161 .unwrap();
1162 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1163
1164 cx.update(|cx| {
1165 open_paths(
1166 &[PathBuf::from("/root/a")],
1167 app_state.clone(),
1168 workspace::OpenOptions {
1169 open_new_workspace: Some(false),
1170 ..Default::default()
1171 },
1172 cx,
1173 )
1174 })
1175 .await
1176 .unwrap();
1177 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1178
1179 cx.update(|cx| {
1180 open_paths(
1181 &[PathBuf::from("/root/dir/c")],
1182 app_state.clone(),
1183 workspace::OpenOptions {
1184 open_new_workspace: Some(true),
1185 ..Default::default()
1186 },
1187 cx,
1188 )
1189 })
1190 .await
1191 .unwrap();
1192 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
1193 }
1194
1195 #[gpui::test]
1196 async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
1197 let app_state = init_test(cx);
1198 app_state
1199 .fs
1200 .as_fake()
1201 .insert_tree("/root", json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}))
1202 .await;
1203
1204 cx.update(|cx| {
1205 open_paths(
1206 &[PathBuf::from("/root/dir1/a")],
1207 app_state.clone(),
1208 workspace::OpenOptions::default(),
1209 cx,
1210 )
1211 })
1212 .await
1213 .unwrap();
1214 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1215 let window1 = cx.update(|cx| cx.active_window().unwrap());
1216
1217 cx.update(|cx| {
1218 open_paths(
1219 &[PathBuf::from("/root/dir2/c")],
1220 app_state.clone(),
1221 workspace::OpenOptions::default(),
1222 cx,
1223 )
1224 })
1225 .await
1226 .unwrap();
1227 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1228
1229 cx.update(|cx| {
1230 open_paths(
1231 &[PathBuf::from("/root/dir2")],
1232 app_state.clone(),
1233 workspace::OpenOptions::default(),
1234 cx,
1235 )
1236 })
1237 .await
1238 .unwrap();
1239 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
1240 let window2 = cx.update(|cx| cx.active_window().unwrap());
1241 assert!(window1 != window2);
1242 cx.update_window(window1, |_, cx| cx.activate_window())
1243 .unwrap();
1244
1245 cx.update(|cx| {
1246 open_paths(
1247 &[PathBuf::from("/root/dir2/c")],
1248 app_state.clone(),
1249 workspace::OpenOptions::default(),
1250 cx,
1251 )
1252 })
1253 .await
1254 .unwrap();
1255 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
1256 // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
1257 assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
1258 }
1259
1260 #[gpui::test]
1261 async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
1262 let executor = cx.executor();
1263 let app_state = init_test(cx);
1264
1265 cx.update(|cx| {
1266 SettingsStore::update_global(cx, |store, cx| {
1267 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1268 settings.session.restore_unsaved_buffers = false
1269 });
1270 });
1271 });
1272
1273 app_state
1274 .fs
1275 .as_fake()
1276 .insert_tree("/root", json!({"a": "hey"}))
1277 .await;
1278
1279 cx.update(|cx| {
1280 open_paths(
1281 &[PathBuf::from("/root/a")],
1282 app_state.clone(),
1283 workspace::OpenOptions::default(),
1284 cx,
1285 )
1286 })
1287 .await
1288 .unwrap();
1289 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1290
1291 // When opening the workspace, the window is not in a edited state.
1292 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
1293
1294 let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
1295 cx.update(|cx| window.read(cx).unwrap().is_edited())
1296 };
1297 let pane = window
1298 .read_with(cx, |workspace, _| workspace.active_pane().clone())
1299 .unwrap();
1300 let editor = window
1301 .read_with(cx, |workspace, cx| {
1302 workspace
1303 .active_item(cx)
1304 .unwrap()
1305 .downcast::<Editor>()
1306 .unwrap()
1307 })
1308 .unwrap();
1309
1310 assert!(!window_is_edited(window, cx));
1311
1312 // Editing a buffer marks the window as edited.
1313 window
1314 .update(cx, |_, cx| {
1315 editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
1316 })
1317 .unwrap();
1318
1319 assert!(window_is_edited(window, cx));
1320
1321 // Undoing the edit restores the window's edited state.
1322 window
1323 .update(cx, |_, cx| {
1324 editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
1325 })
1326 .unwrap();
1327 assert!(!window_is_edited(window, cx));
1328
1329 // Redoing the edit marks the window as edited again.
1330 window
1331 .update(cx, |_, cx| {
1332 editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
1333 })
1334 .unwrap();
1335 assert!(window_is_edited(window, cx));
1336
1337 // Closing the item restores the window's edited state.
1338 let close = window
1339 .update(cx, |_, cx| {
1340 pane.update(cx, |pane, cx| {
1341 drop(editor);
1342 pane.close_active_item(&Default::default(), cx).unwrap()
1343 })
1344 })
1345 .unwrap();
1346 executor.run_until_parked();
1347
1348 cx.simulate_prompt_answer(1);
1349 close.await.unwrap();
1350 assert!(!window_is_edited(window, cx));
1351
1352 // Advance the clock to ensure that the item has been serialized and dropped from the queue
1353 cx.executor().advance_clock(Duration::from_secs(1));
1354
1355 // Opening the buffer again doesn't impact the window's edited state.
1356 cx.update(|cx| {
1357 open_paths(
1358 &[PathBuf::from("/root/a")],
1359 app_state,
1360 workspace::OpenOptions::default(),
1361 cx,
1362 )
1363 })
1364 .await
1365 .unwrap();
1366 executor.run_until_parked();
1367
1368 window
1369 .update(cx, |workspace, cx| {
1370 let editor = workspace
1371 .active_item(cx)
1372 .unwrap()
1373 .downcast::<Editor>()
1374 .unwrap();
1375
1376 editor.update(cx, |editor, cx| {
1377 assert_eq!(editor.text(cx), "hey");
1378 });
1379 })
1380 .unwrap();
1381
1382 let editor = window
1383 .read_with(cx, |workspace, cx| {
1384 workspace
1385 .active_item(cx)
1386 .unwrap()
1387 .downcast::<Editor>()
1388 .unwrap()
1389 })
1390 .unwrap();
1391 assert!(!window_is_edited(window, cx));
1392
1393 // Editing the buffer marks the window as edited.
1394 window
1395 .update(cx, |_, cx| {
1396 editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
1397 })
1398 .unwrap();
1399 executor.run_until_parked();
1400 assert!(window_is_edited(window, cx));
1401
1402 // Ensure closing the window via the mouse gets preempted due to the
1403 // buffer having unsaved changes.
1404 assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
1405 executor.run_until_parked();
1406 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1407
1408 // The window is successfully closed after the user dismisses the prompt.
1409 cx.simulate_prompt_answer(1);
1410 executor.run_until_parked();
1411 assert_eq!(cx.update(|cx| cx.windows().len()), 0);
1412 }
1413
1414 #[gpui::test]
1415 async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
1416 let app_state = init_test(cx);
1417 app_state
1418 .fs
1419 .as_fake()
1420 .insert_tree("/root", json!({"a": "hey"}))
1421 .await;
1422
1423 cx.update(|cx| {
1424 open_paths(
1425 &[PathBuf::from("/root/a")],
1426 app_state.clone(),
1427 workspace::OpenOptions::default(),
1428 cx,
1429 )
1430 })
1431 .await
1432 .unwrap();
1433
1434 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1435
1436 // When opening the workspace, the window is not in a edited state.
1437 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
1438
1439 let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
1440 cx.update(|cx| window.read(cx).unwrap().is_edited())
1441 };
1442
1443 let editor = window
1444 .read_with(cx, |workspace, cx| {
1445 workspace
1446 .active_item(cx)
1447 .unwrap()
1448 .downcast::<Editor>()
1449 .unwrap()
1450 })
1451 .unwrap();
1452
1453 assert!(!window_is_edited(window, cx));
1454
1455 // Editing a buffer marks the window as edited.
1456 window
1457 .update(cx, |_, cx| {
1458 editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
1459 })
1460 .unwrap();
1461
1462 assert!(window_is_edited(window, cx));
1463 cx.run_until_parked();
1464
1465 // Advance the clock to make sure the workspace is serialized
1466 cx.executor().advance_clock(Duration::from_secs(1));
1467
1468 // When closing the window, no prompt shows up and the window is closed.
1469 // buffer having unsaved changes.
1470 assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
1471 cx.run_until_parked();
1472 assert_eq!(cx.update(|cx| cx.windows().len()), 0);
1473
1474 // When we now reopen the window, the edited state and the edited buffer are back
1475 cx.update(|cx| {
1476 open_paths(
1477 &[PathBuf::from("/root/a")],
1478 app_state.clone(),
1479 workspace::OpenOptions::default(),
1480 cx,
1481 )
1482 })
1483 .await
1484 .unwrap();
1485
1486 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1487 assert!(cx.update(|cx| cx.active_window().is_some()));
1488
1489 // When opening the workspace, the window is not in a edited state.
1490 let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
1491 assert!(window_is_edited(window, cx));
1492
1493 window
1494 .update(cx, |workspace, cx| {
1495 let editor = workspace
1496 .active_item(cx)
1497 .unwrap()
1498 .downcast::<editor::Editor>()
1499 .unwrap();
1500 editor.update(cx, |editor, cx| {
1501 assert_eq!(editor.text(cx), "EDIThey");
1502 assert!(editor.is_dirty(cx));
1503 });
1504
1505 editor
1506 })
1507 .unwrap();
1508 }
1509
1510 #[gpui::test]
1511 async fn test_new_empty_workspace(cx: &mut TestAppContext) {
1512 let app_state = init_test(cx);
1513 cx.update(|cx| {
1514 open_new(app_state.clone(), cx, |workspace, cx| {
1515 Editor::new_file(workspace, &Default::default(), cx)
1516 })
1517 })
1518 .await
1519 .unwrap();
1520 cx.run_until_parked();
1521
1522 let workspace = cx
1523 .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
1524 .unwrap();
1525
1526 let editor = workspace
1527 .update(cx, |workspace, cx| {
1528 let editor = workspace
1529 .active_item(cx)
1530 .unwrap()
1531 .downcast::<editor::Editor>()
1532 .unwrap();
1533 editor.update(cx, |editor, cx| {
1534 assert!(editor.text(cx).is_empty());
1535 assert!(!editor.is_dirty(cx));
1536 });
1537
1538 editor
1539 })
1540 .unwrap();
1541
1542 let save_task = workspace
1543 .update(cx, |workspace, cx| {
1544 workspace.save_active_item(SaveIntent::Save, cx)
1545 })
1546 .unwrap();
1547 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1548 cx.background_executor.run_until_parked();
1549 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
1550 save_task.await.unwrap();
1551 workspace
1552 .update(cx, |_, cx| {
1553 editor.update(cx, |editor, cx| {
1554 assert!(!editor.is_dirty(cx));
1555 assert_eq!(editor.title(cx), "the-new-name");
1556 });
1557 })
1558 .unwrap();
1559 }
1560
1561 #[gpui::test]
1562 async fn test_open_entry(cx: &mut TestAppContext) {
1563 let app_state = init_test(cx);
1564 app_state
1565 .fs
1566 .as_fake()
1567 .insert_tree(
1568 "/root",
1569 json!({
1570 "a": {
1571 "file1": "contents 1",
1572 "file2": "contents 2",
1573 "file3": "contents 3",
1574 },
1575 }),
1576 )
1577 .await;
1578
1579 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1580 project.update(cx, |project, _cx| {
1581 project.languages().add(markdown_language())
1582 });
1583 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1584 let workspace = window.root(cx).unwrap();
1585
1586 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1587 let file1 = entries[0].clone();
1588 let file2 = entries[1].clone();
1589 let file3 = entries[2].clone();
1590
1591 // Open the first entry
1592 let entry_1 = window
1593 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1594 .unwrap()
1595 .await
1596 .unwrap();
1597 cx.read(|cx| {
1598 let pane = workspace.read(cx).active_pane().read(cx);
1599 assert_eq!(
1600 pane.active_item().unwrap().project_path(cx),
1601 Some(file1.clone())
1602 );
1603 assert_eq!(pane.items_len(), 1);
1604 });
1605
1606 // Open the second entry
1607 window
1608 .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1609 .unwrap()
1610 .await
1611 .unwrap();
1612 cx.read(|cx| {
1613 let pane = workspace.read(cx).active_pane().read(cx);
1614 assert_eq!(
1615 pane.active_item().unwrap().project_path(cx),
1616 Some(file2.clone())
1617 );
1618 assert_eq!(pane.items_len(), 2);
1619 });
1620
1621 // Open the first entry again. The existing pane item is activated.
1622 let entry_1b = window
1623 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1624 .unwrap()
1625 .await
1626 .unwrap();
1627 assert_eq!(entry_1.item_id(), entry_1b.item_id());
1628
1629 cx.read(|cx| {
1630 let pane = workspace.read(cx).active_pane().read(cx);
1631 assert_eq!(
1632 pane.active_item().unwrap().project_path(cx),
1633 Some(file1.clone())
1634 );
1635 assert_eq!(pane.items_len(), 2);
1636 });
1637
1638 // Split the pane with the first entry, then open the second entry again.
1639 window
1640 .update(cx, |w, cx| {
1641 w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
1642 w.open_path(file2.clone(), None, true, cx)
1643 })
1644 .unwrap()
1645 .await
1646 .unwrap();
1647
1648 window
1649 .read_with(cx, |w, cx| {
1650 assert_eq!(
1651 w.active_pane()
1652 .read(cx)
1653 .active_item()
1654 .unwrap()
1655 .project_path(cx),
1656 Some(file2.clone())
1657 );
1658 })
1659 .unwrap();
1660
1661 // Open the third entry twice concurrently. Only one pane item is added.
1662 let (t1, t2) = window
1663 .update(cx, |w, cx| {
1664 (
1665 w.open_path(file3.clone(), None, true, cx),
1666 w.open_path(file3.clone(), None, true, cx),
1667 )
1668 })
1669 .unwrap();
1670 t1.await.unwrap();
1671 t2.await.unwrap();
1672 cx.read(|cx| {
1673 let pane = workspace.read(cx).active_pane().read(cx);
1674 assert_eq!(
1675 pane.active_item().unwrap().project_path(cx),
1676 Some(file3.clone())
1677 );
1678 let pane_entries = pane
1679 .items()
1680 .map(|i| i.project_path(cx).unwrap())
1681 .collect::<Vec<_>>();
1682 assert_eq!(pane_entries, &[file1, file2, file3]);
1683 });
1684 }
1685
1686 #[gpui::test]
1687 async fn test_open_paths(cx: &mut TestAppContext) {
1688 let app_state = init_test(cx);
1689
1690 app_state
1691 .fs
1692 .as_fake()
1693 .insert_tree(
1694 "/",
1695 json!({
1696 "dir1": {
1697 "a.txt": ""
1698 },
1699 "dir2": {
1700 "b.txt": ""
1701 },
1702 "dir3": {
1703 "c.txt": ""
1704 },
1705 "d.txt": ""
1706 }),
1707 )
1708 .await;
1709
1710 cx.update(|cx| {
1711 open_paths(
1712 &[PathBuf::from("/dir1/")],
1713 app_state,
1714 workspace::OpenOptions::default(),
1715 cx,
1716 )
1717 })
1718 .await
1719 .unwrap();
1720 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1721 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
1722 let workspace = window.root(cx).unwrap();
1723
1724 #[track_caller]
1725 fn assert_project_panel_selection(
1726 workspace: &Workspace,
1727 expected_worktree_path: &Path,
1728 expected_entry_path: &Path,
1729 cx: &AppContext,
1730 ) {
1731 let project_panel = [
1732 workspace.left_dock().read(cx).panel::<ProjectPanel>(),
1733 workspace.right_dock().read(cx).panel::<ProjectPanel>(),
1734 workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
1735 ]
1736 .into_iter()
1737 .find_map(std::convert::identity)
1738 .expect("found no project panels")
1739 .read(cx);
1740 let (selected_worktree, selected_entry) = project_panel
1741 .selected_entry(cx)
1742 .expect("project panel should have a selected entry");
1743 assert_eq!(
1744 selected_worktree.abs_path().as_ref(),
1745 expected_worktree_path,
1746 "Unexpected project panel selected worktree path"
1747 );
1748 assert_eq!(
1749 selected_entry.path.as_ref(),
1750 expected_entry_path,
1751 "Unexpected project panel selected entry path"
1752 );
1753 }
1754
1755 // Open a file within an existing worktree.
1756 window
1757 .update(cx, |view, cx| {
1758 view.open_paths(vec!["/dir1/a.txt".into()], OpenVisible::All, None, cx)
1759 })
1760 .unwrap()
1761 .await;
1762 cx.read(|cx| {
1763 let workspace = workspace.read(cx);
1764 assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx);
1765 assert_eq!(
1766 workspace
1767 .active_pane()
1768 .read(cx)
1769 .active_item()
1770 .unwrap()
1771 .act_as::<Editor>(cx)
1772 .unwrap()
1773 .read(cx)
1774 .title(cx),
1775 "a.txt"
1776 );
1777 });
1778
1779 // Open a file outside of any existing worktree.
1780 window
1781 .update(cx, |view, cx| {
1782 view.open_paths(vec!["/dir2/b.txt".into()], OpenVisible::All, None, cx)
1783 })
1784 .unwrap()
1785 .await;
1786 cx.read(|cx| {
1787 let workspace = workspace.read(cx);
1788 assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx);
1789 let worktree_roots = workspace
1790 .worktrees(cx)
1791 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1792 .collect::<HashSet<_>>();
1793 assert_eq!(
1794 worktree_roots,
1795 vec!["/dir1", "/dir2/b.txt"]
1796 .into_iter()
1797 .map(Path::new)
1798 .collect(),
1799 );
1800 assert_eq!(
1801 workspace
1802 .active_pane()
1803 .read(cx)
1804 .active_item()
1805 .unwrap()
1806 .act_as::<Editor>(cx)
1807 .unwrap()
1808 .read(cx)
1809 .title(cx),
1810 "b.txt"
1811 );
1812 });
1813
1814 // Ensure opening a directory and one of its children only adds one worktree.
1815 window
1816 .update(cx, |view, cx| {
1817 view.open_paths(
1818 vec!["/dir3".into(), "/dir3/c.txt".into()],
1819 OpenVisible::All,
1820 None,
1821 cx,
1822 )
1823 })
1824 .unwrap()
1825 .await;
1826 cx.read(|cx| {
1827 let workspace = workspace.read(cx);
1828 assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx);
1829 let worktree_roots = workspace
1830 .worktrees(cx)
1831 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1832 .collect::<HashSet<_>>();
1833 assert_eq!(
1834 worktree_roots,
1835 vec!["/dir1", "/dir2/b.txt", "/dir3"]
1836 .into_iter()
1837 .map(Path::new)
1838 .collect(),
1839 );
1840 assert_eq!(
1841 workspace
1842 .active_pane()
1843 .read(cx)
1844 .active_item()
1845 .unwrap()
1846 .act_as::<Editor>(cx)
1847 .unwrap()
1848 .read(cx)
1849 .title(cx),
1850 "c.txt"
1851 );
1852 });
1853
1854 // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
1855 window
1856 .update(cx, |view, cx| {
1857 view.open_paths(vec!["/d.txt".into()], OpenVisible::None, None, cx)
1858 })
1859 .unwrap()
1860 .await;
1861 cx.read(|cx| {
1862 let workspace = workspace.read(cx);
1863 assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx);
1864 let worktree_roots = workspace
1865 .worktrees(cx)
1866 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1867 .collect::<HashSet<_>>();
1868 assert_eq!(
1869 worktree_roots,
1870 vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
1871 .into_iter()
1872 .map(Path::new)
1873 .collect(),
1874 );
1875
1876 let visible_worktree_roots = workspace
1877 .visible_worktrees(cx)
1878 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1879 .collect::<HashSet<_>>();
1880 assert_eq!(
1881 visible_worktree_roots,
1882 vec!["/dir1", "/dir2/b.txt", "/dir3"]
1883 .into_iter()
1884 .map(Path::new)
1885 .collect(),
1886 );
1887
1888 assert_eq!(
1889 workspace
1890 .active_pane()
1891 .read(cx)
1892 .active_item()
1893 .unwrap()
1894 .act_as::<Editor>(cx)
1895 .unwrap()
1896 .read(cx)
1897 .title(cx),
1898 "d.txt"
1899 );
1900 });
1901 }
1902
1903 #[gpui::test]
1904 async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
1905 let app_state = init_test(cx);
1906 cx.update(|cx| {
1907 cx.update_global::<SettingsStore, _>(|store, cx| {
1908 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1909 project_settings.file_scan_exclusions =
1910 Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
1911 });
1912 });
1913 });
1914 app_state
1915 .fs
1916 .as_fake()
1917 .insert_tree(
1918 "/root",
1919 json!({
1920 ".gitignore": "ignored_dir\n",
1921 ".git": {
1922 "HEAD": "ref: refs/heads/main",
1923 },
1924 "regular_dir": {
1925 "file": "regular file contents",
1926 },
1927 "ignored_dir": {
1928 "ignored_subdir": {
1929 "file": "ignored subfile contents",
1930 },
1931 "file": "ignored file contents",
1932 },
1933 "excluded_dir": {
1934 "file": "excluded file contents",
1935 "ignored_subdir": {
1936 "file": "ignored subfile contents",
1937 },
1938 },
1939 }),
1940 )
1941 .await;
1942
1943 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1944 project.update(cx, |project, _cx| {
1945 project.languages().add(markdown_language())
1946 });
1947 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1948 let workspace = window.root(cx).unwrap();
1949
1950 let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
1951 let paths_to_open = [
1952 Path::new("/root/excluded_dir/file").to_path_buf(),
1953 Path::new("/root/.git/HEAD").to_path_buf(),
1954 Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
1955 ];
1956 let (opened_workspace, new_items) = cx
1957 .update(|cx| {
1958 workspace::open_paths(
1959 &paths_to_open,
1960 app_state,
1961 workspace::OpenOptions::default(),
1962 cx,
1963 )
1964 })
1965 .await
1966 .unwrap();
1967
1968 assert_eq!(
1969 opened_workspace.root_view(cx).unwrap().entity_id(),
1970 workspace.entity_id(),
1971 "Excluded files in subfolders of a workspace root should be opened in the workspace"
1972 );
1973 let mut opened_paths = cx.read(|cx| {
1974 assert_eq!(
1975 new_items.len(),
1976 paths_to_open.len(),
1977 "Expect to get the same number of opened items as submitted paths to open"
1978 );
1979 new_items
1980 .iter()
1981 .zip(paths_to_open.iter())
1982 .map(|(i, path)| {
1983 match i {
1984 Some(Ok(i)) => {
1985 Some(i.project_path(cx).map(|p| p.path.display().to_string()))
1986 }
1987 Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
1988 None => None,
1989 }
1990 .flatten()
1991 })
1992 .collect::<Vec<_>>()
1993 });
1994 opened_paths.sort();
1995 assert_eq!(
1996 opened_paths,
1997 vec![
1998 None,
1999 Some(".git/HEAD".to_string()),
2000 Some("excluded_dir/file".to_string()),
2001 ],
2002 "Excluded files should get opened, excluded dir should not get opened"
2003 );
2004
2005 let entries = cx.read(|cx| workspace.file_project_paths(cx));
2006 assert_eq!(
2007 initial_entries, entries,
2008 "Workspace entries should not change after opening excluded files and directories paths"
2009 );
2010
2011 cx.read(|cx| {
2012 let pane = workspace.read(cx).active_pane().read(cx);
2013 let mut opened_buffer_paths = pane
2014 .items()
2015 .map(|i| {
2016 i.project_path(cx)
2017 .expect("all excluded files that got open should have a path")
2018 .path
2019 .display()
2020 .to_string()
2021 })
2022 .collect::<Vec<_>>();
2023 opened_buffer_paths.sort();
2024 assert_eq!(
2025 opened_buffer_paths,
2026 vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()],
2027 "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
2028 );
2029 });
2030 }
2031
2032 #[gpui::test]
2033 async fn test_save_conflicting_item(cx: &mut TestAppContext) {
2034 let app_state = init_test(cx);
2035 app_state
2036 .fs
2037 .as_fake()
2038 .insert_tree("/root", json!({ "a.txt": "" }))
2039 .await;
2040
2041 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
2042 project.update(cx, |project, _cx| {
2043 project.languages().add(markdown_language())
2044 });
2045 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
2046 let workspace = window.root(cx).unwrap();
2047
2048 // Open a file within an existing worktree.
2049 window
2050 .update(cx, |view, cx| {
2051 view.open_paths(
2052 vec![PathBuf::from("/root/a.txt")],
2053 OpenVisible::All,
2054 None,
2055 cx,
2056 )
2057 })
2058 .unwrap()
2059 .await;
2060 let editor = cx.read(|cx| {
2061 let pane = workspace.read(cx).active_pane().read(cx);
2062 let item = pane.active_item().unwrap();
2063 item.downcast::<Editor>().unwrap()
2064 });
2065
2066 window
2067 .update(cx, |_, cx| {
2068 editor.update(cx, |editor, cx| editor.handle_input("x", cx));
2069 })
2070 .unwrap();
2071
2072 app_state
2073 .fs
2074 .as_fake()
2075 .insert_file("/root/a.txt", b"changed".to_vec())
2076 .await;
2077
2078 cx.run_until_parked();
2079 cx.read(|cx| assert!(editor.is_dirty(cx)));
2080 cx.read(|cx| assert!(editor.has_conflict(cx)));
2081
2082 let save_task = window
2083 .update(cx, |workspace, cx| {
2084 workspace.save_active_item(SaveIntent::Save, cx)
2085 })
2086 .unwrap();
2087 cx.background_executor.run_until_parked();
2088 cx.simulate_prompt_answer(0);
2089 save_task.await.unwrap();
2090 window
2091 .update(cx, |_, cx| {
2092 editor.update(cx, |editor, cx| {
2093 assert!(!editor.is_dirty(cx));
2094 assert!(!editor.has_conflict(cx));
2095 });
2096 })
2097 .unwrap();
2098 }
2099
2100 #[gpui::test]
2101 async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
2102 let app_state = init_test(cx);
2103 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
2104
2105 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
2106 project.update(cx, |project, _| {
2107 project.languages().add(markdown_language());
2108 project.languages().add(rust_lang());
2109 });
2110 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
2111 let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
2112
2113 // Create a new untitled buffer
2114 cx.dispatch_action(window.into(), NewFile);
2115 let editor = window
2116 .read_with(cx, |workspace, cx| {
2117 workspace
2118 .active_item(cx)
2119 .unwrap()
2120 .downcast::<Editor>()
2121 .unwrap()
2122 })
2123 .unwrap();
2124
2125 window
2126 .update(cx, |_, cx| {
2127 editor.update(cx, |editor, cx| {
2128 assert!(!editor.is_dirty(cx));
2129 assert_eq!(editor.title(cx), "untitled");
2130 assert!(Arc::ptr_eq(
2131 &editor.buffer().read(cx).language_at(0, cx).unwrap(),
2132 &languages::PLAIN_TEXT
2133 ));
2134 editor.handle_input("hi", cx);
2135 assert!(editor.is_dirty(cx));
2136 });
2137 })
2138 .unwrap();
2139
2140 // Save the buffer. This prompts for a filename.
2141 let save_task = window
2142 .update(cx, |workspace, cx| {
2143 workspace.save_active_item(SaveIntent::Save, cx)
2144 })
2145 .unwrap();
2146 cx.background_executor.run_until_parked();
2147 cx.simulate_new_path_selection(|parent_dir| {
2148 assert_eq!(parent_dir, Path::new("/root"));
2149 Some(parent_dir.join("the-new-name.rs"))
2150 });
2151 cx.read(|cx| {
2152 assert!(editor.is_dirty(cx));
2153 assert_eq!(editor.read(cx).title(cx), "untitled");
2154 });
2155
2156 // When the save completes, the buffer's title is updated and the language is assigned based
2157 // on the path.
2158 save_task.await.unwrap();
2159 window
2160 .update(cx, |_, cx| {
2161 editor.update(cx, |editor, cx| {
2162 assert!(!editor.is_dirty(cx));
2163 assert_eq!(editor.title(cx), "the-new-name.rs");
2164 assert_eq!(
2165 editor
2166 .buffer()
2167 .read(cx)
2168 .language_at(0, cx)
2169 .unwrap()
2170 .name()
2171 .as_ref(),
2172 "Rust"
2173 );
2174 });
2175 })
2176 .unwrap();
2177
2178 // Edit the file and save it again. This time, there is no filename prompt.
2179 window
2180 .update(cx, |_, cx| {
2181 editor.update(cx, |editor, cx| {
2182 editor.handle_input(" there", cx);
2183 assert!(editor.is_dirty(cx));
2184 });
2185 })
2186 .unwrap();
2187
2188 let save_task = window
2189 .update(cx, |workspace, cx| {
2190 workspace.save_active_item(SaveIntent::Save, cx)
2191 })
2192 .unwrap();
2193 save_task.await.unwrap();
2194
2195 assert!(!cx.did_prompt_for_new_path());
2196 window
2197 .update(cx, |_, cx| {
2198 editor.update(cx, |editor, cx| {
2199 assert!(!editor.is_dirty(cx));
2200 assert_eq!(editor.title(cx), "the-new-name.rs")
2201 });
2202 })
2203 .unwrap();
2204
2205 // Open the same newly-created file in another pane item. The new editor should reuse
2206 // the same buffer.
2207 cx.dispatch_action(window.into(), NewFile);
2208 window
2209 .update(cx, |workspace, cx| {
2210 workspace.split_and_clone(
2211 workspace.active_pane().clone(),
2212 SplitDirection::Right,
2213 cx,
2214 );
2215 workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
2216 })
2217 .unwrap()
2218 .await
2219 .unwrap();
2220 let editor2 = window
2221 .update(cx, |workspace, cx| {
2222 workspace
2223 .active_item(cx)
2224 .unwrap()
2225 .downcast::<Editor>()
2226 .unwrap()
2227 })
2228 .unwrap();
2229 cx.read(|cx| {
2230 assert_eq!(
2231 editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
2232 editor.read(cx).buffer().read(cx).as_singleton().unwrap()
2233 );
2234 })
2235 }
2236
2237 #[gpui::test]
2238 async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
2239 let app_state = init_test(cx);
2240 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
2241
2242 let project = Project::test(app_state.fs.clone(), [], cx).await;
2243 project.update(cx, |project, _| {
2244 project.languages().add(rust_lang());
2245 project.languages().add(markdown_language());
2246 });
2247 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
2248
2249 // Create a new untitled buffer
2250 cx.dispatch_action(window.into(), NewFile);
2251 let editor = window
2252 .read_with(cx, |workspace, cx| {
2253 workspace
2254 .active_item(cx)
2255 .unwrap()
2256 .downcast::<Editor>()
2257 .unwrap()
2258 })
2259 .unwrap();
2260 window
2261 .update(cx, |_, cx| {
2262 editor.update(cx, |editor, cx| {
2263 assert!(Arc::ptr_eq(
2264 &editor.buffer().read(cx).language_at(0, cx).unwrap(),
2265 &languages::PLAIN_TEXT
2266 ));
2267 editor.handle_input("hi", cx);
2268 assert!(editor.is_dirty(cx));
2269 });
2270 })
2271 .unwrap();
2272
2273 // Save the buffer. This prompts for a filename.
2274 let save_task = window
2275 .update(cx, |workspace, cx| {
2276 workspace.save_active_item(SaveIntent::Save, cx)
2277 })
2278 .unwrap();
2279 cx.background_executor.run_until_parked();
2280 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
2281 save_task.await.unwrap();
2282 // The buffer is not dirty anymore and the language is assigned based on the path.
2283 window
2284 .update(cx, |_, cx| {
2285 editor.update(cx, |editor, cx| {
2286 assert!(!editor.is_dirty(cx));
2287 assert_eq!(
2288 editor
2289 .buffer()
2290 .read(cx)
2291 .language_at(0, cx)
2292 .unwrap()
2293 .name()
2294 .as_ref(),
2295 "Rust"
2296 )
2297 });
2298 })
2299 .unwrap();
2300 }
2301
2302 #[gpui::test]
2303 async fn test_pane_actions(cx: &mut TestAppContext) {
2304 let app_state = init_test(cx);
2305 app_state
2306 .fs
2307 .as_fake()
2308 .insert_tree(
2309 "/root",
2310 json!({
2311 "a": {
2312 "file1": "contents 1",
2313 "file2": "contents 2",
2314 "file3": "contents 3",
2315 },
2316 }),
2317 )
2318 .await;
2319
2320 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
2321 project.update(cx, |project, _cx| {
2322 project.languages().add(markdown_language())
2323 });
2324 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
2325 let workspace = window.root(cx).unwrap();
2326
2327 let entries = cx.read(|cx| workspace.file_project_paths(cx));
2328 let file1 = entries[0].clone();
2329
2330 let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
2331
2332 window
2333 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
2334 .unwrap()
2335 .await
2336 .unwrap();
2337
2338 let (editor_1, buffer) = window
2339 .update(cx, |_, cx| {
2340 pane_1.update(cx, |pane_1, cx| {
2341 let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
2342 assert_eq!(editor.project_path(cx), Some(file1.clone()));
2343 let buffer = editor.update(cx, |editor, cx| {
2344 editor.insert("dirt", cx);
2345 editor.buffer().downgrade()
2346 });
2347 (editor.downgrade(), buffer)
2348 })
2349 })
2350 .unwrap();
2351
2352 cx.dispatch_action(window.into(), pane::SplitRight);
2353 let editor_2 = cx.update(|cx| {
2354 let pane_2 = workspace.read(cx).active_pane().clone();
2355 assert_ne!(pane_1, pane_2);
2356
2357 let pane2_item = pane_2.read(cx).active_item().unwrap();
2358 assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
2359
2360 pane2_item.downcast::<Editor>().unwrap().downgrade()
2361 });
2362 cx.dispatch_action(
2363 window.into(),
2364 workspace::CloseActiveItem { save_intent: None },
2365 );
2366
2367 cx.background_executor.run_until_parked();
2368 window
2369 .read_with(cx, |workspace, _| {
2370 assert_eq!(workspace.panes().len(), 1);
2371 assert_eq!(workspace.active_pane(), &pane_1);
2372 })
2373 .unwrap();
2374
2375 cx.dispatch_action(
2376 window.into(),
2377 workspace::CloseActiveItem { save_intent: None },
2378 );
2379 cx.background_executor.run_until_parked();
2380 cx.simulate_prompt_answer(1);
2381 cx.background_executor.run_until_parked();
2382
2383 window
2384 .update(cx, |workspace, cx| {
2385 assert_eq!(workspace.panes().len(), 1);
2386 assert!(workspace.active_item(cx).is_none());
2387 })
2388 .unwrap();
2389
2390 cx.run_until_parked();
2391 editor_1.assert_released();
2392 editor_2.assert_released();
2393 buffer.assert_released();
2394 }
2395
2396 #[gpui::test]
2397 async fn test_navigation(cx: &mut TestAppContext) {
2398 let app_state = init_test(cx);
2399 app_state
2400 .fs
2401 .as_fake()
2402 .insert_tree(
2403 "/root",
2404 json!({
2405 "a": {
2406 "file1": "contents 1\n".repeat(20),
2407 "file2": "contents 2\n".repeat(20),
2408 "file3": "contents 3\n".repeat(20),
2409 },
2410 }),
2411 )
2412 .await;
2413
2414 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
2415 project.update(cx, |project, _cx| {
2416 project.languages().add(markdown_language())
2417 });
2418 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2419 let pane = workspace
2420 .read_with(cx, |workspace, _| workspace.active_pane().clone())
2421 .unwrap();
2422
2423 let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
2424 let file1 = entries[0].clone();
2425 let file2 = entries[1].clone();
2426 let file3 = entries[2].clone();
2427
2428 let editor1 = workspace
2429 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
2430 .unwrap()
2431 .await
2432 .unwrap()
2433 .downcast::<Editor>()
2434 .unwrap();
2435 workspace
2436 .update(cx, |_, cx| {
2437 editor1.update(cx, |editor, cx| {
2438 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
2439 s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0)
2440 ..DisplayPoint::new(DisplayRow(10), 0)])
2441 });
2442 });
2443 })
2444 .unwrap();
2445
2446 let editor2 = workspace
2447 .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
2448 .unwrap()
2449 .await
2450 .unwrap()
2451 .downcast::<Editor>()
2452 .unwrap();
2453 let editor3 = workspace
2454 .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
2455 .unwrap()
2456 .await
2457 .unwrap()
2458 .downcast::<Editor>()
2459 .unwrap();
2460
2461 workspace
2462 .update(cx, |_, cx| {
2463 editor3.update(cx, |editor, cx| {
2464 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
2465 s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
2466 ..DisplayPoint::new(DisplayRow(12), 0)])
2467 });
2468 editor.newline(&Default::default(), cx);
2469 editor.newline(&Default::default(), cx);
2470 editor.move_down(&Default::default(), cx);
2471 editor.move_down(&Default::default(), cx);
2472 editor.save(true, project.clone(), cx)
2473 })
2474 })
2475 .unwrap()
2476 .await
2477 .unwrap();
2478 workspace
2479 .update(cx, |_, cx| {
2480 editor3.update(cx, |editor, cx| {
2481 editor.set_scroll_position(point(0., 12.5), cx)
2482 });
2483 })
2484 .unwrap();
2485 assert_eq!(
2486 active_location(&workspace, cx),
2487 (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
2488 );
2489
2490 workspace
2491 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2492 .unwrap()
2493 .await
2494 .unwrap();
2495 assert_eq!(
2496 active_location(&workspace, cx),
2497 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2498 );
2499
2500 workspace
2501 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2502 .unwrap()
2503 .await
2504 .unwrap();
2505 assert_eq!(
2506 active_location(&workspace, cx),
2507 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2508 );
2509
2510 workspace
2511 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2512 .unwrap()
2513 .await
2514 .unwrap();
2515 assert_eq!(
2516 active_location(&workspace, cx),
2517 (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
2518 );
2519
2520 workspace
2521 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2522 .unwrap()
2523 .await
2524 .unwrap();
2525 assert_eq!(
2526 active_location(&workspace, cx),
2527 (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2528 );
2529
2530 // Go back one more time and ensure we don't navigate past the first item in the history.
2531 workspace
2532 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2533 .unwrap()
2534 .await
2535 .unwrap();
2536 assert_eq!(
2537 active_location(&workspace, cx),
2538 (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2539 );
2540
2541 workspace
2542 .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2543 .unwrap()
2544 .await
2545 .unwrap();
2546 assert_eq!(
2547 active_location(&workspace, cx),
2548 (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
2549 );
2550
2551 workspace
2552 .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2553 .unwrap()
2554 .await
2555 .unwrap();
2556 assert_eq!(
2557 active_location(&workspace, cx),
2558 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2559 );
2560
2561 // Go forward to an item that has been closed, ensuring it gets re-opened at the same
2562 // location.
2563 workspace
2564 .update(cx, |_, cx| {
2565 pane.update(cx, |pane, cx| {
2566 let editor3_id = editor3.entity_id();
2567 drop(editor3);
2568 pane.close_item_by_id(editor3_id, SaveIntent::Close, cx)
2569 })
2570 })
2571 .unwrap()
2572 .await
2573 .unwrap();
2574 workspace
2575 .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2576 .unwrap()
2577 .await
2578 .unwrap();
2579 assert_eq!(
2580 active_location(&workspace, cx),
2581 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2582 );
2583
2584 workspace
2585 .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2586 .unwrap()
2587 .await
2588 .unwrap();
2589 assert_eq!(
2590 active_location(&workspace, cx),
2591 (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
2592 );
2593
2594 workspace
2595 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2596 .unwrap()
2597 .await
2598 .unwrap();
2599 assert_eq!(
2600 active_location(&workspace, cx),
2601 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2602 );
2603
2604 // Go back to an item that has been closed and removed from disk
2605 workspace
2606 .update(cx, |_, cx| {
2607 pane.update(cx, |pane, cx| {
2608 let editor2_id = editor2.entity_id();
2609 drop(editor2);
2610 pane.close_item_by_id(editor2_id, SaveIntent::Close, cx)
2611 })
2612 })
2613 .unwrap()
2614 .await
2615 .unwrap();
2616 app_state
2617 .fs
2618 .remove_file(Path::new("/root/a/file2"), Default::default())
2619 .await
2620 .unwrap();
2621 cx.background_executor.run_until_parked();
2622
2623 workspace
2624 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2625 .unwrap()
2626 .await
2627 .unwrap();
2628 assert_eq!(
2629 active_location(&workspace, cx),
2630 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2631 );
2632 workspace
2633 .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2634 .unwrap()
2635 .await
2636 .unwrap();
2637 assert_eq!(
2638 active_location(&workspace, cx),
2639 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2640 );
2641
2642 // Modify file to collapse multiple nav history entries into the same location.
2643 // Ensure we don't visit the same location twice when navigating.
2644 workspace
2645 .update(cx, |_, cx| {
2646 editor1.update(cx, |editor, cx| {
2647 editor.change_selections(None, cx, |s| {
2648 s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0)
2649 ..DisplayPoint::new(DisplayRow(15), 0)])
2650 })
2651 });
2652 })
2653 .unwrap();
2654 for _ in 0..5 {
2655 workspace
2656 .update(cx, |_, cx| {
2657 editor1.update(cx, |editor, cx| {
2658 editor.change_selections(None, cx, |s| {
2659 s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0)
2660 ..DisplayPoint::new(DisplayRow(3), 0)])
2661 });
2662 });
2663 })
2664 .unwrap();
2665
2666 workspace
2667 .update(cx, |_, cx| {
2668 editor1.update(cx, |editor, cx| {
2669 editor.change_selections(None, cx, |s| {
2670 s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
2671 ..DisplayPoint::new(DisplayRow(13), 0)])
2672 })
2673 });
2674 })
2675 .unwrap();
2676 }
2677 workspace
2678 .update(cx, |_, cx| {
2679 editor1.update(cx, |editor, cx| {
2680 editor.transact(cx, |editor, cx| {
2681 editor.change_selections(None, cx, |s| {
2682 s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
2683 ..DisplayPoint::new(DisplayRow(14), 0)])
2684 });
2685 editor.insert("", cx);
2686 })
2687 });
2688 })
2689 .unwrap();
2690
2691 workspace
2692 .update(cx, |_, cx| {
2693 editor1.update(cx, |editor, cx| {
2694 editor.change_selections(None, cx, |s| {
2695 s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0)
2696 ..DisplayPoint::new(DisplayRow(1), 0)])
2697 })
2698 });
2699 })
2700 .unwrap();
2701 workspace
2702 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2703 .unwrap()
2704 .await
2705 .unwrap();
2706 assert_eq!(
2707 active_location(&workspace, cx),
2708 (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.)
2709 );
2710 workspace
2711 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2712 .unwrap()
2713 .await
2714 .unwrap();
2715 assert_eq!(
2716 active_location(&workspace, cx),
2717 (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.)
2718 );
2719
2720 fn active_location(
2721 workspace: &WindowHandle<Workspace>,
2722 cx: &mut TestAppContext,
2723 ) -> (ProjectPath, DisplayPoint, f32) {
2724 workspace
2725 .update(cx, |workspace, cx| {
2726 let item = workspace.active_item(cx).unwrap();
2727 let editor = item.downcast::<Editor>().unwrap();
2728 let (selections, scroll_position) = editor.update(cx, |editor, cx| {
2729 (
2730 editor.selections.display_ranges(cx),
2731 editor.scroll_position(cx),
2732 )
2733 });
2734 (
2735 item.project_path(cx).unwrap(),
2736 selections[0].start,
2737 scroll_position.y,
2738 )
2739 })
2740 .unwrap()
2741 }
2742 }
2743
2744 #[gpui::test]
2745 async fn test_reopening_closed_items(cx: &mut TestAppContext) {
2746 let app_state = init_test(cx);
2747 app_state
2748 .fs
2749 .as_fake()
2750 .insert_tree(
2751 "/root",
2752 json!({
2753 "a": {
2754 "file1": "",
2755 "file2": "",
2756 "file3": "",
2757 "file4": "",
2758 },
2759 }),
2760 )
2761 .await;
2762
2763 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
2764 project.update(cx, |project, _cx| {
2765 project.languages().add(markdown_language())
2766 });
2767 let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
2768 let pane = workspace
2769 .read_with(cx, |workspace, _| workspace.active_pane().clone())
2770 .unwrap();
2771
2772 let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
2773 let file1 = entries[0].clone();
2774 let file2 = entries[1].clone();
2775 let file3 = entries[2].clone();
2776 let file4 = entries[3].clone();
2777
2778 let file1_item_id = workspace
2779 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
2780 .unwrap()
2781 .await
2782 .unwrap()
2783 .item_id();
2784 let file2_item_id = workspace
2785 .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
2786 .unwrap()
2787 .await
2788 .unwrap()
2789 .item_id();
2790 let file3_item_id = workspace
2791 .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
2792 .unwrap()
2793 .await
2794 .unwrap()
2795 .item_id();
2796 let file4_item_id = workspace
2797 .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
2798 .unwrap()
2799 .await
2800 .unwrap()
2801 .item_id();
2802 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2803
2804 // Close all the pane items in some arbitrary order.
2805 workspace
2806 .update(cx, |_, cx| {
2807 pane.update(cx, |pane, cx| {
2808 pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx)
2809 })
2810 })
2811 .unwrap()
2812 .await
2813 .unwrap();
2814 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2815
2816 workspace
2817 .update(cx, |_, cx| {
2818 pane.update(cx, |pane, cx| {
2819 pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx)
2820 })
2821 })
2822 .unwrap()
2823 .await
2824 .unwrap();
2825 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2826
2827 workspace
2828 .update(cx, |_, cx| {
2829 pane.update(cx, |pane, cx| {
2830 pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx)
2831 })
2832 })
2833 .unwrap()
2834 .await
2835 .unwrap();
2836 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2837 workspace
2838 .update(cx, |_, cx| {
2839 pane.update(cx, |pane, cx| {
2840 pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx)
2841 })
2842 })
2843 .unwrap()
2844 .await
2845 .unwrap();
2846
2847 assert_eq!(active_path(&workspace, cx), None);
2848
2849 // Reopen all the closed items, ensuring they are reopened in the same order
2850 // in which they were closed.
2851 workspace
2852 .update(cx, Workspace::reopen_closed_item)
2853 .unwrap()
2854 .await
2855 .unwrap();
2856 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2857
2858 workspace
2859 .update(cx, Workspace::reopen_closed_item)
2860 .unwrap()
2861 .await
2862 .unwrap();
2863 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
2864
2865 workspace
2866 .update(cx, Workspace::reopen_closed_item)
2867 .unwrap()
2868 .await
2869 .unwrap();
2870 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2871
2872 workspace
2873 .update(cx, Workspace::reopen_closed_item)
2874 .unwrap()
2875 .await
2876 .unwrap();
2877 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2878
2879 // Reopening past the last closed item is a no-op.
2880 workspace
2881 .update(cx, Workspace::reopen_closed_item)
2882 .unwrap()
2883 .await
2884 .unwrap();
2885 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2886
2887 // Reopening closed items doesn't interfere with navigation history.
2888 workspace
2889 .update(cx, |workspace, cx| {
2890 workspace.go_back(workspace.active_pane().downgrade(), cx)
2891 })
2892 .unwrap()
2893 .await
2894 .unwrap();
2895 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2896
2897 workspace
2898 .update(cx, |workspace, cx| {
2899 workspace.go_back(workspace.active_pane().downgrade(), cx)
2900 })
2901 .unwrap()
2902 .await
2903 .unwrap();
2904 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
2905
2906 workspace
2907 .update(cx, |workspace, cx| {
2908 workspace.go_back(workspace.active_pane().downgrade(), cx)
2909 })
2910 .unwrap()
2911 .await
2912 .unwrap();
2913 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2914
2915 workspace
2916 .update(cx, |workspace, cx| {
2917 workspace.go_back(workspace.active_pane().downgrade(), cx)
2918 })
2919 .unwrap()
2920 .await
2921 .unwrap();
2922 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2923
2924 workspace
2925 .update(cx, |workspace, cx| {
2926 workspace.go_back(workspace.active_pane().downgrade(), cx)
2927 })
2928 .unwrap()
2929 .await
2930 .unwrap();
2931 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2932
2933 workspace
2934 .update(cx, |workspace, cx| {
2935 workspace.go_back(workspace.active_pane().downgrade(), cx)
2936 })
2937 .unwrap()
2938 .await
2939 .unwrap();
2940 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
2941
2942 workspace
2943 .update(cx, |workspace, cx| {
2944 workspace.go_back(workspace.active_pane().downgrade(), cx)
2945 })
2946 .unwrap()
2947 .await
2948 .unwrap();
2949 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2950
2951 workspace
2952 .update(cx, |workspace, cx| {
2953 workspace.go_back(workspace.active_pane().downgrade(), cx)
2954 })
2955 .unwrap()
2956 .await
2957 .unwrap();
2958 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2959
2960 fn active_path(
2961 workspace: &WindowHandle<Workspace>,
2962 cx: &TestAppContext,
2963 ) -> Option<ProjectPath> {
2964 workspace
2965 .read_with(cx, |workspace, cx| {
2966 let item = workspace.active_item(cx)?;
2967 item.project_path(cx)
2968 })
2969 .unwrap()
2970 }
2971 }
2972
2973 fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
2974 cx.update(|cx| {
2975 let app_state = AppState::test(cx);
2976
2977 theme::init(theme::LoadThemes::JustBase, cx);
2978 client::init(&app_state.client, cx);
2979 language::init(cx);
2980 workspace::init(app_state.clone(), cx);
2981 welcome::init(cx);
2982 Project::init_settings(cx);
2983 app_state
2984 })
2985 }
2986
2987 #[gpui::test]
2988 async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
2989 let executor = cx.executor();
2990 let app_state = init_keymap_test(cx);
2991 let project = Project::test(app_state.fs.clone(), [], cx).await;
2992 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2993
2994 actions!(test1, [A, B]);
2995 // From the Atom keymap
2996 use workspace::ActivatePreviousPane;
2997 // From the JetBrains keymap
2998 use workspace::ActivatePrevItem;
2999
3000 app_state
3001 .fs
3002 .save(
3003 "/settings.json".as_ref(),
3004 &r#"
3005 {
3006 "base_keymap": "Atom"
3007 }
3008 "#
3009 .into(),
3010 Default::default(),
3011 )
3012 .await
3013 .unwrap();
3014
3015 app_state
3016 .fs
3017 .save(
3018 "/keymap.json".as_ref(),
3019 &r#"
3020 [
3021 {
3022 "bindings": {
3023 "backspace": "test1::A"
3024 }
3025 }
3026 ]
3027 "#
3028 .into(),
3029 Default::default(),
3030 )
3031 .await
3032 .unwrap();
3033 executor.run_until_parked();
3034 cx.update(|cx| {
3035 let settings_rx = watch_config_file(
3036 &executor,
3037 app_state.fs.clone(),
3038 PathBuf::from("/settings.json"),
3039 );
3040 let keymap_rx = watch_config_file(
3041 &executor,
3042 app_state.fs.clone(),
3043 PathBuf::from("/keymap.json"),
3044 );
3045 handle_settings_file_changes(settings_rx, cx);
3046 handle_keymap_file_changes(keymap_rx, cx);
3047 });
3048 workspace
3049 .update(cx, |workspace, cx| {
3050 workspace.register_action(|_, _: &A, _cx| {});
3051 workspace.register_action(|_, _: &B, _cx| {});
3052 workspace.register_action(|_, _: &ActivatePreviousPane, _cx| {});
3053 workspace.register_action(|_, _: &ActivatePrevItem, _cx| {});
3054 cx.notify();
3055 })
3056 .unwrap();
3057 executor.run_until_parked();
3058 // Test loading the keymap base at all
3059 assert_key_bindings_for(
3060 workspace.into(),
3061 cx,
3062 vec![("backspace", &A), ("k", &ActivatePreviousPane)],
3063 line!(),
3064 );
3065
3066 // Test modifying the users keymap, while retaining the base keymap
3067 app_state
3068 .fs
3069 .save(
3070 "/keymap.json".as_ref(),
3071 &r#"
3072 [
3073 {
3074 "bindings": {
3075 "backspace": "test1::B"
3076 }
3077 }
3078 ]
3079 "#
3080 .into(),
3081 Default::default(),
3082 )
3083 .await
3084 .unwrap();
3085
3086 executor.run_until_parked();
3087
3088 assert_key_bindings_for(
3089 workspace.into(),
3090 cx,
3091 vec![("backspace", &B), ("k", &ActivatePreviousPane)],
3092 line!(),
3093 );
3094
3095 // Test modifying the base, while retaining the users keymap
3096 app_state
3097 .fs
3098 .save(
3099 "/settings.json".as_ref(),
3100 &r#"
3101 {
3102 "base_keymap": "JetBrains"
3103 }
3104 "#
3105 .into(),
3106 Default::default(),
3107 )
3108 .await
3109 .unwrap();
3110
3111 executor.run_until_parked();
3112
3113 assert_key_bindings_for(
3114 workspace.into(),
3115 cx,
3116 vec![("backspace", &B), ("[", &ActivatePrevItem)],
3117 line!(),
3118 );
3119 }
3120
3121 #[gpui::test]
3122 async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
3123 let executor = cx.executor();
3124 let app_state = init_keymap_test(cx);
3125 let project = Project::test(app_state.fs.clone(), [], cx).await;
3126 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3127
3128 actions!(test2, [A, B]);
3129 // From the Atom keymap
3130 use workspace::ActivatePreviousPane;
3131 // From the JetBrains keymap
3132 use pane::ActivatePrevItem;
3133 workspace
3134 .update(cx, |workspace, _| {
3135 workspace
3136 .register_action(|_, _: &A, _| {})
3137 .register_action(|_, _: &B, _| {});
3138 })
3139 .unwrap();
3140 app_state
3141 .fs
3142 .save(
3143 "/settings.json".as_ref(),
3144 &r#"
3145 {
3146 "base_keymap": "Atom"
3147 }
3148 "#
3149 .into(),
3150 Default::default(),
3151 )
3152 .await
3153 .unwrap();
3154 app_state
3155 .fs
3156 .save(
3157 "/keymap.json".as_ref(),
3158 &r#"
3159 [
3160 {
3161 "bindings": {
3162 "backspace": "test2::A"
3163 }
3164 }
3165 ]
3166 "#
3167 .into(),
3168 Default::default(),
3169 )
3170 .await
3171 .unwrap();
3172
3173 cx.update(|cx| {
3174 let settings_rx = watch_config_file(
3175 &executor,
3176 app_state.fs.clone(),
3177 PathBuf::from("/settings.json"),
3178 );
3179 let keymap_rx = watch_config_file(
3180 &executor,
3181 app_state.fs.clone(),
3182 PathBuf::from("/keymap.json"),
3183 );
3184
3185 handle_settings_file_changes(settings_rx, cx);
3186 handle_keymap_file_changes(keymap_rx, cx);
3187 });
3188
3189 cx.background_executor.run_until_parked();
3190
3191 cx.background_executor.run_until_parked();
3192 // Test loading the keymap base at all
3193 assert_key_bindings_for(
3194 workspace.into(),
3195 cx,
3196 vec![("backspace", &A), ("k", &ActivatePreviousPane)],
3197 line!(),
3198 );
3199
3200 // Test disabling the key binding for the base keymap
3201 app_state
3202 .fs
3203 .save(
3204 "/keymap.json".as_ref(),
3205 &r#"
3206 [
3207 {
3208 "bindings": {
3209 "backspace": null
3210 }
3211 }
3212 ]
3213 "#
3214 .into(),
3215 Default::default(),
3216 )
3217 .await
3218 .unwrap();
3219
3220 cx.background_executor.run_until_parked();
3221
3222 assert_key_bindings_for(
3223 workspace.into(),
3224 cx,
3225 vec![("k", &ActivatePreviousPane)],
3226 line!(),
3227 );
3228
3229 // Test modifying the base, while retaining the users keymap
3230 app_state
3231 .fs
3232 .save(
3233 "/settings.json".as_ref(),
3234 &r#"
3235 {
3236 "base_keymap": "JetBrains"
3237 }
3238 "#
3239 .into(),
3240 Default::default(),
3241 )
3242 .await
3243 .unwrap();
3244
3245 cx.background_executor.run_until_parked();
3246
3247 assert_key_bindings_for(
3248 workspace.into(),
3249 cx,
3250 vec![("[", &ActivatePrevItem)],
3251 line!(),
3252 );
3253 }
3254
3255 #[gpui::test]
3256 fn test_bundled_settings_and_themes(cx: &mut AppContext) {
3257 cx.text_system()
3258 .add_fonts(vec![
3259 Assets
3260 .load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
3261 .unwrap()
3262 .unwrap(),
3263 Assets
3264 .load("fonts/plex-sans/ZedPlexSans-Regular.ttf")
3265 .unwrap()
3266 .unwrap(),
3267 ])
3268 .unwrap();
3269 let themes = ThemeRegistry::default();
3270 settings::init(cx);
3271 theme::init(theme::LoadThemes::JustBase, cx);
3272
3273 let mut has_default_theme = false;
3274 for theme_name in themes.list(false).into_iter().map(|meta| meta.name) {
3275 let theme = themes.get(&theme_name).unwrap();
3276 assert_eq!(theme.name, theme_name);
3277 if theme.name == ThemeSettings::get(None, cx).active_theme.name {
3278 has_default_theme = true;
3279 }
3280 }
3281 assert!(has_default_theme);
3282 }
3283
3284 #[gpui::test]
3285 async fn test_bundled_languages(cx: &mut TestAppContext) {
3286 let settings = cx.update(|cx| SettingsStore::test(cx));
3287 cx.set_global(settings);
3288 let languages = LanguageRegistry::test(cx.executor());
3289 let languages = Arc::new(languages);
3290 let node_runtime = node_runtime::FakeNodeRuntime::new();
3291 cx.update(|cx| {
3292 languages::init(languages.clone(), node_runtime, cx);
3293 });
3294 for name in languages.language_names() {
3295 languages
3296 .language_for_name(&name)
3297 .await
3298 .with_context(|| format!("language name {name}"))
3299 .unwrap();
3300 }
3301 cx.run_until_parked();
3302 }
3303
3304 #[gpui::test]
3305 async fn test_spawn_terminal_task_real_fs(cx: &mut TestAppContext) {
3306 let mut app_state = cx.update(|cx| AppState::test(cx));
3307 let state = Arc::get_mut(&mut app_state).unwrap();
3308 state.fs = Arc::new(fs::RealFs::default());
3309 let app_state = init_test_with_state(cx, app_state);
3310
3311 cx.executor().allow_parking();
3312 let project_root = util::test::temp_tree(json!({
3313 "sample.txt": ""
3314 }));
3315
3316 let spawn_in_terminal = SpawnInTerminal {
3317 command: "echo SAMPLE-OUTPUT".to_string(),
3318 cwd: None,
3319 env: HashMap::default(),
3320 id: task::TaskId(String::from("sample-id")),
3321 full_label: String::from("sample-full_label"),
3322 label: String::from("sample-label"),
3323 args: vec![],
3324 command_label: String::from("sample-command_label"),
3325 use_new_terminal: false,
3326 allow_concurrent_runs: false,
3327 reveal: RevealStrategy::Always,
3328 };
3329 let project = Project::test(app_state.fs.clone(), [project_root.path()], cx).await;
3330 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
3331 cx.run_until_parked();
3332 cx.update(|cx| {
3333 window
3334 .update(cx, |_workspace, cx| {
3335 cx.emit(workspace::Event::SpawnTask(spawn_in_terminal));
3336 })
3337 .unwrap();
3338 });
3339 cx.run_until_parked();
3340
3341 run_until(|| {
3342 cx.update(|cx| {
3343 window
3344 .read_with(cx, |workspace, cx| {
3345 let terminal = workspace
3346 .project()
3347 .read(cx)
3348 .local_terminal_handles()
3349 .first()
3350 .unwrap()
3351 .upgrade()
3352 .unwrap()
3353 .read(cx);
3354 terminal
3355 .last_n_non_empty_lines(99)
3356 .join("")
3357 .contains("SAMPLE-OUTPUT")
3358 })
3359 .unwrap()
3360 })
3361 })
3362 .await;
3363 }
3364
3365 async fn run_until(predicate: impl Fn() -> bool) {
3366 let timer = async { smol::Timer::after(std::time::Duration::from_secs(3)).await };
3367
3368 use futures::FutureExt as _;
3369 use smol::future::FutureExt as _;
3370
3371 async {
3372 loop {
3373 if predicate() {
3374 return Ok(());
3375 }
3376 smol::Timer::after(std::time::Duration::from_millis(10)).await;
3377 }
3378 }
3379 .race(timer.map(|_| Err(anyhow!("condition timed out"))))
3380 .await
3381 .unwrap();
3382 }
3383
3384 fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
3385 init_test_with_state(cx, cx.update(|cx| AppState::test(cx)))
3386 }
3387
3388 fn init_test_with_state(
3389 cx: &mut TestAppContext,
3390 mut app_state: Arc<AppState>,
3391 ) -> Arc<AppState> {
3392 cx.update(move |cx| {
3393 env_logger::builder().is_test(true).try_init().ok();
3394
3395 let state = Arc::get_mut(&mut app_state).unwrap();
3396 state.build_window_options = build_window_options;
3397
3398 app_state.languages.add(markdown_language());
3399
3400 theme::init(theme::LoadThemes::JustBase, cx);
3401 audio::init((), cx);
3402 channel::init(&app_state.client, app_state.user_store.clone(), cx);
3403 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
3404 notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
3405 workspace::init(app_state.clone(), cx);
3406 Project::init_settings(cx);
3407 release_channel::init(SemanticVersion::default(), cx);
3408 command_palette::init(cx);
3409 language::init(cx);
3410 editor::init(cx);
3411 collab_ui::init(&app_state, cx);
3412 project_panel::init((), cx);
3413 outline_panel::init((), cx);
3414 terminal_view::init(cx);
3415 assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
3416 repl::init(cx);
3417 tasks_ui::init(cx);
3418 initialize_workspace(app_state.clone(), cx);
3419 app_state
3420 })
3421 }
3422
3423 fn rust_lang() -> Arc<language::Language> {
3424 Arc::new(language::Language::new(
3425 language::LanguageConfig {
3426 name: "Rust".into(),
3427 matcher: LanguageMatcher {
3428 path_suffixes: vec!["rs".to_string()],
3429 ..Default::default()
3430 },
3431 ..Default::default()
3432 },
3433 Some(tree_sitter_rust::language()),
3434 ))
3435 }
3436
3437 fn markdown_language() -> Arc<language::Language> {
3438 Arc::new(language::Language::new(
3439 language::LanguageConfig {
3440 name: "Markdown".into(),
3441 matcher: LanguageMatcher {
3442 path_suffixes: vec!["md".to_string()],
3443 ..Default::default()
3444 },
3445 ..Default::default()
3446 },
3447 Some(tree_sitter_markdown::language()),
3448 ))
3449 }
3450
3451 #[track_caller]
3452 fn assert_key_bindings_for(
3453 window: AnyWindowHandle,
3454 cx: &TestAppContext,
3455 actions: Vec<(&'static str, &dyn Action)>,
3456 line: u32,
3457 ) {
3458 let available_actions = cx
3459 .update(|cx| window.update(cx, |_, cx| cx.available_actions()))
3460 .unwrap();
3461 for (key, action) in actions {
3462 let bindings = cx
3463 .update(|cx| window.update(cx, |_, cx| cx.bindings_for_action(action)))
3464 .unwrap();
3465 // assert that...
3466 assert!(
3467 available_actions.iter().any(|bound_action| {
3468 // actions match...
3469 bound_action.partial_eq(action)
3470 }),
3471 "On {} Failed to find {}",
3472 line,
3473 action.name(),
3474 );
3475 assert!(
3476 // and key strokes contain the given key
3477 bindings
3478 .into_iter()
3479 .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)),
3480 "On {} Failed to find {} with key binding {}",
3481 line,
3482 action.name(),
3483 key
3484 );
3485 }
3486 }
3487}
3488
3489async fn register_zed_scheme(cx: &AsyncAppContext) -> anyhow::Result<()> {
3490 cx.update(|cx| cx.register_url_scheme(ZED_URL_SCHEME))?
3491 .await
3492}