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