1pub mod assets;
2pub mod languages;
3pub mod menus;
4pub mod only_instance;
5#[cfg(any(test, feature = "test-support"))]
6pub mod test;
7
8use ai::AssistantPanel;
9use anyhow::Context;
10use assets::Assets;
11use breadcrumbs::Breadcrumbs;
12pub use client;
13use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
14use collections::VecDeque;
15pub use editor;
16use editor::{Editor, MultiBuffer};
17
18use anyhow::anyhow;
19use feedback::{
20 feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton,
21};
22use futures::{channel::mpsc, StreamExt};
23use gpui::{
24 anyhow::{self, Result},
25 geometry::vector::vec2f,
26 impl_actions,
27 platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions},
28 AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle,
29};
30pub use lsp;
31pub use project;
32use project_panel::ProjectPanel;
33use search::{BufferSearchBar, ProjectSearchBar};
34use serde::Deserialize;
35use serde_json::to_string_pretty;
36use settings::{initial_local_settings_content, KeymapFile, SettingsStore};
37use std::{borrow::Cow, str, sync::Arc};
38use terminal_view::terminal_panel::{self, TerminalPanel};
39use util::{
40 asset_str,
41 channel::ReleaseChannel,
42 paths::{self, LOCAL_SETTINGS_RELATIVE_PATH},
43 ResultExt,
44};
45use uuid::Uuid;
46use welcome::BaseKeymap;
47pub use workspace;
48use workspace::{
49 create_and_open_local_file, dock::PanelHandle,
50 notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile,
51 NewWindow, Workspace, WorkspaceSettings,
52};
53use zed_actions::*;
54
55#[derive(Deserialize, Clone, PartialEq)]
56pub struct OpenBrowser {
57 url: Arc<str>,
58}
59
60impl_actions!(zed, [OpenBrowser]);
61
62pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
63 cx.add_action(about);
64 cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| {
65 cx.platform().hide();
66 });
67 cx.add_global_action(|_: &HideOthers, cx: &mut gpui::AppContext| {
68 cx.platform().hide_other_apps();
69 });
70 cx.add_global_action(|_: &ShowAll, cx: &mut gpui::AppContext| {
71 cx.platform().unhide_other_apps();
72 });
73 cx.add_action(
74 |_: &mut Workspace, _: &Minimize, cx: &mut ViewContext<Workspace>| {
75 cx.minimize_window();
76 },
77 );
78 cx.add_action(
79 |_: &mut Workspace, _: &Zoom, cx: &mut ViewContext<Workspace>| {
80 cx.zoom_window();
81 },
82 );
83 cx.add_action(
84 |_: &mut Workspace, _: &ToggleFullScreen, cx: &mut ViewContext<Workspace>| {
85 cx.toggle_full_screen();
86 },
87 );
88 cx.add_action(
89 |workspace: &mut Workspace, _: &ToggleContactsMenu, cx: &mut ViewContext<Workspace>| {
90 if let Some(item) = workspace
91 .titlebar_item()
92 .and_then(|item| item.downcast::<CollabTitlebarItem>())
93 {
94 cx.defer(move |_, cx| {
95 item.update(cx, |item, cx| {
96 item.toggle_contacts_popover(&Default::default(), cx);
97 });
98 });
99 }
100 },
101 );
102 cx.add_global_action(quit);
103 cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
104 cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
105 theme::adjust_font_size(cx, |size| *size += 1.0)
106 });
107 cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
108 theme::adjust_font_size(cx, |size| *size -= 1.0)
109 });
110 cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx));
111 cx.add_global_action(move |_: &install_cli::Install, cx| {
112 cx.spawn(|cx| async move {
113 install_cli::install_cli(&cx)
114 .await
115 .context("error creating CLI symlink")
116 })
117 .detach_and_log_err(cx);
118 });
119 cx.add_action(
120 move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext<Workspace>| {
121 open_log_file(workspace, cx);
122 },
123 );
124 cx.add_action(
125 move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
126 open_bundled_file(
127 workspace,
128 asset_str::<Assets>("licenses.md"),
129 "Open Source License Attribution",
130 "Markdown",
131 cx,
132 );
133 },
134 );
135 cx.add_action(
136 move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext<Workspace>| {
137 open_telemetry_log_file(workspace, cx);
138 },
139 );
140 cx.add_action(
141 move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
142 create_and_open_local_file(&paths::KEYMAP, cx, Default::default).detach_and_log_err(cx);
143 },
144 );
145 cx.add_action(
146 move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
147 create_and_open_local_file(&paths::SETTINGS, cx, || {
148 settings::initial_user_settings_content().as_ref().into()
149 })
150 .detach_and_log_err(cx);
151 },
152 );
153 cx.add_action(open_local_settings_file);
154 cx.add_action(
155 move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
156 open_bundled_file(
157 workspace,
158 settings::default_keymap(),
159 "Default Key Bindings",
160 "JSON",
161 cx,
162 );
163 },
164 );
165 cx.add_action(
166 move |workspace: &mut Workspace,
167 _: &OpenDefaultSettings,
168 cx: &mut ViewContext<Workspace>| {
169 open_bundled_file(
170 workspace,
171 settings::default_settings(),
172 "Default Settings",
173 "JSON",
174 cx,
175 );
176 },
177 );
178 cx.add_action({
179 move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
180 let app_state = workspace.app_state().clone();
181 let markdown = app_state.languages.language_for_name("JSON");
182 let window_id = cx.window_id();
183 cx.spawn(|workspace, mut cx| async move {
184 let markdown = markdown.await.log_err();
185 let content = to_string_pretty(
186 &cx.debug_elements(window_id)
187 .ok_or_else(|| anyhow!("could not debug elements for {window_id}"))?,
188 )
189 .unwrap();
190 workspace
191 .update(&mut cx, |workspace, cx| {
192 workspace.with_local_workspace(cx, move |workspace, cx| {
193 let project = workspace.project().clone();
194
195 let buffer = project
196 .update(cx, |project, cx| {
197 project.create_buffer(&content, markdown, cx)
198 })
199 .expect("creating buffers on a local workspace always succeeds");
200 let buffer = cx.add_model(|cx| {
201 MultiBuffer::singleton(buffer, cx)
202 .with_title("Debug Elements".into())
203 });
204 workspace.add_item(
205 Box::new(cx.add_view(|cx| {
206 Editor::for_multibuffer(buffer, Some(project.clone()), cx)
207 })),
208 cx,
209 );
210 })
211 })?
212 .await
213 })
214 .detach_and_log_err(cx);
215 }
216 });
217 cx.add_action(
218 |workspace: &mut Workspace,
219 _: &project_panel::ToggleFocus,
220 cx: &mut ViewContext<Workspace>| {
221 workspace.toggle_panel_focus::<ProjectPanel>(cx);
222 },
223 );
224 cx.add_action(
225 |workspace: &mut Workspace,
226 _: &terminal_panel::ToggleFocus,
227 cx: &mut ViewContext<Workspace>| {
228 workspace.toggle_panel_focus::<TerminalPanel>(cx);
229 },
230 );
231 cx.add_global_action({
232 let app_state = Arc::downgrade(&app_state);
233 move |_: &NewWindow, cx: &mut AppContext| {
234 if let Some(app_state) = app_state.upgrade() {
235 open_new(&app_state, cx, |workspace, cx| {
236 Editor::new_file(workspace, &Default::default(), cx)
237 })
238 .detach();
239 }
240 }
241 });
242 cx.add_global_action({
243 let app_state = Arc::downgrade(&app_state);
244 move |_: &NewFile, cx: &mut AppContext| {
245 if let Some(app_state) = app_state.upgrade() {
246 open_new(&app_state, cx, |workspace, cx| {
247 Editor::new_file(workspace, &Default::default(), cx)
248 })
249 .detach();
250 }
251 }
252 });
253 load_default_keymap(cx);
254}
255
256pub fn initialize_workspace(
257 workspace_handle: WeakViewHandle<Workspace>,
258 was_deserialized: bool,
259 app_state: Arc<AppState>,
260 cx: AsyncAppContext,
261) -> Task<Result<()>> {
262 cx.spawn(|mut cx| async move {
263 workspace_handle.update(&mut cx, |workspace, cx| {
264 let workspace_handle = cx.handle();
265 cx.subscribe(&workspace_handle, {
266 move |workspace, _, event, cx| {
267 if let workspace::Event::PaneAdded(pane) = event {
268 pane.update(cx, |pane, cx| {
269 pane.toolbar().update(cx, |toolbar, cx| {
270 let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
271 toolbar.add_item(breadcrumbs, cx);
272 let buffer_search_bar = cx.add_view(BufferSearchBar::new);
273 toolbar.add_item(buffer_search_bar, cx);
274 let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
275 toolbar.add_item(project_search_bar, cx);
276 let submit_feedback_button =
277 cx.add_view(|_| SubmitFeedbackButton::new());
278 toolbar.add_item(submit_feedback_button, cx);
279 let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
280 toolbar.add_item(feedback_info_text, cx);
281 let lsp_log_item =
282 cx.add_view(|_| language_tools::LspLogToolbarItemView::new());
283 toolbar.add_item(lsp_log_item, cx);
284 let syntax_tree_item = cx
285 .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
286 toolbar.add_item(syntax_tree_item, cx);
287 })
288 });
289 }
290 }
291 })
292 .detach();
293
294 cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
295
296 let collab_titlebar_item =
297 cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
298 workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
299
300 let copilot =
301 cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
302 let diagnostic_summary =
303 cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
304 let activity_indicator = activity_indicator::ActivityIndicator::new(
305 workspace,
306 app_state.languages.clone(),
307 cx,
308 );
309 let active_buffer_language =
310 cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
311 let feedback_button = cx.add_view(|_| {
312 feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
313 });
314 let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
315 workspace.status_bar().update(cx, |status_bar, cx| {
316 status_bar.add_left_item(diagnostic_summary, cx);
317 status_bar.add_left_item(activity_indicator, cx);
318 status_bar.add_right_item(feedback_button, cx);
319 status_bar.add_right_item(copilot, cx);
320 status_bar.add_right_item(active_buffer_language, cx);
321 status_bar.add_right_item(cursor_position, cx);
322 });
323
324 auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
325
326 vim::observe_keystrokes(cx);
327
328 cx.on_window_should_close(|workspace, cx| {
329 if let Some(task) = workspace.close(&Default::default(), cx) {
330 task.detach_and_log_err(cx);
331 }
332 false
333 });
334 })?;
335
336 let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
337 let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
338 let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
339 let (project_panel, terminal_panel, assistant_panel) =
340 futures::try_join!(project_panel, terminal_panel, assistant_panel)?;
341
342 cx.update(|cx| {
343 if let Some(workspace) = workspace_handle.upgrade(cx) {
344 cx.update_window(project_panel.window_id(), |cx| {
345 workspace.update(cx, |workspace, cx| {
346 let project_panel_subscription =
347 cx.subscribe(&project_panel, move |workspace, _, event, cx| {
348 if let project_panel::Event::NewSearchInDirectory { dir_entry } =
349 event
350 {
351 search::ProjectSearchView::new_search_in_directory(
352 workspace, dir_entry, cx,
353 )
354 }
355 });
356 workspace.push_subscription(project_panel_subscription);
357 });
358 });
359 }
360 });
361
362 workspace_handle.update(&mut cx, |workspace, cx| {
363 let project_panel_position = project_panel.position(cx);
364 workspace.add_panel(project_panel, cx);
365 workspace.add_panel(terminal_panel, cx);
366 workspace.add_panel(assistant_panel, cx);
367
368 if !was_deserialized
369 && workspace
370 .project()
371 .read(cx)
372 .visible_worktrees(cx)
373 .any(|tree| {
374 tree.read(cx)
375 .root_entry()
376 .map_or(false, |entry| entry.is_dir())
377 })
378 {
379 workspace.toggle_dock(project_panel_position, cx);
380 }
381 cx.focus_self();
382 })?;
383 Ok(())
384 })
385}
386
387pub fn build_window_options(
388 bounds: Option<WindowBounds>,
389 display: Option<Uuid>,
390 platform: &dyn Platform,
391) -> WindowOptions<'static> {
392 let bounds = bounds.unwrap_or(WindowBounds::Maximized);
393 let screen = display.and_then(|display| platform.screen_by_id(display));
394
395 WindowOptions {
396 titlebar: Some(TitlebarOptions {
397 title: None,
398 appears_transparent: true,
399 traffic_light_position: Some(vec2f(8., 8.)),
400 }),
401 center: false,
402 focus: false,
403 show: false,
404 kind: WindowKind::Normal,
405 is_movable: true,
406 bounds,
407 screen,
408 }
409}
410
411fn quit(_: &Quit, cx: &mut gpui::AppContext) {
412 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
413 cx.spawn(|mut cx| async move {
414 let mut workspaces = cx
415 .window_ids()
416 .into_iter()
417 .filter_map(|window_id| {
418 Some(
419 cx.root_view(window_id)?
420 .clone()
421 .downcast::<Workspace>()?
422 .downgrade(),
423 )
424 })
425 .collect::<Vec<_>>();
426
427 // If multiple windows have unsaved changes, and need a save prompt,
428 // prompt in the active window before switching to a different window.
429 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
430
431 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
432 let answer = cx.prompt(
433 workspace.window_id(),
434 PromptLevel::Info,
435 "Are you sure you want to quit?",
436 &["Quit", "Cancel"],
437 );
438
439 if let Some(mut answer) = answer {
440 let answer = answer.next().await;
441 if answer != Some(0) {
442 return Ok(());
443 }
444 }
445 }
446
447 // If the user cancels any save prompt, then keep the app open.
448 for workspace in workspaces {
449 if !workspace
450 .update(&mut cx, |workspace, cx| {
451 workspace.prepare_to_close(true, cx)
452 })?
453 .await?
454 {
455 return Ok(());
456 }
457 }
458 cx.platform().quit();
459 anyhow::Ok(())
460 })
461 .detach_and_log_err(cx);
462}
463
464fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
465 let app_name = cx.global::<ReleaseChannel>().display_name();
466 let version = env!("CARGO_PKG_VERSION");
467 cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]);
468}
469
470fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
471 const MAX_LINES: usize = 1000;
472
473 workspace
474 .with_local_workspace(cx, move |workspace, cx| {
475 let fs = workspace.app_state().fs.clone();
476 cx.spawn(|workspace, mut cx| async move {
477 let (old_log, new_log) =
478 futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG));
479
480 let mut lines = VecDeque::with_capacity(MAX_LINES);
481 for line in old_log
482 .iter()
483 .flat_map(|log| log.lines())
484 .chain(new_log.iter().flat_map(|log| log.lines()))
485 {
486 if lines.len() == MAX_LINES {
487 lines.pop_front();
488 }
489 lines.push_back(line);
490 }
491 let log = lines
492 .into_iter()
493 .flat_map(|line| [line, "\n"])
494 .collect::<String>();
495
496 workspace
497 .update(&mut cx, |workspace, cx| {
498 let project = workspace.project().clone();
499 let buffer = project
500 .update(cx, |project, cx| project.create_buffer("", None, cx))
501 .expect("creating buffers on a local workspace always succeeds");
502 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx));
503
504 let buffer = cx.add_model(|cx| {
505 MultiBuffer::singleton(buffer, cx).with_title("Log".into())
506 });
507 workspace.add_item(
508 Box::new(
509 cx.add_view(|cx| {
510 Editor::for_multibuffer(buffer, Some(project), cx)
511 }),
512 ),
513 cx,
514 );
515 })
516 .log_err();
517 })
518 .detach();
519 })
520 .detach();
521}
522
523pub fn load_default_keymap(cx: &mut AppContext) {
524 for path in ["keymaps/default.json", "keymaps/vim.json"] {
525 KeymapFile::load_asset(path, cx).unwrap();
526 }
527
528 if let Some(asset_path) = settings::get::<BaseKeymap>(cx).asset_path() {
529 KeymapFile::load_asset(asset_path, cx).unwrap();
530 }
531}
532
533pub fn handle_keymap_file_changes(
534 mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
535 cx: &mut AppContext,
536) {
537 cx.spawn(move |mut cx| async move {
538 let mut settings_subscription = None;
539 while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
540 if let Ok(keymap_content) = KeymapFile::parse(&user_keymap_content) {
541 cx.update(|cx| reload_keymaps(cx, &keymap_content));
542
543 let mut old_base_keymap = cx.read(|cx| *settings::get::<BaseKeymap>(cx));
544 drop(settings_subscription);
545 settings_subscription = Some(cx.update(|cx| {
546 cx.observe_global::<SettingsStore, _>(move |cx| {
547 let new_base_keymap = *settings::get::<BaseKeymap>(cx);
548 if new_base_keymap != old_base_keymap {
549 old_base_keymap = new_base_keymap.clone();
550 reload_keymaps(cx, &keymap_content);
551 }
552 })
553 .detach();
554 }));
555 }
556 }
557 })
558 .detach();
559}
560
561fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
562 cx.clear_bindings();
563 load_default_keymap(cx);
564 keymap_content.clone().add_to_cx(cx).log_err();
565 cx.set_menus(menus::menus());
566}
567
568fn open_local_settings_file(
569 workspace: &mut Workspace,
570 _: &OpenLocalSettings,
571 cx: &mut ViewContext<Workspace>,
572) {
573 let project = workspace.project().clone();
574 let worktree = project
575 .read(cx)
576 .visible_worktrees(cx)
577 .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
578 if let Some(worktree) = worktree {
579 let tree_id = worktree.read(cx).id();
580 cx.spawn(|workspace, mut cx| async move {
581 let file_path = &*LOCAL_SETTINGS_RELATIVE_PATH;
582
583 if let Some(dir_path) = file_path.parent() {
584 if worktree.read_with(&cx, |tree, _| tree.entry_for_path(dir_path).is_none()) {
585 project
586 .update(&mut cx, |project, cx| {
587 project.create_entry((tree_id, dir_path), true, cx)
588 })
589 .ok_or_else(|| anyhow!("worktree was removed"))?
590 .await?;
591 }
592 }
593
594 if worktree.read_with(&cx, |tree, _| tree.entry_for_path(file_path).is_none()) {
595 project
596 .update(&mut cx, |project, cx| {
597 project.create_entry((tree_id, file_path), false, cx)
598 })
599 .ok_or_else(|| anyhow!("worktree was removed"))?
600 .await?;
601 }
602
603 let editor = workspace
604 .update(&mut cx, |workspace, cx| {
605 workspace.open_path((tree_id, file_path), None, true, cx)
606 })?
607 .await?
608 .downcast::<Editor>()
609 .ok_or_else(|| anyhow!("unexpected item type"))?;
610
611 editor
612 .downgrade()
613 .update(&mut cx, |editor, cx| {
614 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
615 if buffer.read(cx).is_empty() {
616 buffer.update(cx, |buffer, cx| {
617 buffer.edit([(0..0, initial_local_settings_content())], None, cx)
618 });
619 }
620 }
621 })
622 .ok();
623
624 anyhow::Ok(())
625 })
626 .detach();
627 } else {
628 workspace.show_notification(0, cx, |cx| {
629 cx.add_view(|_| MessageNotification::new("This project has no folders open."))
630 })
631 }
632}
633
634fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
635 workspace.with_local_workspace(cx, move |workspace, cx| {
636 let app_state = workspace.app_state().clone();
637 cx.spawn(|workspace, mut cx| async move {
638 async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
639 let path = app_state.client.telemetry().log_file_path()?;
640 app_state.fs.load(&path).await.log_err()
641 }
642
643 let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
644
645 const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
646 let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
647 if let Some(newline_offset) = log[start_offset..].find('\n') {
648 start_offset += newline_offset + 1;
649 }
650 let log_suffix = &log[start_offset..];
651 let json = app_state.languages.language_for_name("JSON").await.log_err();
652
653 workspace.update(&mut cx, |workspace, cx| {
654 let project = workspace.project().clone();
655 let buffer = project
656 .update(cx, |project, cx| project.create_buffer("", None, cx))
657 .expect("creating buffers on a local workspace always succeeds");
658 buffer.update(cx, |buffer, cx| {
659 buffer.set_language(json, cx);
660 buffer.edit(
661 [(
662 0..0,
663 concat!(
664 "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
665 "// Telemetry can be disabled via the `settings.json` file.\n",
666 "// Here is the data that has been reported for the current session:\n",
667 "\n"
668 ),
669 )],
670 None,
671 cx,
672 );
673 buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
674 });
675
676 let buffer = cx.add_model(|cx| {
677 MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
678 });
679 workspace.add_item(
680 Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
681 cx,
682 );
683 }).log_err()?;
684
685 Some(())
686 })
687 .detach();
688 }).detach();
689}
690
691fn open_bundled_file(
692 workspace: &mut Workspace,
693 text: Cow<'static, str>,
694 title: &'static str,
695 language: &'static str,
696 cx: &mut ViewContext<Workspace>,
697) {
698 let language = workspace.app_state().languages.language_for_name(language);
699 cx.spawn(|workspace, mut cx| async move {
700 let language = language.await.log_err();
701 workspace
702 .update(&mut cx, |workspace, cx| {
703 workspace.with_local_workspace(cx, |workspace, cx| {
704 let project = workspace.project();
705 let buffer = project.update(cx, move |project, cx| {
706 project
707 .create_buffer(text.as_ref(), language, cx)
708 .expect("creating buffers on a local workspace always succeeds")
709 });
710 let buffer = cx.add_model(|cx| {
711 MultiBuffer::singleton(buffer, cx).with_title(title.into())
712 });
713 workspace.add_item(
714 Box::new(cx.add_view(|cx| {
715 Editor::for_multibuffer(buffer, Some(project.clone()), cx)
716 })),
717 cx,
718 );
719 })
720 })?
721 .await
722 })
723 .detach_and_log_err(cx);
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729 use assets::Assets;
730 use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
731 use fs::{FakeFs, Fs};
732 use gpui::{
733 actions, elements::Empty, executor::Deterministic, Action, AnyElement, AppContext,
734 AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
735 };
736 use language::LanguageRegistry;
737 use node_runtime::NodeRuntime;
738 use project::{Project, ProjectPath};
739 use serde_json::json;
740 use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
741 use std::{
742 collections::HashSet,
743 path::{Path, PathBuf},
744 };
745 use theme::{ThemeRegistry, ThemeSettings};
746 use util::http::FakeHttpClient;
747 use workspace::{
748 item::{Item, ItemHandle},
749 open_new, open_paths, pane, NewFile, SplitDirection, WorkspaceHandle,
750 };
751
752 #[gpui::test]
753 async fn test_open_paths_action(cx: &mut TestAppContext) {
754 let app_state = init_test(cx);
755 app_state
756 .fs
757 .as_fake()
758 .insert_tree(
759 "/root",
760 json!({
761 "a": {
762 "aa": null,
763 "ab": null,
764 },
765 "b": {
766 "ba": null,
767 "bb": null,
768 },
769 "c": {
770 "ca": null,
771 "cb": null,
772 },
773 "d": {
774 "da": null,
775 "db": null,
776 },
777 }),
778 )
779 .await;
780
781 cx.update(|cx| {
782 open_paths(
783 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
784 &app_state,
785 None,
786 cx,
787 )
788 })
789 .await
790 .unwrap();
791 assert_eq!(cx.window_ids().len(), 1);
792
793 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
794 .await
795 .unwrap();
796 assert_eq!(cx.window_ids().len(), 1);
797 let workspace_1 = cx
798 .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
799 .unwrap()
800 .downcast::<Workspace>()
801 .unwrap();
802 workspace_1.update(cx, |workspace, cx| {
803 assert_eq!(workspace.worktrees(cx).count(), 2);
804 assert!(workspace.left_dock().read(cx).is_open());
805 assert!(workspace.active_pane().is_focused(cx));
806 });
807
808 cx.update(|cx| {
809 open_paths(
810 &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
811 &app_state,
812 None,
813 cx,
814 )
815 })
816 .await
817 .unwrap();
818 assert_eq!(cx.window_ids().len(), 2);
819
820 // Replace existing windows
821 let window_id = cx.window_ids()[0];
822 cx.update(|cx| {
823 open_paths(
824 &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
825 &app_state,
826 Some(window_id),
827 cx,
828 )
829 })
830 .await
831 .unwrap();
832 assert_eq!(cx.window_ids().len(), 2);
833 let workspace_1 = cx
834 .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
835 .unwrap()
836 .clone()
837 .downcast::<Workspace>()
838 .unwrap();
839 workspace_1.update(cx, |workspace, cx| {
840 assert_eq!(
841 workspace
842 .worktrees(cx)
843 .map(|w| w.read(cx).abs_path())
844 .collect::<Vec<_>>(),
845 &[Path::new("/root/c").into(), Path::new("/root/d").into()]
846 );
847 assert!(workspace.left_dock().read(cx).is_open());
848 assert!(workspace.active_pane().is_focused(cx));
849 });
850 }
851
852 #[gpui::test]
853 async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
854 let app_state = init_test(cx);
855 app_state
856 .fs
857 .as_fake()
858 .insert_tree("/root", json!({"a": "hey"}))
859 .await;
860
861 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
862 .await
863 .unwrap();
864 assert_eq!(cx.window_ids().len(), 1);
865
866 // When opening the workspace, the window is not in a edited state.
867 let workspace = cx
868 .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
869 .unwrap()
870 .downcast::<Workspace>()
871 .unwrap();
872 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
873 let editor = workspace.read_with(cx, |workspace, cx| {
874 workspace
875 .active_item(cx)
876 .unwrap()
877 .downcast::<Editor>()
878 .unwrap()
879 });
880 assert!(!cx.is_window_edited(workspace.window_id()));
881
882 // Editing a buffer marks the window as edited.
883 editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
884 assert!(cx.is_window_edited(workspace.window_id()));
885
886 // Undoing the edit restores the window's edited state.
887 editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
888 assert!(!cx.is_window_edited(workspace.window_id()));
889
890 // Redoing the edit marks the window as edited again.
891 editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
892 assert!(cx.is_window_edited(workspace.window_id()));
893
894 // Closing the item restores the window's edited state.
895 let close = pane.update(cx, |pane, cx| {
896 drop(editor);
897 pane.close_active_item(&Default::default(), cx).unwrap()
898 });
899 executor.run_until_parked();
900 cx.simulate_prompt_answer(workspace.window_id(), 1);
901 close.await.unwrap();
902 assert!(!cx.is_window_edited(workspace.window_id()));
903
904 // Opening the buffer again doesn't impact the window's edited state.
905 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
906 .await
907 .unwrap();
908 let editor = workspace.read_with(cx, |workspace, cx| {
909 workspace
910 .active_item(cx)
911 .unwrap()
912 .downcast::<Editor>()
913 .unwrap()
914 });
915 assert!(!cx.is_window_edited(workspace.window_id()));
916
917 // Editing the buffer marks the window as edited.
918 editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
919 assert!(cx.is_window_edited(workspace.window_id()));
920
921 // Ensure closing the window via the mouse gets preempted due to the
922 // buffer having unsaved changes.
923 assert!(!cx.simulate_window_close(workspace.window_id()));
924 executor.run_until_parked();
925 assert_eq!(cx.window_ids().len(), 1);
926
927 // The window is successfully closed after the user dismisses the prompt.
928 cx.simulate_prompt_answer(workspace.window_id(), 1);
929 executor.run_until_parked();
930 assert_eq!(cx.window_ids().len(), 0);
931 }
932
933 #[gpui::test]
934 async fn test_new_empty_workspace(cx: &mut TestAppContext) {
935 let app_state = init_test(cx);
936 cx.update(|cx| {
937 open_new(&app_state, cx, |workspace, cx| {
938 Editor::new_file(workspace, &Default::default(), cx)
939 })
940 })
941 .await;
942
943 let window_id = *cx.window_ids().first().unwrap();
944 let workspace = cx
945 .read_window(window_id, |cx| cx.root_view().clone())
946 .unwrap()
947 .downcast::<Workspace>()
948 .unwrap();
949
950 let editor = workspace.update(cx, |workspace, cx| {
951 workspace
952 .active_item(cx)
953 .unwrap()
954 .downcast::<editor::Editor>()
955 .unwrap()
956 });
957
958 editor.update(cx, |editor, cx| {
959 assert!(editor.text(cx).is_empty());
960 });
961
962 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
963 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
964 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
965 save_task.await.unwrap();
966 editor.read_with(cx, |editor, cx| {
967 assert!(!editor.is_dirty(cx));
968 assert_eq!(editor.title(cx), "the-new-name");
969 });
970 }
971
972 #[gpui::test]
973 async fn test_open_entry(cx: &mut TestAppContext) {
974 let app_state = init_test(cx);
975 app_state
976 .fs
977 .as_fake()
978 .insert_tree(
979 "/root",
980 json!({
981 "a": {
982 "file1": "contents 1",
983 "file2": "contents 2",
984 "file3": "contents 3",
985 },
986 }),
987 )
988 .await;
989
990 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
991 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
992
993 let entries = cx.read(|cx| workspace.file_project_paths(cx));
994 let file1 = entries[0].clone();
995 let file2 = entries[1].clone();
996 let file3 = entries[2].clone();
997
998 // Open the first entry
999 let entry_1 = workspace
1000 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1001 .await
1002 .unwrap();
1003 cx.read(|cx| {
1004 let pane = workspace.read(cx).active_pane().read(cx);
1005 assert_eq!(
1006 pane.active_item().unwrap().project_path(cx),
1007 Some(file1.clone())
1008 );
1009 assert_eq!(pane.items_len(), 1);
1010 });
1011
1012 // Open the second entry
1013 workspace
1014 .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1015 .await
1016 .unwrap();
1017 cx.read(|cx| {
1018 let pane = workspace.read(cx).active_pane().read(cx);
1019 assert_eq!(
1020 pane.active_item().unwrap().project_path(cx),
1021 Some(file2.clone())
1022 );
1023 assert_eq!(pane.items_len(), 2);
1024 });
1025
1026 // Open the first entry again. The existing pane item is activated.
1027 let entry_1b = workspace
1028 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1029 .await
1030 .unwrap();
1031 assert_eq!(entry_1.id(), entry_1b.id());
1032
1033 cx.read(|cx| {
1034 let pane = workspace.read(cx).active_pane().read(cx);
1035 assert_eq!(
1036 pane.active_item().unwrap().project_path(cx),
1037 Some(file1.clone())
1038 );
1039 assert_eq!(pane.items_len(), 2);
1040 });
1041
1042 // Split the pane with the first entry, then open the second entry again.
1043 workspace
1044 .update(cx, |w, cx| {
1045 w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
1046 w.open_path(file2.clone(), None, true, cx)
1047 })
1048 .await
1049 .unwrap();
1050
1051 workspace.read_with(cx, |w, cx| {
1052 assert_eq!(
1053 w.active_pane()
1054 .read(cx)
1055 .active_item()
1056 .unwrap()
1057 .project_path(cx),
1058 Some(file2.clone())
1059 );
1060 });
1061
1062 // Open the third entry twice concurrently. Only one pane item is added.
1063 let (t1, t2) = workspace.update(cx, |w, cx| {
1064 (
1065 w.open_path(file3.clone(), None, true, cx),
1066 w.open_path(file3.clone(), None, true, cx),
1067 )
1068 });
1069 t1.await.unwrap();
1070 t2.await.unwrap();
1071 cx.read(|cx| {
1072 let pane = workspace.read(cx).active_pane().read(cx);
1073 assert_eq!(
1074 pane.active_item().unwrap().project_path(cx),
1075 Some(file3.clone())
1076 );
1077 let pane_entries = pane
1078 .items()
1079 .map(|i| i.project_path(cx).unwrap())
1080 .collect::<Vec<_>>();
1081 assert_eq!(pane_entries, &[file1, file2, file3]);
1082 });
1083 }
1084
1085 #[gpui::test]
1086 async fn test_open_paths(cx: &mut TestAppContext) {
1087 let app_state = init_test(cx);
1088
1089 app_state
1090 .fs
1091 .as_fake()
1092 .insert_tree(
1093 "/",
1094 json!({
1095 "dir1": {
1096 "a.txt": ""
1097 },
1098 "dir2": {
1099 "b.txt": ""
1100 },
1101 "dir3": {
1102 "c.txt": ""
1103 },
1104 "d.txt": ""
1105 }),
1106 )
1107 .await;
1108
1109 let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
1110 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1111
1112 // Open a file within an existing worktree.
1113 workspace
1114 .update(cx, |view, cx| {
1115 view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
1116 })
1117 .await;
1118 cx.read(|cx| {
1119 assert_eq!(
1120 workspace
1121 .read(cx)
1122 .active_pane()
1123 .read(cx)
1124 .active_item()
1125 .unwrap()
1126 .as_any()
1127 .downcast_ref::<Editor>()
1128 .unwrap()
1129 .read(cx)
1130 .title(cx),
1131 "a.txt"
1132 );
1133 });
1134
1135 // Open a file outside of any existing worktree.
1136 workspace
1137 .update(cx, |view, cx| {
1138 view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
1139 })
1140 .await;
1141 cx.read(|cx| {
1142 let worktree_roots = workspace
1143 .read(cx)
1144 .worktrees(cx)
1145 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1146 .collect::<HashSet<_>>();
1147 assert_eq!(
1148 worktree_roots,
1149 vec!["/dir1", "/dir2/b.txt"]
1150 .into_iter()
1151 .map(Path::new)
1152 .collect(),
1153 );
1154 assert_eq!(
1155 workspace
1156 .read(cx)
1157 .active_pane()
1158 .read(cx)
1159 .active_item()
1160 .unwrap()
1161 .as_any()
1162 .downcast_ref::<Editor>()
1163 .unwrap()
1164 .read(cx)
1165 .title(cx),
1166 "b.txt"
1167 );
1168 });
1169
1170 // Ensure opening a directory and one of its children only adds one worktree.
1171 workspace
1172 .update(cx, |view, cx| {
1173 view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
1174 })
1175 .await;
1176 cx.read(|cx| {
1177 let worktree_roots = workspace
1178 .read(cx)
1179 .worktrees(cx)
1180 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1181 .collect::<HashSet<_>>();
1182 assert_eq!(
1183 worktree_roots,
1184 vec!["/dir1", "/dir2/b.txt", "/dir3"]
1185 .into_iter()
1186 .map(Path::new)
1187 .collect(),
1188 );
1189 assert_eq!(
1190 workspace
1191 .read(cx)
1192 .active_pane()
1193 .read(cx)
1194 .active_item()
1195 .unwrap()
1196 .as_any()
1197 .downcast_ref::<Editor>()
1198 .unwrap()
1199 .read(cx)
1200 .title(cx),
1201 "c.txt"
1202 );
1203 });
1204
1205 // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
1206 workspace
1207 .update(cx, |view, cx| {
1208 view.open_paths(vec!["/d.txt".into()], false, cx)
1209 })
1210 .await;
1211 cx.read(|cx| {
1212 let worktree_roots = workspace
1213 .read(cx)
1214 .worktrees(cx)
1215 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1216 .collect::<HashSet<_>>();
1217 assert_eq!(
1218 worktree_roots,
1219 vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
1220 .into_iter()
1221 .map(Path::new)
1222 .collect(),
1223 );
1224
1225 let visible_worktree_roots = workspace
1226 .read(cx)
1227 .visible_worktrees(cx)
1228 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1229 .collect::<HashSet<_>>();
1230 assert_eq!(
1231 visible_worktree_roots,
1232 vec!["/dir1", "/dir2/b.txt", "/dir3"]
1233 .into_iter()
1234 .map(Path::new)
1235 .collect(),
1236 );
1237
1238 assert_eq!(
1239 workspace
1240 .read(cx)
1241 .active_pane()
1242 .read(cx)
1243 .active_item()
1244 .unwrap()
1245 .as_any()
1246 .downcast_ref::<Editor>()
1247 .unwrap()
1248 .read(cx)
1249 .title(cx),
1250 "d.txt"
1251 );
1252 });
1253 }
1254
1255 #[gpui::test]
1256 async fn test_save_conflicting_item(cx: &mut TestAppContext) {
1257 let app_state = init_test(cx);
1258 app_state
1259 .fs
1260 .as_fake()
1261 .insert_tree("/root", json!({ "a.txt": "" }))
1262 .await;
1263
1264 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1265 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1266
1267 // Open a file within an existing worktree.
1268 workspace
1269 .update(cx, |view, cx| {
1270 view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
1271 })
1272 .await;
1273 let editor = cx.read(|cx| {
1274 let pane = workspace.read(cx).active_pane().read(cx);
1275 let item = pane.active_item().unwrap();
1276 item.downcast::<Editor>().unwrap()
1277 });
1278
1279 editor.update(cx, |editor, cx| editor.handle_input("x", cx));
1280 app_state
1281 .fs
1282 .as_fake()
1283 .insert_file("/root/a.txt", "changed".to_string())
1284 .await;
1285 editor
1286 .condition(cx, |editor, cx| editor.has_conflict(cx))
1287 .await;
1288 cx.read(|cx| assert!(editor.is_dirty(cx)));
1289
1290 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1291 cx.simulate_prompt_answer(window_id, 0);
1292 save_task.await.unwrap();
1293 editor.read_with(cx, |editor, cx| {
1294 assert!(!editor.is_dirty(cx));
1295 assert!(!editor.has_conflict(cx));
1296 });
1297 }
1298
1299 #[gpui::test]
1300 async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
1301 let app_state = init_test(cx);
1302 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1303
1304 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1305 project.update(cx, |project, _| project.languages().add(rust_lang()));
1306 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1307 let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
1308
1309 // Create a new untitled buffer
1310 cx.dispatch_action(window_id, NewFile);
1311 let editor = workspace.read_with(cx, |workspace, cx| {
1312 workspace
1313 .active_item(cx)
1314 .unwrap()
1315 .downcast::<Editor>()
1316 .unwrap()
1317 });
1318
1319 editor.update(cx, |editor, cx| {
1320 assert!(!editor.is_dirty(cx));
1321 assert_eq!(editor.title(cx), "untitled");
1322 assert!(Arc::ptr_eq(
1323 &editor.language_at(0, cx).unwrap(),
1324 &languages::PLAIN_TEXT
1325 ));
1326 editor.handle_input("hi", cx);
1327 assert!(editor.is_dirty(cx));
1328 });
1329
1330 // Save the buffer. This prompts for a filename.
1331 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1332 cx.simulate_new_path_selection(|parent_dir| {
1333 assert_eq!(parent_dir, Path::new("/root"));
1334 Some(parent_dir.join("the-new-name.rs"))
1335 });
1336 cx.read(|cx| {
1337 assert!(editor.is_dirty(cx));
1338 assert_eq!(editor.read(cx).title(cx), "untitled");
1339 });
1340
1341 // When the save completes, the buffer's title is updated and the language is assigned based
1342 // on the path.
1343 save_task.await.unwrap();
1344 editor.read_with(cx, |editor, cx| {
1345 assert!(!editor.is_dirty(cx));
1346 assert_eq!(editor.title(cx), "the-new-name.rs");
1347 assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
1348 });
1349
1350 // Edit the file and save it again. This time, there is no filename prompt.
1351 editor.update(cx, |editor, cx| {
1352 editor.handle_input(" there", cx);
1353 assert!(editor.is_dirty(cx));
1354 });
1355 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1356 save_task.await.unwrap();
1357 assert!(!cx.did_prompt_for_new_path());
1358 editor.read_with(cx, |editor, cx| {
1359 assert!(!editor.is_dirty(cx));
1360 assert_eq!(editor.title(cx), "the-new-name.rs")
1361 });
1362
1363 // Open the same newly-created file in another pane item. The new editor should reuse
1364 // the same buffer.
1365 cx.dispatch_action(window_id, NewFile);
1366 workspace
1367 .update(cx, |workspace, cx| {
1368 workspace.split_and_clone(
1369 workspace.active_pane().clone(),
1370 SplitDirection::Right,
1371 cx,
1372 );
1373 workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
1374 })
1375 .await
1376 .unwrap();
1377 let editor2 = workspace.update(cx, |workspace, cx| {
1378 workspace
1379 .active_item(cx)
1380 .unwrap()
1381 .downcast::<Editor>()
1382 .unwrap()
1383 });
1384 cx.read(|cx| {
1385 assert_eq!(
1386 editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
1387 editor.read(cx).buffer().read(cx).as_singleton().unwrap()
1388 );
1389 })
1390 }
1391
1392 #[gpui::test]
1393 async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
1394 let app_state = init_test(cx);
1395 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1396
1397 let project = Project::test(app_state.fs.clone(), [], cx).await;
1398 project.update(cx, |project, _| project.languages().add(rust_lang()));
1399 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1400
1401 // Create a new untitled buffer
1402 cx.dispatch_action(window_id, NewFile);
1403 let editor = workspace.read_with(cx, |workspace, cx| {
1404 workspace
1405 .active_item(cx)
1406 .unwrap()
1407 .downcast::<Editor>()
1408 .unwrap()
1409 });
1410
1411 editor.update(cx, |editor, cx| {
1412 assert!(Arc::ptr_eq(
1413 &editor.language_at(0, cx).unwrap(),
1414 &languages::PLAIN_TEXT
1415 ));
1416 editor.handle_input("hi", cx);
1417 assert!(editor.is_dirty(cx));
1418 });
1419
1420 // Save the buffer. This prompts for a filename.
1421 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1422 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
1423 save_task.await.unwrap();
1424 // The buffer is not dirty anymore and the language is assigned based on the path.
1425 editor.read_with(cx, |editor, cx| {
1426 assert!(!editor.is_dirty(cx));
1427 assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
1428 });
1429 }
1430
1431 #[gpui::test]
1432 async fn test_pane_actions(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 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1451
1452 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1453 let file1 = entries[0].clone();
1454
1455 let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
1456
1457 workspace
1458 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1459 .await
1460 .unwrap();
1461
1462 let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
1463 let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
1464 assert_eq!(editor.project_path(cx), Some(file1.clone()));
1465 let buffer = editor.update(cx, |editor, cx| {
1466 editor.insert("dirt", cx);
1467 editor.buffer().downgrade()
1468 });
1469 (editor.downgrade(), buffer)
1470 });
1471
1472 cx.dispatch_action(window_id, pane::SplitRight);
1473 let editor_2 = cx.update(|cx| {
1474 let pane_2 = workspace.read(cx).active_pane().clone();
1475 assert_ne!(pane_1, pane_2);
1476
1477 let pane2_item = pane_2.read(cx).active_item().unwrap();
1478 assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
1479
1480 pane2_item.downcast::<Editor>().unwrap().downgrade()
1481 });
1482 cx.dispatch_action(window_id, workspace::CloseActiveItem);
1483
1484 cx.foreground().run_until_parked();
1485 workspace.read_with(cx, |workspace, _| {
1486 assert_eq!(workspace.panes().len(), 1);
1487 assert_eq!(workspace.active_pane(), &pane_1);
1488 });
1489
1490 cx.dispatch_action(window_id, workspace::CloseActiveItem);
1491 cx.foreground().run_until_parked();
1492 cx.simulate_prompt_answer(window_id, 1);
1493 cx.foreground().run_until_parked();
1494
1495 workspace.read_with(cx, |workspace, cx| {
1496 assert_eq!(workspace.panes().len(), 1);
1497 assert!(workspace.active_item(cx).is_none());
1498 });
1499
1500 cx.assert_dropped(editor_1);
1501 cx.assert_dropped(editor_2);
1502 cx.assert_dropped(buffer);
1503 }
1504
1505 #[gpui::test]
1506 async fn test_navigation(cx: &mut TestAppContext) {
1507 let app_state = init_test(cx);
1508 app_state
1509 .fs
1510 .as_fake()
1511 .insert_tree(
1512 "/root",
1513 json!({
1514 "a": {
1515 "file1": "contents 1\n".repeat(20),
1516 "file2": "contents 2\n".repeat(20),
1517 "file3": "contents 3\n".repeat(20),
1518 },
1519 }),
1520 )
1521 .await;
1522
1523 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1524 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1525 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1526
1527 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1528 let file1 = entries[0].clone();
1529 let file2 = entries[1].clone();
1530 let file3 = entries[2].clone();
1531
1532 let editor1 = workspace
1533 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1534 .await
1535 .unwrap()
1536 .downcast::<Editor>()
1537 .unwrap();
1538 editor1.update(cx, |editor, cx| {
1539 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1540 s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
1541 });
1542 });
1543 let editor2 = workspace
1544 .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1545 .await
1546 .unwrap()
1547 .downcast::<Editor>()
1548 .unwrap();
1549 let editor3 = workspace
1550 .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
1551 .await
1552 .unwrap()
1553 .downcast::<Editor>()
1554 .unwrap();
1555
1556 editor3
1557 .update(cx, |editor, cx| {
1558 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1559 s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
1560 });
1561 editor.newline(&Default::default(), cx);
1562 editor.newline(&Default::default(), cx);
1563 editor.move_down(&Default::default(), cx);
1564 editor.move_down(&Default::default(), cx);
1565 editor.save(project.clone(), cx)
1566 })
1567 .await
1568 .unwrap();
1569 editor3.update(cx, |editor, cx| {
1570 editor.set_scroll_position(vec2f(0., 12.5), cx)
1571 });
1572 assert_eq!(
1573 active_location(&workspace, cx),
1574 (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1575 );
1576
1577 workspace
1578 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1579 .await
1580 .unwrap();
1581 assert_eq!(
1582 active_location(&workspace, cx),
1583 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1584 );
1585
1586 workspace
1587 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1588 .await
1589 .unwrap();
1590 assert_eq!(
1591 active_location(&workspace, cx),
1592 (file2.clone(), DisplayPoint::new(0, 0), 0.)
1593 );
1594
1595 workspace
1596 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1597 .await
1598 .unwrap();
1599 assert_eq!(
1600 active_location(&workspace, cx),
1601 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1602 );
1603
1604 workspace
1605 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1606 .await
1607 .unwrap();
1608 assert_eq!(
1609 active_location(&workspace, cx),
1610 (file1.clone(), DisplayPoint::new(0, 0), 0.)
1611 );
1612
1613 // Go back one more time and ensure we don't navigate past the first item in the history.
1614 workspace
1615 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1616 .await
1617 .unwrap();
1618 assert_eq!(
1619 active_location(&workspace, cx),
1620 (file1.clone(), DisplayPoint::new(0, 0), 0.)
1621 );
1622
1623 workspace
1624 .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1625 .await
1626 .unwrap();
1627 assert_eq!(
1628 active_location(&workspace, cx),
1629 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1630 );
1631
1632 workspace
1633 .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1634 .await
1635 .unwrap();
1636 assert_eq!(
1637 active_location(&workspace, cx),
1638 (file2.clone(), DisplayPoint::new(0, 0), 0.)
1639 );
1640
1641 // Go forward to an item that has been closed, ensuring it gets re-opened at the same
1642 // location.
1643 pane.update(cx, |pane, cx| {
1644 let editor3_id = editor3.id();
1645 drop(editor3);
1646 pane.close_item_by_id(editor3_id, cx)
1647 })
1648 .await
1649 .unwrap();
1650 workspace
1651 .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1652 .await
1653 .unwrap();
1654 assert_eq!(
1655 active_location(&workspace, cx),
1656 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1657 );
1658
1659 workspace
1660 .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1661 .await
1662 .unwrap();
1663 assert_eq!(
1664 active_location(&workspace, cx),
1665 (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1666 );
1667
1668 workspace
1669 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1670 .await
1671 .unwrap();
1672 assert_eq!(
1673 active_location(&workspace, cx),
1674 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1675 );
1676
1677 // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
1678 pane.update(cx, |pane, cx| {
1679 let editor2_id = editor2.id();
1680 drop(editor2);
1681 pane.close_item_by_id(editor2_id, cx)
1682 })
1683 .await
1684 .unwrap();
1685 app_state
1686 .fs
1687 .remove_file(Path::new("/root/a/file2"), Default::default())
1688 .await
1689 .unwrap();
1690 workspace
1691 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1692 .await
1693 .unwrap();
1694 assert_eq!(
1695 active_location(&workspace, cx),
1696 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1697 );
1698 workspace
1699 .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1700 .await
1701 .unwrap();
1702 assert_eq!(
1703 active_location(&workspace, cx),
1704 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1705 );
1706
1707 // Modify file to collapse multiple nav history entries into the same location.
1708 // Ensure we don't visit the same location twice when navigating.
1709 editor1.update(cx, |editor, cx| {
1710 editor.change_selections(None, cx, |s| {
1711 s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
1712 })
1713 });
1714
1715 for _ in 0..5 {
1716 editor1.update(cx, |editor, cx| {
1717 editor.change_selections(None, cx, |s| {
1718 s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
1719 });
1720 });
1721 editor1.update(cx, |editor, cx| {
1722 editor.change_selections(None, cx, |s| {
1723 s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
1724 })
1725 });
1726 }
1727
1728 editor1.update(cx, |editor, cx| {
1729 editor.transact(cx, |editor, cx| {
1730 editor.change_selections(None, cx, |s| {
1731 s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
1732 });
1733 editor.insert("", cx);
1734 })
1735 });
1736
1737 editor1.update(cx, |editor, cx| {
1738 editor.change_selections(None, cx, |s| {
1739 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1740 })
1741 });
1742 workspace
1743 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1744 .await
1745 .unwrap();
1746 assert_eq!(
1747 active_location(&workspace, cx),
1748 (file1.clone(), DisplayPoint::new(2, 0), 0.)
1749 );
1750 workspace
1751 .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1752 .await
1753 .unwrap();
1754 assert_eq!(
1755 active_location(&workspace, cx),
1756 (file1.clone(), DisplayPoint::new(3, 0), 0.)
1757 );
1758
1759 fn active_location(
1760 workspace: &ViewHandle<Workspace>,
1761 cx: &mut TestAppContext,
1762 ) -> (ProjectPath, DisplayPoint, f32) {
1763 workspace.update(cx, |workspace, cx| {
1764 let item = workspace.active_item(cx).unwrap();
1765 let editor = item.downcast::<Editor>().unwrap();
1766 let (selections, scroll_position) = editor.update(cx, |editor, cx| {
1767 (
1768 editor.selections.display_ranges(cx),
1769 editor.scroll_position(cx),
1770 )
1771 });
1772 (
1773 item.project_path(cx).unwrap(),
1774 selections[0].start,
1775 scroll_position.y(),
1776 )
1777 })
1778 }
1779 }
1780
1781 #[gpui::test]
1782 async fn test_reopening_closed_items(cx: &mut TestAppContext) {
1783 let app_state = init_test(cx);
1784 app_state
1785 .fs
1786 .as_fake()
1787 .insert_tree(
1788 "/root",
1789 json!({
1790 "a": {
1791 "file1": "",
1792 "file2": "",
1793 "file3": "",
1794 "file4": "",
1795 },
1796 }),
1797 )
1798 .await;
1799
1800 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1801 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1802 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1803
1804 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1805 let file1 = entries[0].clone();
1806 let file2 = entries[1].clone();
1807 let file3 = entries[2].clone();
1808 let file4 = entries[3].clone();
1809
1810 let file1_item_id = workspace
1811 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1812 .await
1813 .unwrap()
1814 .id();
1815 let file2_item_id = workspace
1816 .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1817 .await
1818 .unwrap()
1819 .id();
1820 let file3_item_id = workspace
1821 .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
1822 .await
1823 .unwrap()
1824 .id();
1825 let file4_item_id = workspace
1826 .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
1827 .await
1828 .unwrap()
1829 .id();
1830 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1831
1832 // Close all the pane items in some arbitrary order.
1833 pane.update(cx, |pane, cx| pane.close_item_by_id(file1_item_id, cx))
1834 .await
1835 .unwrap();
1836 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1837
1838 pane.update(cx, |pane, cx| pane.close_item_by_id(file4_item_id, cx))
1839 .await
1840 .unwrap();
1841 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1842
1843 pane.update(cx, |pane, cx| pane.close_item_by_id(file2_item_id, cx))
1844 .await
1845 .unwrap();
1846 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1847
1848 pane.update(cx, |pane, cx| pane.close_item_by_id(file3_item_id, cx))
1849 .await
1850 .unwrap();
1851 assert_eq!(active_path(&workspace, cx), None);
1852
1853 // Reopen all the closed items, ensuring they are reopened in the same order
1854 // in which they were closed.
1855 workspace
1856 .update(cx, Workspace::reopen_closed_item)
1857 .await
1858 .unwrap();
1859 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1860
1861 workspace
1862 .update(cx, Workspace::reopen_closed_item)
1863 .await
1864 .unwrap();
1865 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1866
1867 workspace
1868 .update(cx, Workspace::reopen_closed_item)
1869 .await
1870 .unwrap();
1871 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1872
1873 workspace
1874 .update(cx, Workspace::reopen_closed_item)
1875 .await
1876 .unwrap();
1877 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1878
1879 // Reopening past the last closed item is a no-op.
1880 workspace
1881 .update(cx, Workspace::reopen_closed_item)
1882 .await
1883 .unwrap();
1884 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1885
1886 // Reopening closed items doesn't interfere with navigation history.
1887 workspace
1888 .update(cx, |workspace, cx| {
1889 workspace.go_back(workspace.active_pane().downgrade(), cx)
1890 })
1891 .await
1892 .unwrap();
1893 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1894
1895 workspace
1896 .update(cx, |workspace, cx| {
1897 workspace.go_back(workspace.active_pane().downgrade(), cx)
1898 })
1899 .await
1900 .unwrap();
1901 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1902
1903 workspace
1904 .update(cx, |workspace, cx| {
1905 workspace.go_back(workspace.active_pane().downgrade(), cx)
1906 })
1907 .await
1908 .unwrap();
1909 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1910
1911 workspace
1912 .update(cx, |workspace, cx| {
1913 workspace.go_back(workspace.active_pane().downgrade(), cx)
1914 })
1915 .await
1916 .unwrap();
1917 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1918
1919 workspace
1920 .update(cx, |workspace, cx| {
1921 workspace.go_back(workspace.active_pane().downgrade(), cx)
1922 })
1923 .await
1924 .unwrap();
1925 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1926
1927 workspace
1928 .update(cx, |workspace, cx| {
1929 workspace.go_back(workspace.active_pane().downgrade(), cx)
1930 })
1931 .await
1932 .unwrap();
1933 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1934
1935 workspace
1936 .update(cx, |workspace, cx| {
1937 workspace.go_back(workspace.active_pane().downgrade(), cx)
1938 })
1939 .await
1940 .unwrap();
1941 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1942
1943 workspace
1944 .update(cx, |workspace, cx| {
1945 workspace.go_back(workspace.active_pane().downgrade(), cx)
1946 })
1947 .await
1948 .unwrap();
1949 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1950
1951 fn active_path(
1952 workspace: &ViewHandle<Workspace>,
1953 cx: &TestAppContext,
1954 ) -> Option<ProjectPath> {
1955 workspace.read_with(cx, |workspace, cx| {
1956 let item = workspace.active_item(cx)?;
1957 item.project_path(cx)
1958 })
1959 }
1960 }
1961
1962 #[gpui::test]
1963 async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
1964 struct TestView;
1965
1966 impl Entity for TestView {
1967 type Event = ();
1968 }
1969
1970 impl View for TestView {
1971 fn ui_name() -> &'static str {
1972 "TestView"
1973 }
1974
1975 fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
1976 Empty::new().into_any()
1977 }
1978 }
1979
1980 let executor = cx.background();
1981 let fs = FakeFs::new(executor.clone());
1982
1983 actions!(test, [A, B]);
1984 // From the Atom keymap
1985 actions!(workspace, [ActivatePreviousPane]);
1986 // From the JetBrains keymap
1987 actions!(pane, [ActivatePrevItem]);
1988
1989 fs.save(
1990 "/settings.json".as_ref(),
1991 &r#"
1992 {
1993 "base_keymap": "Atom"
1994 }
1995 "#
1996 .into(),
1997 Default::default(),
1998 )
1999 .await
2000 .unwrap();
2001
2002 fs.save(
2003 "/keymap.json".as_ref(),
2004 &r#"
2005 [
2006 {
2007 "bindings": {
2008 "backspace": "test::A"
2009 }
2010 }
2011 ]
2012 "#
2013 .into(),
2014 Default::default(),
2015 )
2016 .await
2017 .unwrap();
2018
2019 cx.update(|cx| {
2020 cx.set_global(SettingsStore::test(cx));
2021 theme::init(Assets, cx);
2022 welcome::init(cx);
2023
2024 cx.add_global_action(|_: &A, _cx| {});
2025 cx.add_global_action(|_: &B, _cx| {});
2026 cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
2027 cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
2028
2029 let settings_rx = watch_config_file(
2030 executor.clone(),
2031 fs.clone(),
2032 PathBuf::from("/settings.json"),
2033 );
2034 let keymap_rx =
2035 watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
2036
2037 handle_keymap_file_changes(keymap_rx, cx);
2038 handle_settings_file_changes(settings_rx, cx);
2039 });
2040
2041 cx.foreground().run_until_parked();
2042
2043 let (window_id, _view) = cx.add_window(|_| TestView);
2044
2045 // Test loading the keymap base at all
2046 assert_key_bindings_for(
2047 window_id,
2048 cx,
2049 vec![("backspace", &A), ("k", &ActivatePreviousPane)],
2050 line!(),
2051 );
2052
2053 // Test modifying the users keymap, while retaining the base keymap
2054 fs.save(
2055 "/keymap.json".as_ref(),
2056 &r#"
2057 [
2058 {
2059 "bindings": {
2060 "backspace": "test::B"
2061 }
2062 }
2063 ]
2064 "#
2065 .into(),
2066 Default::default(),
2067 )
2068 .await
2069 .unwrap();
2070
2071 cx.foreground().run_until_parked();
2072
2073 assert_key_bindings_for(
2074 window_id,
2075 cx,
2076 vec![("backspace", &B), ("k", &ActivatePreviousPane)],
2077 line!(),
2078 );
2079
2080 // Test modifying the base, while retaining the users keymap
2081 fs.save(
2082 "/settings.json".as_ref(),
2083 &r#"
2084 {
2085 "base_keymap": "JetBrains"
2086 }
2087 "#
2088 .into(),
2089 Default::default(),
2090 )
2091 .await
2092 .unwrap();
2093
2094 cx.foreground().run_until_parked();
2095
2096 assert_key_bindings_for(
2097 window_id,
2098 cx,
2099 vec![("backspace", &B), ("[", &ActivatePrevItem)],
2100 line!(),
2101 );
2102
2103 #[track_caller]
2104 fn assert_key_bindings_for<'a>(
2105 window_id: usize,
2106 cx: &TestAppContext,
2107 actions: Vec<(&'static str, &'a dyn Action)>,
2108 line: u32,
2109 ) {
2110 for (key, action) in actions {
2111 // assert that...
2112 assert!(
2113 cx.available_actions(window_id, 0)
2114 .into_iter()
2115 .any(|(_, bound_action, b)| {
2116 // action names match...
2117 bound_action.name() == action.name()
2118 && bound_action.namespace() == action.namespace()
2119 // and key strokes contain the given key
2120 && b.iter()
2121 .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
2122 }),
2123 "On {} Failed to find {} with key binding {}",
2124 line,
2125 action.name(),
2126 key
2127 );
2128 }
2129 }
2130 }
2131
2132 #[gpui::test]
2133 async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
2134 struct TestView;
2135
2136 impl Entity for TestView {
2137 type Event = ();
2138 }
2139
2140 impl View for TestView {
2141 fn ui_name() -> &'static str {
2142 "TestView"
2143 }
2144
2145 fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
2146 Empty::new().into_any()
2147 }
2148 }
2149
2150 let executor = cx.background();
2151 let fs = FakeFs::new(executor.clone());
2152
2153 actions!(test, [A, B]);
2154 // From the Atom keymap
2155 actions!(workspace, [ActivatePreviousPane]);
2156 // From the JetBrains keymap
2157 actions!(pane, [ActivatePrevItem]);
2158
2159 fs.save(
2160 "/settings.json".as_ref(),
2161 &r#"
2162 {
2163 "base_keymap": "Atom"
2164 }
2165 "#
2166 .into(),
2167 Default::default(),
2168 )
2169 .await
2170 .unwrap();
2171
2172 fs.save(
2173 "/keymap.json".as_ref(),
2174 &r#"
2175 [
2176 {
2177 "bindings": {
2178 "backspace": "test::A"
2179 }
2180 }
2181 ]
2182 "#
2183 .into(),
2184 Default::default(),
2185 )
2186 .await
2187 .unwrap();
2188
2189 cx.update(|cx| {
2190 cx.set_global(SettingsStore::test(cx));
2191 theme::init(Assets, cx);
2192 welcome::init(cx);
2193
2194 cx.add_global_action(|_: &A, _cx| {});
2195 cx.add_global_action(|_: &B, _cx| {});
2196 cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
2197 cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
2198
2199 let settings_rx = watch_config_file(
2200 executor.clone(),
2201 fs.clone(),
2202 PathBuf::from("/settings.json"),
2203 );
2204 let keymap_rx =
2205 watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
2206
2207 handle_keymap_file_changes(keymap_rx, cx);
2208 handle_settings_file_changes(settings_rx, cx);
2209 });
2210
2211 cx.foreground().run_until_parked();
2212
2213 let (window_id, _view) = cx.add_window(|_| TestView);
2214
2215 // Test loading the keymap base at all
2216 assert_key_bindings_for(
2217 window_id,
2218 cx,
2219 vec![("backspace", &A), ("k", &ActivatePreviousPane)],
2220 line!(),
2221 );
2222
2223 // Test disabling the key binding for the base keymap
2224 fs.save(
2225 "/keymap.json".as_ref(),
2226 &r#"
2227 [
2228 {
2229 "bindings": {
2230 "backspace": null
2231 }
2232 }
2233 ]
2234 "#
2235 .into(),
2236 Default::default(),
2237 )
2238 .await
2239 .unwrap();
2240
2241 cx.foreground().run_until_parked();
2242
2243 assert_key_bindings_for(window_id, cx, vec![("k", &ActivatePreviousPane)], line!());
2244
2245 // Test modifying the base, while retaining the users keymap
2246 fs.save(
2247 "/settings.json".as_ref(),
2248 &r#"
2249 {
2250 "base_keymap": "JetBrains"
2251 }
2252 "#
2253 .into(),
2254 Default::default(),
2255 )
2256 .await
2257 .unwrap();
2258
2259 cx.foreground().run_until_parked();
2260
2261 assert_key_bindings_for(window_id, cx, vec![("[", &ActivatePrevItem)], line!());
2262
2263 #[track_caller]
2264 fn assert_key_bindings_for<'a>(
2265 window_id: usize,
2266 cx: &TestAppContext,
2267 actions: Vec<(&'static str, &'a dyn Action)>,
2268 line: u32,
2269 ) {
2270 for (key, action) in actions {
2271 // assert that...
2272 assert!(
2273 cx.available_actions(window_id, 0)
2274 .into_iter()
2275 .any(|(_, bound_action, b)| {
2276 // action names match...
2277 bound_action.name() == action.name()
2278 && bound_action.namespace() == action.namespace()
2279 // and key strokes contain the given key
2280 && b.iter()
2281 .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
2282 }),
2283 "On {} Failed to find {} with key binding {}",
2284 line,
2285 action.name(),
2286 key
2287 );
2288 }
2289 }
2290 }
2291
2292 #[gpui::test]
2293 fn test_bundled_settings_and_themes(cx: &mut AppContext) {
2294 cx.platform()
2295 .fonts()
2296 .add_fonts(&[
2297 Assets
2298 .load("fonts/zed-sans/zed-sans-extended.ttf")
2299 .unwrap()
2300 .to_vec()
2301 .into(),
2302 Assets
2303 .load("fonts/zed-mono/zed-mono-extended.ttf")
2304 .unwrap()
2305 .to_vec()
2306 .into(),
2307 ])
2308 .unwrap();
2309 let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
2310 let mut settings = SettingsStore::default();
2311 settings
2312 .set_default_settings(&settings::default_settings(), cx)
2313 .unwrap();
2314 cx.set_global(settings);
2315 theme::init(Assets, cx);
2316
2317 let mut has_default_theme = false;
2318 for theme_name in themes.list(false).map(|meta| meta.name) {
2319 let theme = themes.get(&theme_name).unwrap();
2320 assert_eq!(theme.meta.name, theme_name);
2321 if theme.meta.name == settings::get::<ThemeSettings>(cx).theme.meta.name {
2322 has_default_theme = true;
2323 }
2324 }
2325 assert!(has_default_theme);
2326 }
2327
2328 #[gpui::test]
2329 fn test_bundled_languages(cx: &mut AppContext) {
2330 let mut languages = LanguageRegistry::test();
2331 languages.set_executor(cx.background().clone());
2332 let languages = Arc::new(languages);
2333 let http = FakeHttpClient::with_404_response();
2334 let node_runtime = NodeRuntime::instance(http, cx.background().to_owned());
2335 languages::init(languages.clone(), node_runtime);
2336 for name in languages.language_names() {
2337 languages.language_for_name(&name);
2338 }
2339 cx.foreground().run_until_parked();
2340 }
2341
2342 fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
2343 cx.foreground().forbid_parking();
2344 cx.update(|cx| {
2345 let mut app_state = AppState::test(cx);
2346 let state = Arc::get_mut(&mut app_state).unwrap();
2347 state.initialize_workspace = initialize_workspace;
2348 state.build_window_options = build_window_options;
2349 theme::init((), cx);
2350 audio::init((), cx);
2351 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
2352 workspace::init(app_state.clone(), cx);
2353 Project::init_settings(cx);
2354 language::init(cx);
2355 editor::init(cx);
2356 project_panel::init_settings(cx);
2357 pane::init(cx);
2358 project_panel::init((), cx);
2359 terminal_view::init(cx);
2360 ai::init(cx);
2361 app_state
2362 })
2363 }
2364
2365 fn rust_lang() -> Arc<language::Language> {
2366 Arc::new(language::Language::new(
2367 language::LanguageConfig {
2368 name: "Rust".into(),
2369 path_suffixes: vec!["rs".to_string()],
2370 ..Default::default()
2371 },
2372 Some(tree_sitter_rust::language()),
2373 ))
2374 }
2375}