1mod persistence;
2pub mod terminal_element;
3pub mod terminal_panel;
4pub mod terminal_scrollbar;
5pub mod terminal_tab_tooltip;
6
7use editor::{Editor, EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide};
8use gpui::{
9 AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
10 KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, ScrollWheelEvent,
11 Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div,
12 impl_actions,
13};
14use itertools::Itertools;
15use persistence::TERMINAL_DB;
16use project::{Entry, Metadata, Project, search::SearchQuery, terminals::TerminalKind};
17use schemars::JsonSchema;
18use task::TaskId;
19use terminal::{
20 Clear, Copy, Event, HoveredWord, MaybeNavigationTarget, Paste, ScrollLineDown, ScrollLineUp,
21 ScrollPageDown, ScrollPageUp, ScrollToBottom, ScrollToTop, ShowCharacterPalette, TaskState,
22 TaskStatus, Terminal, TerminalBounds, ToggleViMode,
23 alacritty_terminal::{
24 index::Point,
25 term::{TermMode, search::RegexSearch},
26 },
27 terminal_settings::{self, CursorShape, TerminalBlink, TerminalSettings, WorkingDirectory},
28};
29use terminal_element::{TerminalElement, is_blank};
30use terminal_panel::TerminalPanel;
31use terminal_scrollbar::TerminalScrollHandle;
32use terminal_tab_tooltip::TerminalTooltip;
33use ui::{
34 ContextMenu, Icon, IconName, Label, Scrollbar, ScrollbarState, Tooltip, h_flex, prelude::*,
35};
36use util::{ResultExt, debug_panic, paths::PathWithPosition};
37use workspace::{
38 CloseActiveItem, NewCenterTerminal, NewTerminal, OpenOptions, OpenVisible, ToolbarItemLocation,
39 Workspace, WorkspaceId, delete_unloaded_items,
40 item::{
41 BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent,
42 },
43 register_serializable_item,
44 searchable::{Direction, SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
45};
46
47use anyhow::Context as _;
48use serde::Deserialize;
49use settings::{Settings, SettingsStore};
50use smol::Timer;
51use zed_actions::assistant::InlineAssist;
52
53use std::{
54 cmp,
55 ops::{Range, RangeInclusive},
56 path::{Path, PathBuf},
57 rc::Rc,
58 sync::Arc,
59 time::Duration,
60};
61
62const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
63
64const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"];
65
66/// Event to transmit the scroll from the element to the view
67#[derive(Clone, Debug, PartialEq)]
68pub struct ScrollTerminal(pub i32);
69
70#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq)]
71pub struct SendText(String);
72
73#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq)]
74pub struct SendKeystroke(String);
75
76actions!(terminal, [RerunTask]);
77
78impl_actions!(terminal, [SendText, SendKeystroke]);
79
80pub fn init(cx: &mut App) {
81 terminal_panel::init(cx);
82 terminal::init(cx);
83
84 register_serializable_item::<TerminalView>(cx);
85
86 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
87 workspace.register_action(TerminalView::deploy);
88 })
89 .detach();
90}
91
92pub struct BlockProperties {
93 pub height: u8,
94 pub render: Box<dyn Send + Fn(&mut BlockContext) -> AnyElement>,
95}
96
97pub struct BlockContext<'a, 'b> {
98 pub window: &'a mut Window,
99 pub context: &'b mut App,
100 pub dimensions: TerminalBounds,
101}
102
103///A terminal view, maintains the PTY's file handles and communicates with the terminal
104pub struct TerminalView {
105 terminal: Entity<Terminal>,
106 workspace: WeakEntity<Workspace>,
107 project: WeakEntity<Project>,
108 focus_handle: FocusHandle,
109 //Currently using iTerm bell, show bell emoji in tab until input is received
110 has_bell: bool,
111 context_menu: Option<(Entity<ContextMenu>, gpui::Point<Pixels>, Subscription)>,
112 cursor_shape: CursorShape,
113 blink_state: bool,
114 embedded: bool,
115 blinking_terminal_enabled: bool,
116 cwd_serialized: bool,
117 blinking_paused: bool,
118 blink_epoch: usize,
119 hover: Option<HoverTarget>,
120 hover_tooltip_update: Task<()>,
121 workspace_id: Option<WorkspaceId>,
122 show_breadcrumbs: bool,
123 block_below_cursor: Option<Rc<BlockProperties>>,
124 scroll_top: Pixels,
125 scrollbar_state: ScrollbarState,
126 scroll_handle: TerminalScrollHandle,
127 show_scrollbar: bool,
128 hide_scrollbar_task: Option<Task<()>>,
129 marked_text: Option<String>,
130 marked_range_utf16: Option<Range<usize>>,
131 _subscriptions: Vec<Subscription>,
132 _terminal_subscriptions: Vec<Subscription>,
133}
134
135#[derive(Debug)]
136struct HoverTarget {
137 tooltip: String,
138 hovered_word: HoveredWord,
139}
140
141impl EventEmitter<Event> for TerminalView {}
142impl EventEmitter<ItemEvent> for TerminalView {}
143impl EventEmitter<SearchEvent> for TerminalView {}
144
145impl Focusable for TerminalView {
146 fn focus_handle(&self, _cx: &App) -> FocusHandle {
147 self.focus_handle.clone()
148 }
149}
150
151impl TerminalView {
152 ///Create a new Terminal in the current working directory or the user's home directory
153 pub fn deploy(
154 workspace: &mut Workspace,
155 _: &NewCenterTerminal,
156 window: &mut Window,
157 cx: &mut Context<Workspace>,
158 ) {
159 let working_directory = default_working_directory(workspace, cx);
160 TerminalPanel::add_center_terminal(
161 workspace,
162 TerminalKind::Shell(working_directory),
163 window,
164 cx,
165 )
166 .detach_and_log_err(cx);
167 }
168
169 pub fn new(
170 terminal: Entity<Terminal>,
171 workspace: WeakEntity<Workspace>,
172 workspace_id: Option<WorkspaceId>,
173 project: WeakEntity<Project>,
174 embedded: bool,
175 window: &mut Window,
176 cx: &mut Context<Self>,
177 ) -> Self {
178 let workspace_handle = workspace.clone();
179 let terminal_subscriptions =
180 subscribe_for_terminal_events(&terminal, workspace, window, cx);
181
182 let focus_handle = cx.focus_handle();
183 let focus_in = cx.on_focus_in(&focus_handle, window, |terminal_view, window, cx| {
184 terminal_view.focus_in(window, cx);
185 });
186 let focus_out = cx.on_focus_out(
187 &focus_handle,
188 window,
189 |terminal_view, _event, window, cx| {
190 terminal_view.focus_out(window, cx);
191 },
192 );
193 let cursor_shape = TerminalSettings::get_global(cx)
194 .cursor_shape
195 .unwrap_or_default();
196
197 let scroll_handle = TerminalScrollHandle::new(terminal.read(cx));
198
199 Self {
200 terminal,
201 workspace: workspace_handle,
202 project,
203 has_bell: false,
204 focus_handle,
205 context_menu: None,
206 cursor_shape,
207 blink_state: true,
208 blinking_terminal_enabled: false,
209 blinking_paused: false,
210 blink_epoch: 0,
211 hover: None,
212 hover_tooltip_update: Task::ready(()),
213 embedded,
214 workspace_id,
215 show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs,
216 block_below_cursor: None,
217 scroll_top: Pixels::ZERO,
218 scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
219 scroll_handle,
220 show_scrollbar: !Self::should_autohide_scrollbar(cx),
221 hide_scrollbar_task: None,
222 cwd_serialized: false,
223 marked_text: None,
224 marked_range_utf16: None,
225 _subscriptions: vec![
226 focus_in,
227 focus_out,
228 cx.observe_global::<SettingsStore>(Self::settings_changed),
229 ],
230 _terminal_subscriptions: terminal_subscriptions,
231 }
232 }
233
234 /// Sets the marked (pre-edit) text from the IME.
235 pub(crate) fn set_marked_text(
236 &mut self,
237 text: String,
238 range: Range<usize>,
239 cx: &mut Context<Self>,
240 ) {
241 self.marked_text = Some(text);
242 self.marked_range_utf16 = Some(range);
243 cx.notify();
244 }
245
246 /// Gets the current marked range (UTF-16).
247 pub(crate) fn marked_text_range(&self) -> Option<Range<usize>> {
248 self.marked_range_utf16.clone()
249 }
250
251 /// Clears the marked (pre-edit) text state.
252 pub(crate) fn clear_marked_text(&mut self, cx: &mut Context<Self>) {
253 if self.marked_text.is_some() {
254 self.marked_text = None;
255 self.marked_range_utf16 = None;
256 cx.notify();
257 }
258 }
259
260 /// Commits (sends) the given text to the PTY. Called by InputHandler::replace_text_in_range.
261 pub(crate) fn commit_text(&mut self, text: &str, cx: &mut Context<Self>) {
262 if !text.is_empty() {
263 self.terminal.update(cx, |term, _| {
264 term.input(text.to_string());
265 });
266 }
267 }
268
269 pub(crate) fn terminal_bounds(&self, cx: &App) -> TerminalBounds {
270 self.terminal.read(cx).last_content().terminal_bounds
271 }
272
273 pub fn entity(&self) -> &Entity<Terminal> {
274 &self.terminal
275 }
276
277 pub fn has_bell(&self) -> bool {
278 self.has_bell
279 }
280
281 pub fn clear_bell(&mut self, cx: &mut Context<TerminalView>) {
282 self.has_bell = false;
283 cx.emit(Event::Wakeup);
284 }
285
286 pub fn deploy_context_menu(
287 &mut self,
288 position: gpui::Point<Pixels>,
289 window: &mut Window,
290 cx: &mut Context<Self>,
291 ) {
292 let assistant_enabled = self
293 .workspace
294 .upgrade()
295 .and_then(|workspace| workspace.read(cx).panel::<TerminalPanel>(cx))
296 .map_or(false, |terminal_panel| {
297 terminal_panel.read(cx).assistant_enabled()
298 });
299 let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
300 menu.context(self.focus_handle.clone())
301 .action("New Terminal", Box::new(NewTerminal))
302 .separator()
303 .action("Copy", Box::new(Copy))
304 .action("Paste", Box::new(Paste))
305 .action("Select All", Box::new(SelectAll))
306 .action("Clear", Box::new(Clear))
307 .when(assistant_enabled, |menu| {
308 menu.separator()
309 .action("Inline Assist", Box::new(InlineAssist::default()))
310 })
311 .separator()
312 .action(
313 "Close Terminal Tab",
314 Box::new(CloseActiveItem {
315 save_intent: None,
316 close_pinned: true,
317 }),
318 )
319 });
320
321 window.focus(&context_menu.focus_handle(cx));
322 let subscription = cx.subscribe_in(
323 &context_menu,
324 window,
325 |this, _, _: &DismissEvent, window, cx| {
326 if this.context_menu.as_ref().is_some_and(|context_menu| {
327 context_menu.0.focus_handle(cx).contains_focused(window, cx)
328 }) {
329 cx.focus_self(window);
330 }
331 this.context_menu.take();
332 cx.notify();
333 },
334 );
335
336 self.context_menu = Some((context_menu, position, subscription));
337 }
338
339 fn settings_changed(&mut self, cx: &mut Context<Self>) {
340 let settings = TerminalSettings::get_global(cx);
341 self.show_breadcrumbs = settings.toolbar.breadcrumbs;
342
343 let new_cursor_shape = settings.cursor_shape.unwrap_or_default();
344 let old_cursor_shape = self.cursor_shape;
345 if old_cursor_shape != new_cursor_shape {
346 self.cursor_shape = new_cursor_shape;
347 self.terminal.update(cx, |term, _| {
348 term.set_cursor_shape(self.cursor_shape);
349 });
350 }
351
352 cx.notify();
353 }
354
355 fn show_character_palette(
356 &mut self,
357 _: &ShowCharacterPalette,
358 window: &mut Window,
359 cx: &mut Context<Self>,
360 ) {
361 if self
362 .terminal
363 .read(cx)
364 .last_content
365 .mode
366 .contains(TermMode::ALT_SCREEN)
367 {
368 self.terminal.update(cx, |term, cx| {
369 term.try_keystroke(
370 &Keystroke::parse("ctrl-cmd-space").unwrap(),
371 TerminalSettings::get_global(cx).option_as_meta,
372 )
373 });
374 } else {
375 window.show_character_palette();
376 }
377 }
378
379 fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
380 self.terminal.update(cx, |term, _| term.select_all());
381 cx.notify();
382 }
383
384 fn rerun_task(&mut self, _: &RerunTask, window: &mut Window, cx: &mut Context<Self>) {
385 let task = self
386 .terminal
387 .update(cx, |terminal, _| {
388 terminal
389 .task()
390 .map(|task| terminal_rerun_override(&task.id))
391 })
392 .unwrap_or_default();
393 window.dispatch_action(Box::new(task), cx);
394 }
395
396 fn clear(&mut self, _: &Clear, _: &mut Window, cx: &mut Context<Self>) {
397 self.scroll_top = px(0.);
398 self.terminal.update(cx, |term, _| term.clear());
399 cx.notify();
400 }
401
402 fn max_scroll_top(&self, cx: &App) -> Pixels {
403 let terminal = self.terminal.read(cx);
404
405 let Some(block) = self.block_below_cursor.as_ref() else {
406 return Pixels::ZERO;
407 };
408
409 let line_height = terminal.last_content().terminal_bounds.line_height;
410 let mut terminal_lines = terminal.total_lines();
411 let viewport_lines = terminal.viewport_lines();
412 if terminal.total_lines() == terminal.viewport_lines() {
413 let mut last_line = None;
414 for cell in terminal.last_content.cells.iter().rev() {
415 if !is_blank(cell) {
416 break;
417 }
418
419 let last_line = last_line.get_or_insert(cell.point.line);
420 if *last_line != cell.point.line {
421 terminal_lines -= 1;
422 }
423 *last_line = cell.point.line;
424 }
425 }
426
427 let max_scroll_top_in_lines =
428 (block.height as usize).saturating_sub(viewport_lines.saturating_sub(terminal_lines));
429
430 max_scroll_top_in_lines as f32 * line_height
431 }
432
433 fn scroll_wheel(&mut self, event: &ScrollWheelEvent, cx: &mut Context<Self>) {
434 let terminal_content = self.terminal.read(cx).last_content();
435
436 if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 {
437 let line_height = terminal_content.terminal_bounds.line_height;
438 let y_delta = event.delta.pixel_delta(line_height).y;
439 if y_delta < Pixels::ZERO || self.scroll_top > Pixels::ZERO {
440 self.scroll_top = cmp::max(
441 Pixels::ZERO,
442 cmp::min(self.scroll_top - y_delta, self.max_scroll_top(cx)),
443 );
444 cx.notify();
445 return;
446 }
447 }
448 self.terminal.update(cx, |term, _| term.scroll_wheel(event));
449 }
450
451 fn scroll_line_up(&mut self, _: &ScrollLineUp, _: &mut Window, cx: &mut Context<Self>) {
452 let terminal_content = self.terminal.read(cx).last_content();
453 if self.block_below_cursor.is_some()
454 && terminal_content.display_offset == 0
455 && self.scroll_top > Pixels::ZERO
456 {
457 let line_height = terminal_content.terminal_bounds.line_height;
458 self.scroll_top = cmp::max(self.scroll_top - line_height, Pixels::ZERO);
459 return;
460 }
461
462 self.terminal.update(cx, |term, _| term.scroll_line_up());
463 cx.notify();
464 }
465
466 fn scroll_line_down(&mut self, _: &ScrollLineDown, _: &mut Window, cx: &mut Context<Self>) {
467 let terminal_content = self.terminal.read(cx).last_content();
468 if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 {
469 let max_scroll_top = self.max_scroll_top(cx);
470 if self.scroll_top < max_scroll_top {
471 let line_height = terminal_content.terminal_bounds.line_height;
472 self.scroll_top = cmp::min(self.scroll_top + line_height, max_scroll_top);
473 }
474 return;
475 }
476
477 self.terminal.update(cx, |term, _| term.scroll_line_down());
478 cx.notify();
479 }
480
481 fn scroll_page_up(&mut self, _: &ScrollPageUp, _: &mut Window, cx: &mut Context<Self>) {
482 if self.scroll_top == Pixels::ZERO {
483 self.terminal.update(cx, |term, _| term.scroll_page_up());
484 } else {
485 let line_height = self
486 .terminal
487 .read(cx)
488 .last_content
489 .terminal_bounds
490 .line_height();
491 let visible_block_lines = (self.scroll_top / line_height) as usize;
492 let viewport_lines = self.terminal.read(cx).viewport_lines();
493 let visible_content_lines = viewport_lines - visible_block_lines;
494
495 if visible_block_lines >= viewport_lines {
496 self.scroll_top = ((visible_block_lines - viewport_lines) as f32) * line_height;
497 } else {
498 self.scroll_top = px(0.);
499 self.terminal
500 .update(cx, |term, _| term.scroll_up_by(visible_content_lines));
501 }
502 }
503 cx.notify();
504 }
505
506 fn scroll_page_down(&mut self, _: &ScrollPageDown, _: &mut Window, cx: &mut Context<Self>) {
507 self.terminal.update(cx, |term, _| term.scroll_page_down());
508 let terminal = self.terminal.read(cx);
509 if terminal.last_content().display_offset < terminal.viewport_lines() {
510 self.scroll_top = self.max_scroll_top(cx);
511 }
512 cx.notify();
513 }
514
515 fn scroll_to_top(&mut self, _: &ScrollToTop, _: &mut Window, cx: &mut Context<Self>) {
516 self.terminal.update(cx, |term, _| term.scroll_to_top());
517 cx.notify();
518 }
519
520 fn scroll_to_bottom(&mut self, _: &ScrollToBottom, _: &mut Window, cx: &mut Context<Self>) {
521 self.terminal.update(cx, |term, _| term.scroll_to_bottom());
522 if self.block_below_cursor.is_some() {
523 self.scroll_top = self.max_scroll_top(cx);
524 }
525 cx.notify();
526 }
527
528 fn toggle_vi_mode(&mut self, _: &ToggleViMode, _: &mut Window, cx: &mut Context<Self>) {
529 self.terminal.update(cx, |term, _| term.toggle_vi_mode());
530 cx.notify();
531 }
532
533 pub fn should_show_cursor(&self, focused: bool, cx: &mut Context<Self>) -> bool {
534 //Don't blink the cursor when not focused, blinking is disabled, or paused
535 if !focused
536 || self.blinking_paused
537 || self
538 .terminal
539 .read(cx)
540 .last_content
541 .mode
542 .contains(TermMode::ALT_SCREEN)
543 {
544 return true;
545 }
546
547 match TerminalSettings::get_global(cx).blinking {
548 //If the user requested to never blink, don't blink it.
549 TerminalBlink::Off => true,
550 //If the terminal is controlling it, check terminal mode
551 TerminalBlink::TerminalControlled => {
552 !self.blinking_terminal_enabled || self.blink_state
553 }
554 TerminalBlink::On => self.blink_state,
555 }
556 }
557
558 fn blink_cursors(&mut self, epoch: usize, window: &mut Window, cx: &mut Context<Self>) {
559 if epoch == self.blink_epoch && !self.blinking_paused {
560 self.blink_state = !self.blink_state;
561 cx.notify();
562
563 let epoch = self.next_blink_epoch();
564 cx.spawn_in(window, async move |this, cx| {
565 Timer::after(CURSOR_BLINK_INTERVAL).await;
566 this.update_in(cx, |this, window, cx| this.blink_cursors(epoch, window, cx))
567 .ok();
568 })
569 .detach();
570 }
571 }
572
573 pub fn pause_cursor_blinking(&mut self, window: &mut Window, cx: &mut Context<Self>) {
574 self.blink_state = true;
575 cx.notify();
576
577 let epoch = self.next_blink_epoch();
578 cx.spawn_in(window, async move |this, cx| {
579 Timer::after(CURSOR_BLINK_INTERVAL).await;
580 this.update_in(cx, |this, window, cx| {
581 this.resume_cursor_blinking(epoch, window, cx)
582 })
583 .ok();
584 })
585 .detach();
586 }
587
588 pub fn terminal(&self) -> &Entity<Terminal> {
589 &self.terminal
590 }
591
592 pub fn set_block_below_cursor(
593 &mut self,
594 block: BlockProperties,
595 window: &mut Window,
596 cx: &mut Context<Self>,
597 ) {
598 self.block_below_cursor = Some(Rc::new(block));
599 self.scroll_to_bottom(&ScrollToBottom, window, cx);
600 cx.notify();
601 }
602
603 pub fn clear_block_below_cursor(&mut self, cx: &mut Context<Self>) {
604 self.block_below_cursor = None;
605 self.scroll_top = Pixels::ZERO;
606 cx.notify();
607 }
608
609 fn next_blink_epoch(&mut self) -> usize {
610 self.blink_epoch += 1;
611 self.blink_epoch
612 }
613
614 fn resume_cursor_blinking(
615 &mut self,
616 epoch: usize,
617 window: &mut Window,
618 cx: &mut Context<Self>,
619 ) {
620 if epoch == self.blink_epoch {
621 self.blinking_paused = false;
622 self.blink_cursors(epoch, window, cx);
623 }
624 }
625
626 ///Attempt to paste the clipboard into the terminal
627 fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
628 self.terminal.update(cx, |term, _| term.copy());
629 cx.notify();
630 }
631
632 ///Attempt to paste the clipboard into the terminal
633 fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
634 if let Some(clipboard_string) = cx.read_from_clipboard().and_then(|item| item.text()) {
635 self.terminal
636 .update(cx, |terminal, _cx| terminal.paste(&clipboard_string));
637 }
638 }
639
640 fn send_text(&mut self, text: &SendText, _: &mut Window, cx: &mut Context<Self>) {
641 self.clear_bell(cx);
642 self.terminal.update(cx, |term, _| {
643 term.input(text.0.to_string());
644 });
645 }
646
647 fn send_keystroke(&mut self, text: &SendKeystroke, _: &mut Window, cx: &mut Context<Self>) {
648 if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
649 self.clear_bell(cx);
650 self.terminal.update(cx, |term, cx| {
651 term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta);
652 });
653 }
654 }
655
656 fn dispatch_context(&self, cx: &App) -> KeyContext {
657 let mut dispatch_context = KeyContext::new_with_defaults();
658 dispatch_context.add("Terminal");
659
660 if self.terminal.read(cx).vi_mode_enabled() {
661 dispatch_context.add("vi_mode");
662 }
663
664 let mode = self.terminal.read(cx).last_content.mode;
665 dispatch_context.set(
666 "screen",
667 if mode.contains(TermMode::ALT_SCREEN) {
668 "alt"
669 } else {
670 "normal"
671 },
672 );
673
674 if mode.contains(TermMode::APP_CURSOR) {
675 dispatch_context.add("DECCKM");
676 }
677 if mode.contains(TermMode::APP_KEYPAD) {
678 dispatch_context.add("DECPAM");
679 } else {
680 dispatch_context.add("DECPNM");
681 }
682 if mode.contains(TermMode::SHOW_CURSOR) {
683 dispatch_context.add("DECTCEM");
684 }
685 if mode.contains(TermMode::LINE_WRAP) {
686 dispatch_context.add("DECAWM");
687 }
688 if mode.contains(TermMode::ORIGIN) {
689 dispatch_context.add("DECOM");
690 }
691 if mode.contains(TermMode::INSERT) {
692 dispatch_context.add("IRM");
693 }
694 //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
695 if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
696 dispatch_context.add("LNM");
697 }
698 if mode.contains(TermMode::FOCUS_IN_OUT) {
699 dispatch_context.add("report_focus");
700 }
701 if mode.contains(TermMode::ALTERNATE_SCROLL) {
702 dispatch_context.add("alternate_scroll");
703 }
704 if mode.contains(TermMode::BRACKETED_PASTE) {
705 dispatch_context.add("bracketed_paste");
706 }
707 if mode.intersects(TermMode::MOUSE_MODE) {
708 dispatch_context.add("any_mouse_reporting");
709 }
710 {
711 let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
712 "click"
713 } else if mode.contains(TermMode::MOUSE_DRAG) {
714 "drag"
715 } else if mode.contains(TermMode::MOUSE_MOTION) {
716 "motion"
717 } else {
718 "off"
719 };
720 dispatch_context.set("mouse_reporting", mouse_reporting);
721 }
722 {
723 let format = if mode.contains(TermMode::SGR_MOUSE) {
724 "sgr"
725 } else if mode.contains(TermMode::UTF8_MOUSE) {
726 "utf8"
727 } else {
728 "normal"
729 };
730 dispatch_context.set("mouse_format", format);
731 };
732 dispatch_context
733 }
734
735 fn set_terminal(
736 &mut self,
737 terminal: Entity<Terminal>,
738 window: &mut Window,
739 cx: &mut Context<TerminalView>,
740 ) {
741 self._terminal_subscriptions =
742 subscribe_for_terminal_events(&terminal, self.workspace.clone(), window, cx);
743 self.terminal = terminal;
744 }
745
746 // Hack: Using editor in terminal causes cyclic dependency i.e. editor -> terminal -> project -> editor.
747 fn map_show_scrollbar_from_editor_to_terminal(
748 show_scrollbar: editor::ShowScrollbar,
749 ) -> terminal_settings::ShowScrollbar {
750 match show_scrollbar {
751 editor::ShowScrollbar::Auto => terminal_settings::ShowScrollbar::Auto,
752 editor::ShowScrollbar::System => terminal_settings::ShowScrollbar::System,
753 editor::ShowScrollbar::Always => terminal_settings::ShowScrollbar::Always,
754 editor::ShowScrollbar::Never => terminal_settings::ShowScrollbar::Never,
755 }
756 }
757
758 fn should_show_scrollbar(cx: &App) -> bool {
759 let show = TerminalSettings::get_global(cx)
760 .scrollbar
761 .show
762 .unwrap_or_else(|| {
763 Self::map_show_scrollbar_from_editor_to_terminal(
764 EditorSettings::get_global(cx).scrollbar.show,
765 )
766 });
767 match show {
768 terminal_settings::ShowScrollbar::Auto => true,
769 terminal_settings::ShowScrollbar::System => true,
770 terminal_settings::ShowScrollbar::Always => true,
771 terminal_settings::ShowScrollbar::Never => false,
772 }
773 }
774
775 fn should_autohide_scrollbar(cx: &App) -> bool {
776 let show = TerminalSettings::get_global(cx)
777 .scrollbar
778 .show
779 .unwrap_or_else(|| {
780 Self::map_show_scrollbar_from_editor_to_terminal(
781 EditorSettings::get_global(cx).scrollbar.show,
782 )
783 });
784 match show {
785 terminal_settings::ShowScrollbar::Auto => true,
786 terminal_settings::ShowScrollbar::System => cx
787 .try_global::<ScrollbarAutoHide>()
788 .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
789 terminal_settings::ShowScrollbar::Always => false,
790 terminal_settings::ShowScrollbar::Never => true,
791 }
792 }
793
794 fn hide_scrollbar(&mut self, cx: &mut Context<Self>) {
795 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
796 if !Self::should_autohide_scrollbar(cx) {
797 return;
798 }
799 self.hide_scrollbar_task = Some(cx.spawn(async move |panel, cx| {
800 cx.background_executor()
801 .timer(SCROLLBAR_SHOW_INTERVAL)
802 .await;
803 panel
804 .update(cx, |panel, cx| {
805 panel.show_scrollbar = false;
806 cx.notify();
807 })
808 .log_err();
809 }))
810 }
811
812 fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
813 if !Self::should_show_scrollbar(cx)
814 || !(self.show_scrollbar || self.scrollbar_state.is_dragging())
815 {
816 return None;
817 }
818
819 if self.terminal.read(cx).total_lines() == self.terminal.read(cx).viewport_lines() {
820 return None;
821 }
822
823 self.scroll_handle.update(self.terminal.read(cx));
824
825 if let Some(new_display_offset) = self.scroll_handle.future_display_offset.take() {
826 self.terminal.update(cx, |term, _| {
827 let delta = new_display_offset as i32 - term.last_content.display_offset as i32;
828 match delta.cmp(&0) {
829 std::cmp::Ordering::Greater => term.scroll_up_by(delta as usize),
830 std::cmp::Ordering::Less => term.scroll_down_by(-delta as usize),
831 std::cmp::Ordering::Equal => {}
832 }
833 });
834 }
835
836 Some(
837 div()
838 .occlude()
839 .id("terminal-view-scroll")
840 .on_mouse_move(cx.listener(|_, _, _window, cx| {
841 cx.notify();
842 cx.stop_propagation()
843 }))
844 .on_hover(|_, _window, cx| {
845 cx.stop_propagation();
846 })
847 .on_any_mouse_down(|_, _window, cx| {
848 cx.stop_propagation();
849 })
850 .on_mouse_up(
851 MouseButton::Left,
852 cx.listener(|terminal_view, _, window, cx| {
853 if !terminal_view.scrollbar_state.is_dragging()
854 && !terminal_view.focus_handle.contains_focused(window, cx)
855 {
856 terminal_view.hide_scrollbar(cx);
857 cx.notify();
858 }
859 cx.stop_propagation();
860 }),
861 )
862 .on_scroll_wheel(cx.listener(|_, _, _window, cx| {
863 cx.notify();
864 }))
865 .h_full()
866 .absolute()
867 .right_1()
868 .top_1()
869 .bottom_0()
870 .w(px(12.))
871 .cursor_default()
872 .children(Scrollbar::vertical(self.scrollbar_state.clone())),
873 )
874 }
875
876 fn rerun_button(task: &TaskState) -> Option<IconButton> {
877 if !task.show_rerun {
878 return None;
879 }
880
881 let task_id = task.id.clone();
882 Some(
883 IconButton::new("rerun-icon", IconName::Rerun)
884 .icon_size(IconSize::Small)
885 .size(ButtonSize::Compact)
886 .icon_color(Color::Default)
887 .shape(ui::IconButtonShape::Square)
888 .tooltip(move |window, cx| {
889 Tooltip::for_action("Rerun task", &RerunTask, window, cx)
890 })
891 .on_click(move |_, window, cx| {
892 window.dispatch_action(Box::new(terminal_rerun_override(&task_id)), cx);
893 }),
894 )
895 }
896}
897
898fn terminal_rerun_override(task: &TaskId) -> zed_actions::Rerun {
899 zed_actions::Rerun {
900 task_id: Some(task.0.clone()),
901 allow_concurrent_runs: Some(true),
902 use_new_terminal: Some(false),
903 reevaluate_context: false,
904 }
905}
906
907fn subscribe_for_terminal_events(
908 terminal: &Entity<Terminal>,
909 workspace: WeakEntity<Workspace>,
910 window: &mut Window,
911 cx: &mut Context<TerminalView>,
912) -> Vec<Subscription> {
913 let terminal_subscription = cx.observe(terminal, |_, _, cx| cx.notify());
914 let mut previous_cwd = None;
915 let terminal_events_subscription = cx.subscribe_in(
916 terminal,
917 window,
918 move |terminal_view, terminal, event, window, cx| {
919 let current_cwd = terminal.read(cx).working_directory();
920 if current_cwd != previous_cwd {
921 previous_cwd = current_cwd;
922 terminal_view.cwd_serialized = false;
923 }
924
925 match event {
926 Event::Wakeup => {
927 cx.notify();
928 cx.emit(Event::Wakeup);
929 cx.emit(ItemEvent::UpdateTab);
930 cx.emit(SearchEvent::MatchesInvalidated);
931 }
932
933 Event::Bell => {
934 terminal_view.has_bell = true;
935 cx.emit(Event::Wakeup);
936 }
937
938 Event::BlinkChanged(blinking) => {
939 if matches!(
940 TerminalSettings::get_global(cx).blinking,
941 TerminalBlink::TerminalControlled
942 ) {
943 terminal_view.blinking_terminal_enabled = *blinking;
944 }
945 }
946
947 Event::TitleChanged => {
948 cx.emit(ItemEvent::UpdateTab);
949 }
950
951 Event::NewNavigationTarget(maybe_navigation_target) => {
952 match maybe_navigation_target
953 .as_ref()
954 .zip(terminal.read(cx).last_content.last_hovered_word.as_ref())
955 {
956 Some((MaybeNavigationTarget::Url(url), hovered_word)) => {
957 if Some(hovered_word)
958 != terminal_view
959 .hover
960 .as_ref()
961 .map(|hover| &hover.hovered_word)
962 {
963 terminal_view.hover = Some(HoverTarget {
964 tooltip: url.clone(),
965 hovered_word: hovered_word.clone(),
966 });
967 terminal_view.hover_tooltip_update = Task::ready(());
968 cx.notify();
969 }
970 }
971 Some((MaybeNavigationTarget::PathLike(path_like_target), hovered_word)) => {
972 if Some(hovered_word)
973 != terminal_view
974 .hover
975 .as_ref()
976 .map(|hover| &hover.hovered_word)
977 {
978 let valid_files_to_open_task = possible_open_target(
979 &workspace,
980 &path_like_target.terminal_dir,
981 &path_like_target.maybe_path,
982 cx,
983 );
984 let hovered_word = hovered_word.clone();
985
986 terminal_view.hover = None;
987 terminal_view.hover_tooltip_update =
988 cx.spawn(async move |terminal_view, cx| {
989 let file_to_open = valid_files_to_open_task.await;
990 terminal_view
991 .update(cx, |terminal_view, _| match file_to_open {
992 Some(
993 OpenTarget::File(path, _)
994 | OpenTarget::Worktree(path, _),
995 ) => {
996 terminal_view.hover = Some(HoverTarget {
997 tooltip: path.to_string(|path| {
998 path.to_string_lossy().to_string()
999 }),
1000 hovered_word,
1001 });
1002 }
1003 None => {
1004 terminal_view.hover = None;
1005 }
1006 })
1007 .ok();
1008 });
1009 cx.notify();
1010 }
1011 }
1012 None => {
1013 terminal_view.hover = None;
1014 terminal_view.hover_tooltip_update = Task::ready(());
1015 cx.notify();
1016 }
1017 }
1018 }
1019
1020 Event::Open(maybe_navigation_target) => match maybe_navigation_target {
1021 MaybeNavigationTarget::Url(url) => cx.open_url(url),
1022
1023 MaybeNavigationTarget::PathLike(path_like_target) => {
1024 if terminal_view.hover.is_none() {
1025 return;
1026 }
1027 let task_workspace = workspace.clone();
1028 let path_like_target = path_like_target.clone();
1029 cx.spawn_in(window, async move |terminal_view, cx| {
1030 let open_target = terminal_view
1031 .update(cx, |_, cx| {
1032 possible_open_target(
1033 &task_workspace,
1034 &path_like_target.terminal_dir,
1035 &path_like_target.maybe_path,
1036 cx,
1037 )
1038 })?
1039 .await;
1040 if let Some(open_target) = open_target {
1041 let path_to_open = open_target.path();
1042 let opened_items = task_workspace
1043 .update_in(cx, |workspace, window, cx| {
1044 workspace.open_paths(
1045 vec![path_to_open.path.clone()],
1046 OpenOptions {
1047 visible: Some(OpenVisible::OnlyDirectories),
1048 ..Default::default()
1049 },
1050 None,
1051 window,
1052 cx,
1053 )
1054 })
1055 .context("workspace update")?
1056 .await;
1057 if opened_items.len() != 1 {
1058 debug_panic!(
1059 "Received {} items for one path {path_to_open:?}",
1060 opened_items.len(),
1061 );
1062 }
1063
1064 if let Some(opened_item) = opened_items.first() {
1065 if open_target.is_file() {
1066 if let Some(Ok(opened_item)) = opened_item {
1067 if let Some(row) = path_to_open.row {
1068 let col = path_to_open.column.unwrap_or(0);
1069 if let Some(active_editor) =
1070 opened_item.downcast::<Editor>()
1071 {
1072 active_editor
1073 .downgrade()
1074 .update_in(cx, |editor, window, cx| {
1075 editor.go_to_singleton_buffer_point(
1076 language::Point::new(
1077 row.saturating_sub(1),
1078 col.saturating_sub(1),
1079 ),
1080 window,
1081 cx,
1082 )
1083 })
1084 .log_err();
1085 }
1086 }
1087 }
1088 } else if open_target.is_dir() {
1089 task_workspace.update(cx, |workspace, cx| {
1090 workspace.project().update(cx, |_, cx| {
1091 cx.emit(project::Event::ActivateProjectPanel);
1092 })
1093 })?;
1094 }
1095 }
1096 }
1097
1098 anyhow::Ok(())
1099 })
1100 .detach_and_log_err(cx)
1101 }
1102 },
1103 Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs),
1104 Event::CloseTerminal => cx.emit(ItemEvent::CloseItem),
1105 Event::SelectionsChanged => {
1106 window.invalidate_character_coordinates();
1107 cx.emit(SearchEvent::ActiveMatchChanged)
1108 }
1109 }
1110 },
1111 );
1112 vec![terminal_subscription, terminal_events_subscription]
1113}
1114
1115#[derive(Debug, Clone)]
1116enum OpenTarget {
1117 Worktree(PathWithPosition, Entry),
1118 File(PathWithPosition, Metadata),
1119}
1120
1121impl OpenTarget {
1122 fn is_file(&self) -> bool {
1123 match self {
1124 OpenTarget::Worktree(_, entry) => entry.is_file(),
1125 OpenTarget::File(_, metadata) => !metadata.is_dir,
1126 }
1127 }
1128
1129 fn is_dir(&self) -> bool {
1130 match self {
1131 OpenTarget::Worktree(_, entry) => entry.is_dir(),
1132 OpenTarget::File(_, metadata) => metadata.is_dir,
1133 }
1134 }
1135
1136 fn path(&self) -> &PathWithPosition {
1137 match self {
1138 OpenTarget::Worktree(path, _) => path,
1139 OpenTarget::File(path, _) => path,
1140 }
1141 }
1142}
1143
1144fn possible_open_target(
1145 workspace: &WeakEntity<Workspace>,
1146 cwd: &Option<PathBuf>,
1147 maybe_path: &str,
1148 cx: &App,
1149) -> Task<Option<OpenTarget>> {
1150 let Some(workspace) = workspace.upgrade() else {
1151 return Task::ready(None);
1152 };
1153 // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too.
1154 // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away.
1155 let mut potential_paths = Vec::new();
1156 let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path));
1157 let path_with_position = PathWithPosition::parse_str(maybe_path);
1158 let worktree_candidates = workspace
1159 .read(cx)
1160 .worktrees(cx)
1161 .sorted_by_key(|worktree| {
1162 let worktree_root = worktree.read(cx).abs_path();
1163 match cwd
1164 .as_ref()
1165 .and_then(|cwd| worktree_root.strip_prefix(cwd).ok())
1166 {
1167 Some(cwd_child) => cwd_child.components().count(),
1168 None => usize::MAX,
1169 }
1170 })
1171 .collect::<Vec<_>>();
1172 // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it.
1173 for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) {
1174 if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() {
1175 potential_paths.push(PathWithPosition {
1176 path: stripped.to_owned(),
1177 row: original_path.row,
1178 column: original_path.column,
1179 });
1180 }
1181 if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() {
1182 potential_paths.push(PathWithPosition {
1183 path: stripped.to_owned(),
1184 row: path_with_position.row,
1185 column: path_with_position.column,
1186 });
1187 }
1188 }
1189
1190 let insert_both_paths = original_path != path_with_position;
1191 potential_paths.insert(0, original_path);
1192 if insert_both_paths {
1193 potential_paths.insert(1, path_with_position);
1194 }
1195
1196 // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix.
1197 // That will be slow, though, so do the fast checks first.
1198 let mut worktree_paths_to_check = Vec::new();
1199 for worktree in &worktree_candidates {
1200 let worktree_root = worktree.read(cx).abs_path();
1201 let mut paths_to_check = Vec::with_capacity(potential_paths.len());
1202
1203 for path_with_position in &potential_paths {
1204 let path_to_check = if worktree_root.ends_with(&path_with_position.path) {
1205 let root_path_with_position = PathWithPosition {
1206 path: worktree_root.to_path_buf(),
1207 row: path_with_position.row,
1208 column: path_with_position.column,
1209 };
1210 match worktree.read(cx).root_entry() {
1211 Some(root_entry) => {
1212 return Task::ready(Some(OpenTarget::Worktree(
1213 root_path_with_position,
1214 root_entry.clone(),
1215 )));
1216 }
1217 None => root_path_with_position,
1218 }
1219 } else {
1220 PathWithPosition {
1221 path: path_with_position
1222 .path
1223 .strip_prefix(&worktree_root)
1224 .unwrap_or(&path_with_position.path)
1225 .to_owned(),
1226 row: path_with_position.row,
1227 column: path_with_position.column,
1228 }
1229 };
1230
1231 if path_to_check.path.is_relative() {
1232 if let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) {
1233 return Task::ready(Some(OpenTarget::Worktree(
1234 PathWithPosition {
1235 path: worktree_root.join(&entry.path),
1236 row: path_to_check.row,
1237 column: path_to_check.column,
1238 },
1239 entry.clone(),
1240 )));
1241 }
1242 }
1243
1244 paths_to_check.push(path_to_check);
1245 }
1246
1247 if !paths_to_check.is_empty() {
1248 worktree_paths_to_check.push((worktree.clone(), paths_to_check));
1249 }
1250 }
1251
1252 // Before entire worktree traversal(s), make an attempt to do FS checks if available.
1253 let fs_paths_to_check = if workspace.read(cx).project().read(cx).is_local() {
1254 potential_paths
1255 .into_iter()
1256 .flat_map(|path_to_check| {
1257 let mut paths_to_check = Vec::new();
1258 let maybe_path = &path_to_check.path;
1259 if maybe_path.starts_with("~") {
1260 if let Some(home_path) =
1261 maybe_path
1262 .strip_prefix("~")
1263 .ok()
1264 .and_then(|stripped_maybe_path| {
1265 Some(dirs::home_dir()?.join(stripped_maybe_path))
1266 })
1267 {
1268 paths_to_check.push(PathWithPosition {
1269 path: home_path,
1270 row: path_to_check.row,
1271 column: path_to_check.column,
1272 });
1273 }
1274 } else {
1275 paths_to_check.push(PathWithPosition {
1276 path: maybe_path.clone(),
1277 row: path_to_check.row,
1278 column: path_to_check.column,
1279 });
1280 if maybe_path.is_relative() {
1281 if let Some(cwd) = &cwd {
1282 paths_to_check.push(PathWithPosition {
1283 path: cwd.join(maybe_path),
1284 row: path_to_check.row,
1285 column: path_to_check.column,
1286 });
1287 }
1288 for worktree in &worktree_candidates {
1289 paths_to_check.push(PathWithPosition {
1290 path: worktree.read(cx).abs_path().join(maybe_path),
1291 row: path_to_check.row,
1292 column: path_to_check.column,
1293 });
1294 }
1295 }
1296 }
1297 paths_to_check
1298 })
1299 .collect()
1300 } else {
1301 Vec::new()
1302 };
1303
1304 let worktree_check_task = cx.spawn(async move |cx| {
1305 for (worktree, worktree_paths_to_check) in worktree_paths_to_check {
1306 let found_entry = worktree
1307 .update(cx, |worktree, _| {
1308 let worktree_root = worktree.abs_path();
1309 let mut traversal = worktree.traverse_from_path(true, true, false, "".as_ref());
1310 while let Some(entry) = traversal.next() {
1311 if let Some(path_in_worktree) = worktree_paths_to_check
1312 .iter()
1313 .find(|path_to_check| entry.path.ends_with(&path_to_check.path))
1314 {
1315 return Some(OpenTarget::Worktree(
1316 PathWithPosition {
1317 path: worktree_root.join(&entry.path),
1318 row: path_in_worktree.row,
1319 column: path_in_worktree.column,
1320 },
1321 entry.clone(),
1322 ));
1323 }
1324 }
1325 None
1326 })
1327 .ok()?;
1328 if let Some(found_entry) = found_entry {
1329 return Some(found_entry);
1330 }
1331 }
1332 None
1333 });
1334
1335 let fs = workspace.read(cx).project().read(cx).fs().clone();
1336 cx.background_spawn(async move {
1337 for mut path_to_check in fs_paths_to_check {
1338 if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() {
1339 if let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() {
1340 path_to_check.path = fs_path_to_check;
1341 return Some(OpenTarget::File(path_to_check, metadata));
1342 }
1343 }
1344 }
1345
1346 worktree_check_task.await
1347 })
1348}
1349
1350fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<RegexSearch> {
1351 let str = query.as_str();
1352 if query.is_regex() {
1353 if str == "." {
1354 return None;
1355 }
1356 RegexSearch::new(str).ok()
1357 } else {
1358 RegexSearch::new(®ex::escape(str)).ok()
1359 }
1360}
1361
1362impl TerminalView {
1363 fn key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
1364 self.clear_bell(cx);
1365 self.pause_cursor_blinking(window, cx);
1366
1367 self.terminal.update(cx, |term, cx| {
1368 let handled = term.try_keystroke(
1369 &event.keystroke,
1370 TerminalSettings::get_global(cx).option_as_meta,
1371 );
1372 if handled {
1373 cx.stop_propagation();
1374 }
1375 });
1376 }
1377
1378 fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1379 self.terminal.update(cx, |terminal, _| {
1380 terminal.set_cursor_shape(self.cursor_shape);
1381 terminal.focus_in();
1382 });
1383 self.blink_cursors(self.blink_epoch, window, cx);
1384 window.invalidate_character_coordinates();
1385 cx.notify();
1386 }
1387
1388 fn focus_out(&mut self, _: &mut Window, cx: &mut Context<Self>) {
1389 self.terminal.update(cx, |terminal, _| {
1390 terminal.focus_out();
1391 terminal.set_cursor_shape(CursorShape::Hollow);
1392 });
1393 self.hide_scrollbar(cx);
1394 cx.notify();
1395 }
1396}
1397
1398impl Render for TerminalView {
1399 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1400 let terminal_handle = self.terminal.clone();
1401 let terminal_view_handle = cx.entity().clone();
1402
1403 let focused = self.focus_handle.is_focused(window);
1404
1405 div()
1406 .id("terminal-view")
1407 .size_full()
1408 .relative()
1409 .track_focus(&self.focus_handle(cx))
1410 .key_context(self.dispatch_context(cx))
1411 .on_action(cx.listener(TerminalView::send_text))
1412 .on_action(cx.listener(TerminalView::send_keystroke))
1413 .on_action(cx.listener(TerminalView::copy))
1414 .on_action(cx.listener(TerminalView::paste))
1415 .on_action(cx.listener(TerminalView::clear))
1416 .on_action(cx.listener(TerminalView::scroll_line_up))
1417 .on_action(cx.listener(TerminalView::scroll_line_down))
1418 .on_action(cx.listener(TerminalView::scroll_page_up))
1419 .on_action(cx.listener(TerminalView::scroll_page_down))
1420 .on_action(cx.listener(TerminalView::scroll_to_top))
1421 .on_action(cx.listener(TerminalView::scroll_to_bottom))
1422 .on_action(cx.listener(TerminalView::toggle_vi_mode))
1423 .on_action(cx.listener(TerminalView::show_character_palette))
1424 .on_action(cx.listener(TerminalView::select_all))
1425 .on_action(cx.listener(TerminalView::rerun_task))
1426 .on_key_down(cx.listener(Self::key_down))
1427 .on_mouse_down(
1428 MouseButton::Right,
1429 cx.listener(|this, event: &MouseDownEvent, window, cx| {
1430 if !this.terminal.read(cx).mouse_mode(event.modifiers.shift) {
1431 if this.terminal.read(cx).last_content.selection.is_none() {
1432 this.terminal.update(cx, |terminal, _| {
1433 terminal.select_word_at_event_position(event);
1434 });
1435 };
1436 this.deploy_context_menu(event.position, window, cx);
1437 cx.notify();
1438 }
1439 }),
1440 )
1441 .on_hover(cx.listener(|this, hovered, window, cx| {
1442 if *hovered {
1443 this.show_scrollbar = true;
1444 this.hide_scrollbar_task.take();
1445 cx.notify();
1446 } else if !this.focus_handle.contains_focused(window, cx) {
1447 this.hide_scrollbar(cx);
1448 }
1449 }))
1450 .child(
1451 // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu
1452 div()
1453 .size_full()
1454 .child(TerminalElement::new(
1455 terminal_handle,
1456 terminal_view_handle,
1457 self.workspace.clone(),
1458 self.focus_handle.clone(),
1459 focused,
1460 self.should_show_cursor(focused, cx),
1461 self.block_below_cursor.clone(),
1462 self.embedded,
1463 ))
1464 .when_some(self.render_scrollbar(cx), |div, scrollbar| {
1465 div.child(scrollbar)
1466 }),
1467 )
1468 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1469 deferred(
1470 anchored()
1471 .position(*position)
1472 .anchor(gpui::Corner::TopLeft)
1473 .child(menu.clone()),
1474 )
1475 .with_priority(1)
1476 }))
1477 }
1478}
1479
1480impl Item for TerminalView {
1481 type Event = ItemEvent;
1482
1483 fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent> {
1484 let terminal = self.terminal().read(cx);
1485 let title = terminal.title(false);
1486 let pid = terminal.pty_info.pid_getter().fallback_pid();
1487
1488 Some(TabTooltipContent::Custom(Box::new(move |_window, cx| {
1489 cx.new(|_| TerminalTooltip::new(title.clone(), pid)).into()
1490 })))
1491 }
1492
1493 fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
1494 let terminal = self.terminal().read(cx);
1495 let title = terminal.title(true);
1496
1497 let (icon, icon_color, rerun_button) = match terminal.task() {
1498 Some(terminal_task) => match &terminal_task.status {
1499 TaskStatus::Running => (
1500 IconName::Play,
1501 Color::Disabled,
1502 TerminalView::rerun_button(&terminal_task),
1503 ),
1504 TaskStatus::Unknown => (
1505 IconName::Warning,
1506 Color::Warning,
1507 TerminalView::rerun_button(&terminal_task),
1508 ),
1509 TaskStatus::Completed { success } => {
1510 let rerun_button = TerminalView::rerun_button(&terminal_task);
1511
1512 if *success {
1513 (IconName::Check, Color::Success, rerun_button)
1514 } else {
1515 (IconName::XCircle, Color::Error, rerun_button)
1516 }
1517 }
1518 },
1519 None => (IconName::Terminal, Color::Muted, None),
1520 };
1521
1522 h_flex()
1523 .gap_1()
1524 .group("term-tab-icon")
1525 .child(
1526 h_flex()
1527 .group("term-tab-icon")
1528 .child(
1529 div()
1530 .when(rerun_button.is_some(), |this| {
1531 this.hover(|style| style.invisible().w_0())
1532 })
1533 .child(Icon::new(icon).color(icon_color)),
1534 )
1535 .when_some(rerun_button, |this, rerun_button| {
1536 this.child(
1537 div()
1538 .absolute()
1539 .visible_on_hover("term-tab-icon")
1540 .child(rerun_button),
1541 )
1542 }),
1543 )
1544 .child(Label::new(title).color(params.text_color()))
1545 .into_any()
1546 }
1547
1548 fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString {
1549 let terminal = self.terminal().read(cx);
1550 terminal.title(detail == 0).into()
1551 }
1552
1553 fn telemetry_event_text(&self) -> Option<&'static str> {
1554 None
1555 }
1556
1557 fn clone_on_split(
1558 &self,
1559 workspace_id: Option<WorkspaceId>,
1560 window: &mut Window,
1561 cx: &mut Context<Self>,
1562 ) -> Option<Entity<Self>> {
1563 let window_handle = window.window_handle();
1564 let terminal = self
1565 .project
1566 .update(cx, |project, cx| {
1567 let terminal = self.terminal().read(cx);
1568 let working_directory = terminal
1569 .working_directory()
1570 .or_else(|| Some(project.active_project_directory(cx)?.to_path_buf()));
1571 let python_venv_directory = terminal.python_venv_directory.clone();
1572 project.create_terminal_with_venv(
1573 TerminalKind::Shell(working_directory),
1574 python_venv_directory,
1575 window_handle,
1576 cx,
1577 )
1578 })
1579 .ok()?
1580 .log_err()?;
1581
1582 Some(cx.new(|cx| {
1583 TerminalView::new(
1584 terminal,
1585 self.workspace.clone(),
1586 workspace_id,
1587 self.project.clone(),
1588 false,
1589 window,
1590 cx,
1591 )
1592 }))
1593 }
1594
1595 fn is_dirty(&self, cx: &gpui::App) -> bool {
1596 match self.terminal.read(cx).task() {
1597 Some(task) => task.status == TaskStatus::Running,
1598 None => self.has_bell(),
1599 }
1600 }
1601
1602 fn has_conflict(&self, _cx: &App) -> bool {
1603 false
1604 }
1605
1606 fn can_save_as(&self, _cx: &App) -> bool {
1607 false
1608 }
1609
1610 fn is_singleton(&self, _cx: &App) -> bool {
1611 true
1612 }
1613
1614 fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1615 Some(Box::new(handle.clone()))
1616 }
1617
1618 fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
1619 if self.show_breadcrumbs && !self.terminal().read(cx).breadcrumb_text.trim().is_empty() {
1620 ToolbarItemLocation::PrimaryLeft
1621 } else {
1622 ToolbarItemLocation::Hidden
1623 }
1624 }
1625
1626 fn breadcrumbs(&self, _: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
1627 Some(vec![BreadcrumbText {
1628 text: self.terminal().read(cx).breadcrumb_text.clone(),
1629 highlights: None,
1630 font: None,
1631 }])
1632 }
1633
1634 fn added_to_workspace(
1635 &mut self,
1636 workspace: &mut Workspace,
1637 _: &mut Window,
1638 cx: &mut Context<Self>,
1639 ) {
1640 if self.terminal().read(cx).task().is_none() {
1641 if let Some((new_id, old_id)) = workspace.database_id().zip(self.workspace_id) {
1642 log::debug!(
1643 "Updating workspace id for the terminal, old: {old_id:?}, new: {new_id:?}",
1644 );
1645 cx.background_spawn(TERMINAL_DB.update_workspace_id(
1646 new_id,
1647 old_id,
1648 cx.entity_id().as_u64(),
1649 ))
1650 .detach();
1651 }
1652 self.workspace_id = workspace.database_id();
1653 }
1654 }
1655
1656 fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1657 f(*event)
1658 }
1659}
1660
1661impl SerializableItem for TerminalView {
1662 fn serialized_item_kind() -> &'static str {
1663 "Terminal"
1664 }
1665
1666 fn cleanup(
1667 workspace_id: WorkspaceId,
1668 alive_items: Vec<workspace::ItemId>,
1669 _window: &mut Window,
1670 cx: &mut App,
1671 ) -> Task<anyhow::Result<()>> {
1672 delete_unloaded_items(alive_items, workspace_id, "terminals", &TERMINAL_DB, cx)
1673 }
1674
1675 fn serialize(
1676 &mut self,
1677 _workspace: &mut Workspace,
1678 item_id: workspace::ItemId,
1679 _closing: bool,
1680 _: &mut Window,
1681 cx: &mut Context<Self>,
1682 ) -> Option<Task<anyhow::Result<()>>> {
1683 let terminal = self.terminal().read(cx);
1684 if terminal.task().is_some() {
1685 return None;
1686 }
1687
1688 if let Some((cwd, workspace_id)) = terminal.working_directory().zip(self.workspace_id) {
1689 self.cwd_serialized = true;
1690 Some(cx.background_spawn(async move {
1691 TERMINAL_DB
1692 .save_working_directory(item_id, workspace_id, cwd)
1693 .await
1694 }))
1695 } else {
1696 None
1697 }
1698 }
1699
1700 fn should_serialize(&self, _: &Self::Event) -> bool {
1701 !self.cwd_serialized
1702 }
1703
1704 fn deserialize(
1705 project: Entity<Project>,
1706 workspace: WeakEntity<Workspace>,
1707 workspace_id: workspace::WorkspaceId,
1708 item_id: workspace::ItemId,
1709 window: &mut Window,
1710 cx: &mut App,
1711 ) -> Task<anyhow::Result<Entity<Self>>> {
1712 let window_handle = window.window_handle();
1713 window.spawn(cx, async move |cx| {
1714 let cwd = cx
1715 .update(|_window, cx| {
1716 let from_db = TERMINAL_DB
1717 .get_working_directory(item_id, workspace_id)
1718 .log_err()
1719 .flatten();
1720 if from_db
1721 .as_ref()
1722 .is_some_and(|from_db| !from_db.as_os_str().is_empty())
1723 {
1724 from_db
1725 } else {
1726 workspace
1727 .upgrade()
1728 .and_then(|workspace| default_working_directory(workspace.read(cx), cx))
1729 }
1730 })
1731 .ok()
1732 .flatten();
1733
1734 let terminal = project
1735 .update(cx, |project, cx| {
1736 project.create_terminal(TerminalKind::Shell(cwd), window_handle, cx)
1737 })?
1738 .await?;
1739 cx.update(|window, cx| {
1740 cx.new(|cx| {
1741 TerminalView::new(
1742 terminal,
1743 workspace,
1744 Some(workspace_id),
1745 project.downgrade(),
1746 false,
1747 window,
1748 cx,
1749 )
1750 })
1751 })
1752 })
1753 }
1754}
1755
1756impl SearchableItem for TerminalView {
1757 type Match = RangeInclusive<Point>;
1758
1759 fn supported_options(&self) -> SearchOptions {
1760 SearchOptions {
1761 case: false,
1762 word: false,
1763 regex: true,
1764 replacement: false,
1765 selection: false,
1766 find_in_results: false,
1767 }
1768 }
1769
1770 /// Clear stored matches
1771 fn clear_matches(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1772 self.terminal().update(cx, |term, _| term.matches.clear())
1773 }
1774
1775 /// Store matches returned from find_matches somewhere for rendering
1776 fn update_matches(
1777 &mut self,
1778 matches: &[Self::Match],
1779 _window: &mut Window,
1780 cx: &mut Context<Self>,
1781 ) {
1782 self.terminal()
1783 .update(cx, |term, _| term.matches = matches.to_vec())
1784 }
1785
1786 /// Returns the selection content to pre-load into this search
1787 fn query_suggestion(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> String {
1788 self.terminal()
1789 .read(cx)
1790 .last_content
1791 .selection_text
1792 .clone()
1793 .unwrap_or_default()
1794 }
1795
1796 /// Focus match at given index into the Vec of matches
1797 fn activate_match(
1798 &mut self,
1799 index: usize,
1800 _: &[Self::Match],
1801 _window: &mut Window,
1802 cx: &mut Context<Self>,
1803 ) {
1804 self.terminal()
1805 .update(cx, |term, _| term.activate_match(index));
1806 cx.notify();
1807 }
1808
1809 /// Add selections for all matches given.
1810 fn select_matches(&mut self, matches: &[Self::Match], _: &mut Window, cx: &mut Context<Self>) {
1811 self.terminal()
1812 .update(cx, |term, _| term.select_matches(matches));
1813 cx.notify();
1814 }
1815
1816 /// Get all of the matches for this query, should be done on the background
1817 fn find_matches(
1818 &mut self,
1819 query: Arc<SearchQuery>,
1820 _: &mut Window,
1821 cx: &mut Context<Self>,
1822 ) -> Task<Vec<Self::Match>> {
1823 if let Some(s) = regex_search_for_query(&query) {
1824 self.terminal()
1825 .update(cx, |term, cx| term.find_matches(s, cx))
1826 } else {
1827 Task::ready(vec![])
1828 }
1829 }
1830
1831 /// Reports back to the search toolbar what the active match should be (the selection)
1832 fn active_match_index(
1833 &mut self,
1834 direction: Direction,
1835 matches: &[Self::Match],
1836 _: &mut Window,
1837 cx: &mut Context<Self>,
1838 ) -> Option<usize> {
1839 // Selection head might have a value if there's a selection that isn't
1840 // associated with a match. Therefore, if there are no matches, we should
1841 // report None, no matter the state of the terminal
1842 let res = if !matches.is_empty() {
1843 if let Some(selection_head) = self.terminal().read(cx).selection_head {
1844 // If selection head is contained in a match. Return that match
1845 match direction {
1846 Direction::Prev => {
1847 // If no selection before selection head, return the first match
1848 Some(
1849 matches
1850 .iter()
1851 .enumerate()
1852 .rev()
1853 .find(|(_, search_match)| {
1854 search_match.contains(&selection_head)
1855 || search_match.start() < &selection_head
1856 })
1857 .map(|(ix, _)| ix)
1858 .unwrap_or(0),
1859 )
1860 }
1861 Direction::Next => {
1862 // If no selection after selection head, return the last match
1863 Some(
1864 matches
1865 .iter()
1866 .enumerate()
1867 .find(|(_, search_match)| {
1868 search_match.contains(&selection_head)
1869 || search_match.start() > &selection_head
1870 })
1871 .map(|(ix, _)| ix)
1872 .unwrap_or(matches.len().saturating_sub(1)),
1873 )
1874 }
1875 }
1876 } else {
1877 // Matches found but no active selection, return the first last one (closest to cursor)
1878 Some(matches.len().saturating_sub(1))
1879 }
1880 } else {
1881 None
1882 };
1883
1884 res
1885 }
1886 fn replace(
1887 &mut self,
1888 _: &Self::Match,
1889 _: &SearchQuery,
1890 _window: &mut Window,
1891 _: &mut Context<Self>,
1892 ) {
1893 // Replacement is not supported in terminal view, so this is a no-op.
1894 }
1895}
1896
1897///Gets the working directory for the given workspace, respecting the user's settings.
1898/// None implies "~" on whichever machine we end up on.
1899pub(crate) fn default_working_directory(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
1900 match &TerminalSettings::get_global(cx).working_directory {
1901 WorkingDirectory::CurrentProjectDirectory => workspace
1902 .project()
1903 .read(cx)
1904 .active_project_directory(cx)
1905 .as_deref()
1906 .map(Path::to_path_buf),
1907 WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
1908 WorkingDirectory::AlwaysHome => None,
1909 WorkingDirectory::Always { directory } => {
1910 shellexpand::full(&directory) //TODO handle this better
1911 .ok()
1912 .map(|dir| Path::new(&dir.to_string()).to_path_buf())
1913 .filter(|dir| dir.is_dir())
1914 }
1915 }
1916}
1917///Gets the first project's home directory, or the home directory
1918fn first_project_directory(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
1919 let worktree = workspace.worktrees(cx).next()?.read(cx);
1920 if !worktree.root_entry()?.is_dir() {
1921 return None;
1922 }
1923 Some(worktree.abs_path().to_path_buf())
1924}
1925
1926#[cfg(test)]
1927mod tests {
1928 use super::*;
1929 use gpui::TestAppContext;
1930 use project::{Entry, Project, ProjectPath, Worktree};
1931 use std::path::Path;
1932 use workspace::AppState;
1933
1934 // Working directory calculation tests
1935
1936 // No Worktrees in project -> home_dir()
1937 #[gpui::test]
1938 async fn no_worktree(cx: &mut TestAppContext) {
1939 let (project, workspace) = init_test(cx).await;
1940 cx.read(|cx| {
1941 let workspace = workspace.read(cx);
1942 let active_entry = project.read(cx).active_entry();
1943
1944 //Make sure environment is as expected
1945 assert!(active_entry.is_none());
1946 assert!(workspace.worktrees(cx).next().is_none());
1947
1948 let res = default_working_directory(workspace, cx);
1949 assert_eq!(res, None);
1950 let res = first_project_directory(workspace, cx);
1951 assert_eq!(res, None);
1952 });
1953 }
1954
1955 // No active entry, but a worktree, worktree is a file -> home_dir()
1956 #[gpui::test]
1957 async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
1958 let (project, workspace) = init_test(cx).await;
1959
1960 create_file_wt(project.clone(), "/root.txt", cx).await;
1961 cx.read(|cx| {
1962 let workspace = workspace.read(cx);
1963 let active_entry = project.read(cx).active_entry();
1964
1965 //Make sure environment is as expected
1966 assert!(active_entry.is_none());
1967 assert!(workspace.worktrees(cx).next().is_some());
1968
1969 let res = default_working_directory(workspace, cx);
1970 assert_eq!(res, None);
1971 let res = first_project_directory(workspace, cx);
1972 assert_eq!(res, None);
1973 });
1974 }
1975
1976 // No active entry, but a worktree, worktree is a folder -> worktree_folder
1977 #[gpui::test]
1978 async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
1979 let (project, workspace) = init_test(cx).await;
1980
1981 let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
1982 cx.update(|cx| {
1983 let workspace = workspace.read(cx);
1984 let active_entry = project.read(cx).active_entry();
1985
1986 assert!(active_entry.is_none());
1987 assert!(workspace.worktrees(cx).next().is_some());
1988
1989 let res = default_working_directory(workspace, cx);
1990 assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
1991 let res = first_project_directory(workspace, cx);
1992 assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
1993 });
1994 }
1995
1996 // Active entry with a work tree, worktree is a file -> worktree_folder()
1997 #[gpui::test]
1998 async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
1999 let (project, workspace) = init_test(cx).await;
2000
2001 let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
2002 let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await;
2003 insert_active_entry_for(wt2, entry2, project.clone(), cx);
2004
2005 cx.update(|cx| {
2006 let workspace = workspace.read(cx);
2007 let active_entry = project.read(cx).active_entry();
2008
2009 assert!(active_entry.is_some());
2010
2011 let res = default_working_directory(workspace, cx);
2012 assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
2013 let res = first_project_directory(workspace, cx);
2014 assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
2015 });
2016 }
2017
2018 // Active entry, with a worktree, worktree is a folder -> worktree_folder
2019 #[gpui::test]
2020 async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
2021 let (project, workspace) = init_test(cx).await;
2022
2023 let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
2024 let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await;
2025 insert_active_entry_for(wt2, entry2, project.clone(), cx);
2026
2027 cx.update(|cx| {
2028 let workspace = workspace.read(cx);
2029 let active_entry = project.read(cx).active_entry();
2030
2031 assert!(active_entry.is_some());
2032
2033 let res = default_working_directory(workspace, cx);
2034 assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
2035 let res = first_project_directory(workspace, cx);
2036 assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
2037 });
2038 }
2039
2040 /// Creates a worktree with 1 file: /root.txt
2041 pub async fn init_test(cx: &mut TestAppContext) -> (Entity<Project>, Entity<Workspace>) {
2042 let params = cx.update(AppState::test);
2043 cx.update(|cx| {
2044 terminal::init(cx);
2045 theme::init(theme::LoadThemes::JustBase, cx);
2046 Project::init_settings(cx);
2047 language::init(cx);
2048 });
2049
2050 let project = Project::test(params.fs.clone(), [], cx).await;
2051 let workspace = cx
2052 .add_window(|window, cx| Workspace::test_new(project.clone(), window, cx))
2053 .root(cx)
2054 .unwrap();
2055
2056 (project, workspace)
2057 }
2058
2059 /// Creates a worktree with 1 folder: /root{suffix}/
2060 async fn create_folder_wt(
2061 project: Entity<Project>,
2062 path: impl AsRef<Path>,
2063 cx: &mut TestAppContext,
2064 ) -> (Entity<Worktree>, Entry) {
2065 create_wt(project, true, path, cx).await
2066 }
2067
2068 /// Creates a worktree with 1 file: /root{suffix}.txt
2069 async fn create_file_wt(
2070 project: Entity<Project>,
2071 path: impl AsRef<Path>,
2072 cx: &mut TestAppContext,
2073 ) -> (Entity<Worktree>, Entry) {
2074 create_wt(project, false, path, cx).await
2075 }
2076
2077 async fn create_wt(
2078 project: Entity<Project>,
2079 is_dir: bool,
2080 path: impl AsRef<Path>,
2081 cx: &mut TestAppContext,
2082 ) -> (Entity<Worktree>, Entry) {
2083 let (wt, _) = project
2084 .update(cx, |project, cx| {
2085 project.find_or_create_worktree(path, true, cx)
2086 })
2087 .await
2088 .unwrap();
2089
2090 let entry = cx
2091 .update(|cx| {
2092 wt.update(cx, |wt, cx| {
2093 wt.create_entry(Path::new(""), is_dir, None, cx)
2094 })
2095 })
2096 .await
2097 .unwrap()
2098 .to_included()
2099 .unwrap();
2100
2101 (wt, entry)
2102 }
2103
2104 pub fn insert_active_entry_for(
2105 wt: Entity<Worktree>,
2106 entry: Entry,
2107 project: Entity<Project>,
2108 cx: &mut TestAppContext,
2109 ) {
2110 cx.update(|cx| {
2111 let p = ProjectPath {
2112 worktree_id: wt.read(cx).id(),
2113 path: entry.path,
2114 };
2115 project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
2116 });
2117 }
2118}