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