1#![allow(unused_variables)]
2//todo!(remove)
3
4mod persistence;
5pub mod terminal_element;
6pub mod terminal_panel;
7
8// todo!()
9// use crate::terminal_element::TerminalElement;
10use editor::{scroll::autoscroll::Autoscroll, Editor};
11use gpui::{
12 actions, div, img, red, Action, AnyElement, AppContext, Component, DispatchPhase, Div,
13 EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView,
14 InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton,
15 ParentComponent, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
16 WeakView,
17};
18use language::Bias;
19use persistence::TERMINAL_DB;
20use project::{search::SearchQuery, LocalWorktree, Project};
21use terminal::{
22 alacritty_terminal::{
23 index::Point,
24 term::{search::RegexSearch, TermMode},
25 },
26 terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory},
27 Event, MaybeNavigationTarget, Terminal,
28};
29use util::{paths::PathLikeWithPosition, ResultExt};
30use workspace::{
31 item::{BreadcrumbText, Item, ItemEvent},
32 notifications::NotifyResultExt,
33 register_deserializable_item,
34 searchable::{SearchEvent, SearchOptions, SearchableItem},
35 ui::{ContextMenu, Label},
36 CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
37};
38
39use anyhow::Context;
40use dirs::home_dir;
41use serde::Deserialize;
42use settings::Settings;
43use smol::Timer;
44
45use std::{
46 ops::RangeInclusive,
47 path::{Path, PathBuf},
48 sync::Arc,
49 time::Duration,
50};
51
52const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
53
54///Event to transmit the scroll from the element to the view
55#[derive(Clone, Debug, PartialEq)]
56pub struct ScrollTerminal(pub i32);
57
58#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
59pub struct SendText(String);
60
61#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
62pub struct SendKeystroke(String);
63
64actions!(Clear, Copy, Paste, ShowCharacterPalette, SearchTest);
65
66pub fn init(cx: &mut AppContext) {
67 workspace::ui::init(cx);
68 terminal_panel::init(cx);
69 terminal::init(cx);
70
71 register_deserializable_item::<TerminalView>(cx);
72
73 cx.observe_new_views(
74 |workspace: &mut Workspace, cx: &mut ViewContext<Workspace>| {
75 workspace.register_action(TerminalView::deploy);
76 },
77 )
78 .detach();
79}
80
81///A terminal view, maintains the PTY's file handles and communicates with the terminal
82pub struct TerminalView {
83 terminal: Model<Terminal>,
84 focus_handle: FocusHandle,
85 has_new_content: bool,
86 //Currently using iTerm bell, show bell emoji in tab until input is received
87 has_bell: bool,
88 context_menu: Option<View<ContextMenu>>,
89 blink_state: bool,
90 blinking_on: bool,
91 blinking_paused: bool,
92 blink_epoch: usize,
93 can_navigate_to_selected_word: bool,
94 workspace_id: WorkspaceId,
95}
96
97impl EventEmitter<Event> for TerminalView {}
98impl EventEmitter<ItemEvent> for TerminalView {}
99impl EventEmitter<SearchEvent> for TerminalView {}
100
101impl FocusableView for TerminalView {
102 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
103 self.focus_handle.clone()
104 }
105}
106
107impl TerminalView {
108 ///Create a new Terminal in the current working directory or the user's home directory
109 pub fn deploy(
110 workspace: &mut Workspace,
111 _: &NewCenterTerminal,
112 cx: &mut ViewContext<Workspace>,
113 ) {
114 let strategy = TerminalSettings::get_global(cx);
115 let working_directory =
116 get_working_directory(workspace, cx, strategy.working_directory.clone());
117
118 let window = cx.window_handle();
119 let terminal = workspace
120 .project()
121 .update(cx, |project, cx| {
122 project.create_terminal(working_directory, window, cx)
123 })
124 .notify_err(workspace, cx);
125
126 if let Some(terminal) = terminal {
127 let view = cx.build_view(|cx| {
128 TerminalView::new(
129 terminal,
130 workspace.weak_handle(),
131 workspace.database_id(),
132 cx,
133 )
134 });
135 workspace.add_item(Box::new(view), cx)
136 }
137 }
138
139 pub fn new(
140 terminal: Model<Terminal>,
141 workspace: WeakView<Workspace>,
142 workspace_id: WorkspaceId,
143 cx: &mut ViewContext<Self>,
144 ) -> Self {
145 let view_id = cx.entity_id();
146 cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
147 cx.subscribe(&terminal, move |this, _, event, cx| match event {
148 Event::Wakeup => {
149 if !this.focus_handle.is_focused(cx) {
150 this.has_new_content = true;
151 }
152 cx.notify();
153 cx.emit(Event::Wakeup);
154 cx.emit(ItemEvent::UpdateTab);
155 cx.emit(SearchEvent::MatchesInvalidated);
156 }
157
158 Event::Bell => {
159 this.has_bell = true;
160 cx.emit(Event::Wakeup);
161 }
162
163 Event::BlinkChanged => this.blinking_on = !this.blinking_on,
164
165 Event::TitleChanged => {
166 cx.emit(ItemEvent::UpdateTab);
167 if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info {
168 let cwd = foreground_info.cwd.clone();
169
170 let item_id = cx.entity_id();
171 let workspace_id = this.workspace_id;
172 cx.background_executor()
173 .spawn(async move {
174 TERMINAL_DB
175 .save_working_directory(item_id.as_u64(), workspace_id, cwd)
176 .await
177 .log_err();
178 })
179 .detach();
180 }
181 }
182
183 Event::NewNavigationTarget(maybe_navigation_target) => {
184 this.can_navigate_to_selected_word = match maybe_navigation_target {
185 Some(MaybeNavigationTarget::Url(_)) => true,
186 Some(MaybeNavigationTarget::PathLike(maybe_path)) => {
187 !possible_open_targets(&workspace, maybe_path, cx).is_empty()
188 }
189 None => false,
190 }
191 }
192
193 Event::Open(maybe_navigation_target) => match maybe_navigation_target {
194 MaybeNavigationTarget::Url(url) => cx.open_url(url),
195
196 MaybeNavigationTarget::PathLike(maybe_path) => {
197 if !this.can_navigate_to_selected_word {
198 return;
199 }
200 let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx);
201 if let Some(path) = potential_abs_paths.into_iter().next() {
202 let is_dir = path.path_like.is_dir();
203 let task_workspace = workspace.clone();
204 cx.spawn(|_, mut cx| async move {
205 let opened_items = task_workspace
206 .update(&mut cx, |workspace, cx| {
207 workspace.open_paths(vec![path.path_like], is_dir, cx)
208 })
209 .context("workspace update")?
210 .await;
211 anyhow::ensure!(
212 opened_items.len() == 1,
213 "For a single path open, expected single opened item"
214 );
215 let opened_item = opened_items
216 .into_iter()
217 .next()
218 .unwrap()
219 .transpose()
220 .context("path open")?;
221 if is_dir {
222 task_workspace.update(&mut cx, |workspace, cx| {
223 workspace.project().update(cx, |_, cx| {
224 cx.emit(project::Event::ActivateProjectPanel);
225 })
226 })?;
227 } else {
228 if let Some(row) = path.row {
229 let col = path.column.unwrap_or(0);
230 if let Some(active_editor) =
231 opened_item.and_then(|item| item.downcast::<Editor>())
232 {
233 active_editor
234 .downgrade()
235 .update(&mut cx, |editor, cx| {
236 let snapshot = editor.snapshot(cx).display_snapshot;
237 let point = snapshot.buffer_snapshot.clip_point(
238 language::Point::new(
239 row.saturating_sub(1),
240 col.saturating_sub(1),
241 ),
242 Bias::Left,
243 );
244 editor.change_selections(
245 Some(Autoscroll::center()),
246 cx,
247 |s| s.select_ranges([point..point]),
248 );
249 })
250 .log_err();
251 }
252 }
253 }
254 anyhow::Ok(())
255 })
256 .detach_and_log_err(cx);
257 }
258 }
259 },
260 Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs),
261 Event::CloseTerminal => cx.emit(ItemEvent::CloseItem),
262 Event::SelectionsChanged => cx.emit(SearchEvent::ActiveMatchChanged),
263 })
264 .detach();
265
266 Self {
267 terminal,
268 has_new_content: true,
269 has_bell: false,
270 focus_handle: cx.focus_handle(),
271 context_menu: None,
272 blink_state: true,
273 blinking_on: false,
274 blinking_paused: false,
275 blink_epoch: 0,
276 can_navigate_to_selected_word: false,
277 workspace_id,
278 }
279 }
280
281 pub fn model(&self) -> &Model<Terminal> {
282 &self.terminal
283 }
284
285 pub fn has_new_content(&self) -> bool {
286 self.has_new_content
287 }
288
289 pub fn has_bell(&self) -> bool {
290 self.has_bell
291 }
292
293 pub fn clear_bel(&mut self, cx: &mut ViewContext<TerminalView>) {
294 self.has_bell = false;
295 cx.emit(Event::Wakeup);
296 }
297
298 pub fn deploy_context_menu(
299 &mut self,
300 position: gpui::Point<Pixels>,
301 cx: &mut ViewContext<Self>,
302 ) {
303 self.context_menu = Some(cx.build_view(|cx| {
304 ContextMenu::new(cx)
305 .entry(Label::new("Clear"), Box::new(Clear))
306 .entry(
307 Label::new("Close"),
308 Box::new(CloseActiveItem { save_intent: None }),
309 )
310 }));
311 dbg!(&position);
312 // todo!()
313 // self.context_menu
314 // .show(position, AnchorCorner::TopLeft, menu_entries, cx);
315 // cx.notify();
316 }
317
318 fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
319 if !self
320 .terminal
321 .read(cx)
322 .last_content
323 .mode
324 .contains(TermMode::ALT_SCREEN)
325 {
326 cx.show_character_palette();
327 } else {
328 self.terminal.update(cx, |term, cx| {
329 term.try_keystroke(
330 &Keystroke::parse("ctrl-cmd-space").unwrap(),
331 TerminalSettings::get_global(cx).option_as_meta,
332 )
333 });
334 }
335 }
336
337 fn select_all(&mut self, _: &editor::SelectAll, cx: &mut ViewContext<Self>) {
338 self.terminal.update(cx, |term, _| term.select_all());
339 cx.notify();
340 }
341
342 fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
343 self.terminal.update(cx, |term, _| term.clear());
344 cx.notify();
345 }
346
347 pub fn should_show_cursor(&self, focused: bool, cx: &mut gpui::ViewContext<Self>) -> bool {
348 //Don't blink the cursor when not focused, blinking is disabled, or paused
349 if !focused
350 || !self.blinking_on
351 || self.blinking_paused
352 || self
353 .terminal
354 .read(cx)
355 .last_content
356 .mode
357 .contains(TermMode::ALT_SCREEN)
358 {
359 return true;
360 }
361
362 match TerminalSettings::get_global(cx).blinking {
363 //If the user requested to never blink, don't blink it.
364 TerminalBlink::Off => true,
365 //If the terminal is controlling it, check terminal mode
366 TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state,
367 }
368 }
369
370 fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
371 if epoch == self.blink_epoch && !self.blinking_paused {
372 self.blink_state = !self.blink_state;
373 cx.notify();
374
375 let epoch = self.next_blink_epoch();
376 cx.spawn(|this, mut cx| async move {
377 Timer::after(CURSOR_BLINK_INTERVAL).await;
378 this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx))
379 .log_err();
380 })
381 .detach();
382 }
383 }
384
385 pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
386 self.blink_state = true;
387 cx.notify();
388
389 let epoch = self.next_blink_epoch();
390 cx.spawn(|this, mut cx| async move {
391 Timer::after(CURSOR_BLINK_INTERVAL).await;
392 this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
393 .ok();
394 })
395 .detach();
396 }
397
398 pub fn find_matches(
399 &mut self,
400 query: Arc<project::search::SearchQuery>,
401 cx: &mut ViewContext<Self>,
402 ) -> Task<Vec<RangeInclusive<Point>>> {
403 let searcher = regex_search_for_query(&query);
404
405 if let Some(searcher) = searcher {
406 self.terminal
407 .update(cx, |term, cx| term.find_matches(searcher, cx))
408 } else {
409 cx.background_executor().spawn(async { Vec::new() })
410 }
411 }
412
413 pub fn terminal(&self) -> &Model<Terminal> {
414 &self.terminal
415 }
416
417 fn next_blink_epoch(&mut self) -> usize {
418 self.blink_epoch += 1;
419 self.blink_epoch
420 }
421
422 fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
423 if epoch == self.blink_epoch {
424 self.blinking_paused = false;
425 self.blink_cursors(epoch, cx);
426 }
427 }
428
429 ///Attempt to paste the clipboard into the terminal
430 fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
431 self.terminal.update(cx, |term, _| term.copy())
432 }
433
434 ///Attempt to paste the clipboard into the terminal
435 fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
436 if let Some(item) = cx.read_from_clipboard() {
437 self.terminal
438 .update(cx, |terminal, _cx| terminal.paste(item.text()));
439 }
440 }
441
442 fn send_text(&mut self, text: &SendText, cx: &mut ViewContext<Self>) {
443 self.clear_bel(cx);
444 self.terminal.update(cx, |term, _| {
445 term.input(text.0.to_string());
446 });
447 }
448
449 fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext<Self>) {
450 if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
451 self.clear_bel(cx);
452 self.terminal.update(cx, |term, cx| {
453 term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta);
454 });
455 }
456 }
457}
458
459fn possible_open_targets(
460 workspace: &WeakView<Workspace>,
461 maybe_path: &String,
462 cx: &mut ViewContext<'_, TerminalView>,
463) -> Vec<PathLikeWithPosition<PathBuf>> {
464 let path_like = PathLikeWithPosition::parse_str(maybe_path.as_str(), |path_str| {
465 Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf())
466 })
467 .expect("infallible");
468 let maybe_path = path_like.path_like;
469 let potential_abs_paths = if maybe_path.is_absolute() {
470 vec![maybe_path]
471 } else if maybe_path.starts_with("~") {
472 if let Some(abs_path) = maybe_path
473 .strip_prefix("~")
474 .ok()
475 .and_then(|maybe_path| Some(dirs::home_dir()?.join(maybe_path)))
476 {
477 vec![abs_path]
478 } else {
479 Vec::new()
480 }
481 } else if let Some(workspace) = workspace.upgrade() {
482 workspace.update(cx, |workspace, cx| {
483 workspace
484 .worktrees(cx)
485 .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path))
486 .collect()
487 })
488 } else {
489 Vec::new()
490 };
491
492 potential_abs_paths
493 .into_iter()
494 .filter(|path| path.exists())
495 .map(|path| PathLikeWithPosition {
496 path_like: path,
497 row: path_like.row,
498 column: path_like.column,
499 })
500 .collect()
501}
502
503pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<RegexSearch> {
504 let query = query.as_str();
505 let searcher = RegexSearch::new(&query);
506 searcher.ok()
507}
508
509impl TerminalView {
510 fn key_down(
511 &mut self,
512 event: &KeyDownEvent,
513 _dispatch_phase: DispatchPhase,
514 cx: &mut ViewContext<Self>,
515 ) {
516 self.clear_bel(cx);
517 self.pause_cursor_blinking(cx);
518
519 self.terminal.update(cx, |term, cx| {
520 term.try_keystroke(
521 &event.keystroke,
522 TerminalSettings::get_global(cx).option_as_meta,
523 )
524 });
525 }
526
527 fn focus_in(&mut self, event: &FocusEvent, cx: &mut ViewContext<Self>) {
528 self.has_new_content = false;
529 self.terminal.read(cx).focus_in();
530 self.blink_cursors(self.blink_epoch, cx);
531 cx.notify();
532 }
533
534 fn focus_out(&mut self, event: &FocusEvent, cx: &mut ViewContext<Self>) {
535 self.terminal.update(cx, |terminal, _| {
536 terminal.focus_out();
537 });
538 cx.notify();
539 }
540}
541
542impl Render for TerminalView {
543 type Element = Focusable<Self, Div<Self>>;
544
545 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
546 let terminal_handle = self.terminal.clone().downgrade();
547
548 let self_id = cx.entity_id();
549 let focused = self.focus_handle.is_focused(cx);
550
551 div()
552 .relative()
553 .child(
554 div()
555 .z_index(0)
556 .absolute()
557 .on_key_down(Self::key_down)
558 .on_action(TerminalView::send_text)
559 .on_action(TerminalView::send_keystroke)
560 .on_action(TerminalView::copy)
561 .on_action(TerminalView::paste)
562 .on_action(TerminalView::clear)
563 .on_action(TerminalView::show_character_palette)
564 .on_action(TerminalView::select_all)
565 // todo!()
566 .child(
567 "TERMINAL HERE", // TerminalElement::new(
568 // terminal_handle,
569 // focused,
570 // self.should_show_cursor(focused, cx),
571 // self.can_navigate_to_selected_word,
572 // )
573 )
574 .on_mouse_down(MouseButton::Right, |this, event, cx| {
575 this.deploy_context_menu(event.position, cx);
576 cx.notify();
577 }),
578 )
579 .children(
580 self.context_menu
581 .clone()
582 .map(|context_menu| div().z_index(1).absolute().child(context_menu.render())),
583 )
584 .track_focus(&self.focus_handle)
585 .on_focus_in(Self::focus_in)
586 .on_focus_out(Self::focus_out)
587 }
588}
589
590// impl View for TerminalView {
591//todo!()
592// fn modifiers_changed(
593// &mut self,
594// event: &ModifiersChangedEvent,
595// cx: &mut ViewContext<Self>,
596// ) -> bool {
597// let handled = self
598// .terminal()
599// .update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
600// if handled {
601// cx.notify();
602// }
603// handled
604// }
605// }
606
607// todo!()
608// fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &gpui::AppContext) {
609// Self::reset_to_default_keymap_context(keymap);
610
611// let mode = self.terminal.read(cx).last_content.mode;
612// keymap.add_key(
613// "screen",
614// if mode.contains(TermMode::ALT_SCREEN) {
615// "alt"
616// } else {
617// "normal"
618// },
619// );
620
621// if mode.contains(TermMode::APP_CURSOR) {
622// keymap.add_identifier("DECCKM");
623// }
624// if mode.contains(TermMode::APP_KEYPAD) {
625// keymap.add_identifier("DECPAM");
626// } else {
627// keymap.add_identifier("DECPNM");
628// }
629// if mode.contains(TermMode::SHOW_CURSOR) {
630// keymap.add_identifier("DECTCEM");
631// }
632// if mode.contains(TermMode::LINE_WRAP) {
633// keymap.add_identifier("DECAWM");
634// }
635// if mode.contains(TermMode::ORIGIN) {
636// keymap.add_identifier("DECOM");
637// }
638// if mode.contains(TermMode::INSERT) {
639// keymap.add_identifier("IRM");
640// }
641// //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
642// if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
643// keymap.add_identifier("LNM");
644// }
645// if mode.contains(TermMode::FOCUS_IN_OUT) {
646// keymap.add_identifier("report_focus");
647// }
648// if mode.contains(TermMode::ALTERNATE_SCROLL) {
649// keymap.add_identifier("alternate_scroll");
650// }
651// if mode.contains(TermMode::BRACKETED_PASTE) {
652// keymap.add_identifier("bracketed_paste");
653// }
654// if mode.intersects(TermMode::MOUSE_MODE) {
655// keymap.add_identifier("any_mouse_reporting");
656// }
657// {
658// let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
659// "click"
660// } else if mode.contains(TermMode::MOUSE_DRAG) {
661// "drag"
662// } else if mode.contains(TermMode::MOUSE_MOTION) {
663// "motion"
664// } else {
665// "off"
666// };
667// keymap.add_key("mouse_reporting", mouse_reporting);
668// }
669// {
670// let format = if mode.contains(TermMode::SGR_MOUSE) {
671// "sgr"
672// } else if mode.contains(TermMode::UTF8_MOUSE) {
673// "utf8"
674// } else {
675// "normal"
676// };
677// keymap.add_key("mouse_format", format);
678// }
679// }
680
681impl InputHandler for TerminalView {
682 fn text_for_range(
683 &mut self,
684 range: std::ops::Range<usize>,
685 cx: &mut ViewContext<Self>,
686 ) -> Option<String> {
687 todo!()
688 }
689
690 fn selected_text_range(
691 &mut self,
692 cx: &mut ViewContext<Self>,
693 ) -> Option<std::ops::Range<usize>> {
694 if self
695 .terminal
696 .read(cx)
697 .last_content
698 .mode
699 .contains(TermMode::ALT_SCREEN)
700 {
701 None
702 } else {
703 Some(0..0)
704 }
705 }
706
707 fn marked_text_range(&self, cx: &mut ViewContext<Self>) -> Option<std::ops::Range<usize>> {
708 todo!()
709 }
710
711 fn unmark_text(&mut self, cx: &mut ViewContext<Self>) {
712 todo!()
713 }
714
715 fn replace_text_in_range(
716 &mut self,
717 _: Option<std::ops::Range<usize>>,
718 text: &str,
719 cx: &mut ViewContext<Self>,
720 ) {
721 self.terminal.update(cx, |terminal, _| {
722 terminal.input(text.into());
723 });
724 }
725
726 fn replace_and_mark_text_in_range(
727 &mut self,
728 range: Option<std::ops::Range<usize>>,
729 new_text: &str,
730 new_selected_range: Option<std::ops::Range<usize>>,
731 cx: &mut ViewContext<Self>,
732 ) {
733 todo!()
734 }
735
736 fn bounds_for_range(
737 &mut self,
738 range_utf16: std::ops::Range<usize>,
739 element_bounds: gpui::Bounds<Pixels>,
740 cx: &mut ViewContext<Self>,
741 ) -> Option<gpui::Bounds<Pixels>> {
742 todo!()
743 }
744}
745
746impl Item for TerminalView {
747 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
748 Some(self.terminal().read(cx).title().into())
749 }
750
751 fn tab_content<T: 'static>(
752 &self,
753 _detail: Option<usize>,
754 cx: &gpui::AppContext,
755 ) -> AnyElement<T> {
756 let title = self.terminal().read(cx).title();
757
758 div()
759 .child(img().uri("icons/terminal.svg").bg(red()))
760 .child(title)
761 .render()
762 }
763
764 fn clone_on_split(
765 &self,
766 _workspace_id: WorkspaceId,
767 _cx: &mut ViewContext<Self>,
768 ) -> Option<View<Self>> {
769 //From what I can tell, there's no way to tell the current working
770 //Directory of the terminal from outside the shell. There might be
771 //solutions to this, but they are non-trivial and require more IPC
772
773 // Some(TerminalContainer::new(
774 // Err(anyhow::anyhow!("failed to instantiate terminal")),
775 // workspace_id,
776 // cx,
777 // ))
778
779 // TODO
780 None
781 }
782
783 fn is_dirty(&self, _cx: &gpui::AppContext) -> bool {
784 self.has_bell()
785 }
786
787 fn has_conflict(&self, _cx: &AppContext) -> bool {
788 false
789 }
790
791 // todo!()
792 // fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
793 // Some(Box::new(handle.clone()))
794 // }
795
796 fn breadcrumb_location(&self) -> ToolbarItemLocation {
797 ToolbarItemLocation::PrimaryLeft { flex: None }
798 }
799
800 fn breadcrumbs(&self, _: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
801 Some(vec![BreadcrumbText {
802 text: self.terminal().read(cx).breadcrumb_text.clone(),
803 highlights: None,
804 }])
805 }
806
807 fn serialized_item_kind() -> Option<&'static str> {
808 Some("Terminal")
809 }
810
811 fn deserialize(
812 project: Model<Project>,
813 workspace: WeakView<Workspace>,
814 workspace_id: workspace::WorkspaceId,
815 item_id: workspace::ItemId,
816 cx: &mut ViewContext<Pane>,
817 ) -> Task<anyhow::Result<View<Self>>> {
818 let window = cx.window_handle();
819 cx.spawn(|pane, mut cx| async move {
820 let cwd = None;
821 // todo!()
822 // TERMINAL_DB
823 // .get_working_directory(item_id, workspace_id)
824 // .log_err()
825 // .flatten()
826 // .or_else(|| {
827 // cx.read(|cx| {
828 // let strategy = TerminalSettings::get_global(cx).working_directory.clone();
829 // workspace
830 // .upgrade()
831 // .map(|workspace| {
832 // get_working_directory(workspace.read(cx), cx, strategy)
833 // })
834 // .flatten()
835 // })
836 // });
837
838 let terminal = project.update(&mut cx, |project, cx| {
839 project.create_terminal(cwd, window, cx)
840 })??;
841 pane.update(&mut cx, |_, cx| {
842 cx.build_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx))
843 })
844 })
845 }
846
847 fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
848 // todo!()
849 // cx.background()
850 // .spawn(TERMINAL_DB.update_workspace_id(
851 // workspace.database_id(),
852 // self.workspace_id,
853 // cx.view_id(),
854 // ))
855 // .detach();
856 self.workspace_id = workspace.database_id();
857 }
858}
859
860impl SearchableItem for TerminalView {
861 type Match = RangeInclusive<Point>;
862
863 fn supported_options() -> SearchOptions {
864 SearchOptions {
865 case: false,
866 word: false,
867 regex: false,
868 replacement: false,
869 }
870 }
871
872 /// Clear stored matches
873 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
874 self.terminal().update(cx, |term, _| term.matches.clear())
875 }
876
877 /// Store matches returned from find_matches somewhere for rendering
878 fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
879 self.terminal().update(cx, |term, _| term.matches = matches)
880 }
881
882 /// Return the selection content to pre-load into this search
883 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
884 self.terminal()
885 .read(cx)
886 .last_content
887 .selection_text
888 .clone()
889 .unwrap_or_default()
890 }
891
892 /// Focus match at given index into the Vec of matches
893 fn activate_match(&mut self, index: usize, _: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
894 self.terminal()
895 .update(cx, |term, _| term.activate_match(index));
896 cx.notify();
897 }
898
899 /// Add selections for all matches given.
900 fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
901 self.terminal()
902 .update(cx, |term, _| term.select_matches(matches));
903 cx.notify();
904 }
905
906 /// Get all of the matches for this query, should be done on the background
907 fn find_matches(
908 &mut self,
909 query: Arc<project::search::SearchQuery>,
910 cx: &mut ViewContext<Self>,
911 ) -> Task<Vec<Self::Match>> {
912 if let Some(searcher) = regex_search_for_query(&query) {
913 self.terminal()
914 .update(cx, |term, cx| term.find_matches(searcher, cx))
915 } else {
916 Task::ready(vec![])
917 }
918 }
919
920 /// Reports back to the search toolbar what the active match should be (the selection)
921 fn active_match_index(
922 &mut self,
923 matches: Vec<Self::Match>,
924 cx: &mut ViewContext<Self>,
925 ) -> Option<usize> {
926 // Selection head might have a value if there's a selection that isn't
927 // associated with a match. Therefore, if there are no matches, we should
928 // report None, no matter the state of the terminal
929 let res = if matches.len() > 0 {
930 if let Some(selection_head) = self.terminal().read(cx).selection_head {
931 // If selection head is contained in a match. Return that match
932 if let Some(ix) = matches
933 .iter()
934 .enumerate()
935 .find(|(_, search_match)| {
936 search_match.contains(&selection_head)
937 || search_match.start() > &selection_head
938 })
939 .map(|(ix, _)| ix)
940 {
941 Some(ix)
942 } else {
943 // If no selection after selection head, return the last match
944 Some(matches.len().saturating_sub(1))
945 }
946 } else {
947 // Matches found but no active selection, return the first last one (closest to cursor)
948 Some(matches.len().saturating_sub(1))
949 }
950 } else {
951 None
952 };
953
954 res
955 }
956 fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
957 // Replacement is not supported in terminal view, so this is a no-op.
958 }
959}
960
961///Get's the working directory for the given workspace, respecting the user's settings.
962pub fn get_working_directory(
963 workspace: &Workspace,
964 cx: &AppContext,
965 strategy: WorkingDirectory,
966) -> Option<PathBuf> {
967 let res = match strategy {
968 WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx)
969 .or_else(|| first_project_directory(workspace, cx)),
970 WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
971 WorkingDirectory::AlwaysHome => None,
972 WorkingDirectory::Always { directory } => {
973 shellexpand::full(&directory) //TODO handle this better
974 .ok()
975 .map(|dir| Path::new(&dir.to_string()).to_path_buf())
976 .filter(|dir| dir.is_dir())
977 }
978 };
979 res.or_else(home_dir)
980}
981
982///Get's the first project's home directory, or the home directory
983fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
984 workspace
985 .worktrees(cx)
986 .next()
987 .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
988 .and_then(get_path_from_wt)
989}
990
991///Gets the intuitively correct working directory from the given workspace
992///If there is an active entry for this project, returns that entry's worktree root.
993///If there's no active entry but there is a worktree, returns that worktrees root.
994///If either of these roots are files, or if there are any other query failures,
995/// returns the user's home directory
996fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
997 let project = workspace.project().read(cx);
998
999 project
1000 .active_entry()
1001 .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
1002 .or_else(|| workspace.worktrees(cx).next())
1003 .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
1004 .and_then(get_path_from_wt)
1005}
1006
1007fn get_path_from_wt(wt: &LocalWorktree) -> Option<PathBuf> {
1008 wt.root_entry()
1009 .filter(|re| re.is_dir())
1010 .map(|_| wt.abs_path().to_path_buf())
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use super::*;
1016 use gpui::TestAppContext;
1017 use project::{Entry, Project, ProjectPath, Worktree};
1018 use std::path::Path;
1019 use workspace::AppState;
1020
1021 // Working directory calculation tests
1022
1023 // No Worktrees in project -> home_dir()
1024 #[gpui::test]
1025 async fn no_worktree(cx: &mut TestAppContext) {
1026 let (project, workspace) = init_test(cx).await;
1027 cx.read(|cx| {
1028 let workspace = workspace.read(cx);
1029 let active_entry = project.read(cx).active_entry();
1030
1031 //Make sure environment is as expected
1032 assert!(active_entry.is_none());
1033 assert!(workspace.worktrees(cx).next().is_none());
1034
1035 let res = current_project_directory(workspace, cx);
1036 assert_eq!(res, None);
1037 let res = first_project_directory(workspace, cx);
1038 assert_eq!(res, None);
1039 });
1040 }
1041
1042 // No active entry, but a worktree, worktree is a file -> home_dir()
1043 #[gpui::test]
1044 async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
1045 let (project, workspace) = init_test(cx).await;
1046
1047 create_file_wt(project.clone(), "/root.txt", cx).await;
1048 cx.read(|cx| {
1049 let workspace = workspace.read(cx);
1050 let active_entry = project.read(cx).active_entry();
1051
1052 //Make sure environment is as expected
1053 assert!(active_entry.is_none());
1054 assert!(workspace.worktrees(cx).next().is_some());
1055
1056 let res = current_project_directory(workspace, cx);
1057 assert_eq!(res, None);
1058 let res = first_project_directory(workspace, cx);
1059 assert_eq!(res, None);
1060 });
1061 }
1062
1063 // No active entry, but a worktree, worktree is a folder -> worktree_folder
1064 #[gpui::test]
1065 async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
1066 let (project, workspace) = init_test(cx).await;
1067
1068 let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
1069 cx.update(|cx| {
1070 let workspace = workspace.read(cx);
1071 let active_entry = project.read(cx).active_entry();
1072
1073 assert!(active_entry.is_none());
1074 assert!(workspace.worktrees(cx).next().is_some());
1075
1076 let res = current_project_directory(workspace, cx);
1077 assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
1078 let res = first_project_directory(workspace, cx);
1079 assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
1080 });
1081 }
1082
1083 // Active entry with a work tree, worktree is a file -> home_dir()
1084 #[gpui::test]
1085 async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
1086 let (project, workspace) = init_test(cx).await;
1087
1088 let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
1089 let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await;
1090 insert_active_entry_for(wt2, entry2, project.clone(), cx);
1091
1092 cx.update(|cx| {
1093 let workspace = workspace.read(cx);
1094 let active_entry = project.read(cx).active_entry();
1095
1096 assert!(active_entry.is_some());
1097
1098 let res = current_project_directory(workspace, cx);
1099 assert_eq!(res, None);
1100 let res = first_project_directory(workspace, cx);
1101 assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
1102 });
1103 }
1104
1105 // Active entry, with a worktree, worktree is a folder -> worktree_folder
1106 #[gpui::test]
1107 async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
1108 let (project, workspace) = init_test(cx).await;
1109
1110 let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
1111 let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await;
1112 insert_active_entry_for(wt2, entry2, project.clone(), cx);
1113
1114 cx.update(|cx| {
1115 let workspace = workspace.read(cx);
1116 let active_entry = project.read(cx).active_entry();
1117
1118 assert!(active_entry.is_some());
1119
1120 let res = current_project_directory(workspace, cx);
1121 assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
1122 let res = first_project_directory(workspace, cx);
1123 assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
1124 });
1125 }
1126
1127 /// Creates a worktree with 1 file: /root.txt
1128 pub async fn init_test(cx: &mut TestAppContext) -> (Model<Project>, View<Workspace>) {
1129 let params = cx.update(AppState::test);
1130 cx.update(|cx| {
1131 theme::init(theme::LoadThemes::JustBase, cx);
1132 Project::init_settings(cx);
1133 language::init(cx);
1134 });
1135
1136 let project = Project::test(params.fs.clone(), [], cx).await;
1137 let workspace = cx
1138 .add_window(|cx| Workspace::test_new(project.clone(), cx))
1139 .root_view(cx)
1140 .unwrap();
1141
1142 (project, workspace)
1143 }
1144
1145 /// Creates a worktree with 1 folder: /root{suffix}/
1146 async fn create_folder_wt(
1147 project: Model<Project>,
1148 path: impl AsRef<Path>,
1149 cx: &mut TestAppContext,
1150 ) -> (Model<Worktree>, Entry) {
1151 create_wt(project, true, path, cx).await
1152 }
1153
1154 /// Creates a worktree with 1 file: /root{suffix}.txt
1155 async fn create_file_wt(
1156 project: Model<Project>,
1157 path: impl AsRef<Path>,
1158 cx: &mut TestAppContext,
1159 ) -> (Model<Worktree>, Entry) {
1160 create_wt(project, false, path, cx).await
1161 }
1162
1163 async fn create_wt(
1164 project: Model<Project>,
1165 is_dir: bool,
1166 path: impl AsRef<Path>,
1167 cx: &mut TestAppContext,
1168 ) -> (Model<Worktree>, Entry) {
1169 let (wt, _) = project
1170 .update(cx, |project, cx| {
1171 project.find_or_create_local_worktree(path, true, cx)
1172 })
1173 .await
1174 .unwrap();
1175
1176 let entry = cx
1177 .update(|cx| {
1178 wt.update(cx, |wt, cx| {
1179 wt.as_local()
1180 .unwrap()
1181 .create_entry(Path::new(""), is_dir, cx)
1182 })
1183 })
1184 .await
1185 .unwrap();
1186
1187 (wt, entry)
1188 }
1189
1190 pub fn insert_active_entry_for(
1191 wt: Model<Worktree>,
1192 entry: Entry,
1193 project: Model<Project>,
1194 cx: &mut TestAppContext,
1195 ) {
1196 cx.update(|cx| {
1197 let p = ProjectPath {
1198 worktree_id: wt.read(cx).id(),
1199 path: entry.path,
1200 };
1201 project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
1202 });
1203 }
1204}