diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 4c8bed04919d50a1dbe1b9a30b0246f5ac7ce8de..e9091d74c8001c59c03beb6c2e1f4101ae567035 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -5083,6 +5083,7 @@ impl Drop for AnyModelHandle { } } +#[derive(Hash, PartialEq, Eq, Debug)] pub struct AnyWeakModelHandle { model_id: usize, model_type: TypeId, @@ -5092,6 +5093,26 @@ impl AnyWeakModelHandle { pub fn upgrade(&self, cx: &impl UpgradeModelHandle) -> Option { cx.upgrade_any_model_handle(self) } + pub fn model_type(&self) -> TypeId { + self.model_type + } + + fn is(&self) -> bool { + TypeId::of::() == self.model_type + } + + pub fn downcast(&self) -> Option> { + if self.is::() { + let result = Some(WeakModelHandle { + model_id: self.model_id, + model_type: PhantomData, + }); + + result + } else { + None + } + } } impl From> for AnyWeakModelHandle { diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index 3e0fe4a53a408f87a525d31154ccb688efdc47e7..48043ac9186b0701ab7d08fe86de30941ae56dda 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -19,6 +19,15 @@ pub struct ModifiersChangedEvent { pub cmd: bool, } +/// The phase of a touch motion event. +/// Based on the winit enum of the same name, +#[derive(Clone, Copy, Debug)] +pub enum TouchPhase { + Started, + Moved, + Ended, +} + #[derive(Clone, Copy, Debug, Default)] pub struct ScrollWheelEvent { pub position: Vector2F, @@ -28,6 +37,8 @@ pub struct ScrollWheelEvent { pub alt: bool, pub shift: bool, pub cmd: bool, + /// If the platform supports returning the phase of a scroll wheel event, it will be stored here + pub phase: Option, } #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index c6f838b431ac232463501be6f21e1ad8aea38e25..51524f4b15742733b8e865ade79f6d7266b6fc83 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -3,10 +3,10 @@ use crate::{ keymap::Keystroke, platform::{Event, NavigationDirection}, KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent, - MouseMovedEvent, ScrollWheelEvent, + MouseMovedEvent, ScrollWheelEvent, TouchPhase, }; use cocoa::{ - appkit::{NSEvent, NSEventModifierFlags, NSEventType}, + appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType}, base::{id, YES}, foundation::NSString as _, }; @@ -150,6 +150,14 @@ impl Event { NSEventType::NSScrollWheel => window_height.map(|window_height| { let modifiers = native_event.modifierFlags(); + let phase = match native_event.phase() { + NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => { + Some(TouchPhase::Started) + } + NSEventPhase::NSEventPhaseEnded => Some(TouchPhase::Ended), + _ => Some(TouchPhase::Moved), + }; + Self::ScrollWheel(ScrollWheelEvent { position: vec2f( native_event.locationInWindow().x as f32, @@ -159,6 +167,7 @@ impl Event { native_event.scrollingDeltaX() as f32, native_event.scrollingDeltaY() as f32, ), + phase, precise: native_event.hasPreciseScrollingDeltas() == YES, ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask), alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask), diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index da730e62968d6e3d15ae860068b56aab2c55a22c..7e0c6e3d17ce8c3ca1d26859abf1217430f9be0f 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -57,6 +57,7 @@ pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode) -> Option { ("tab", Modifiers::None) => Some("\x09".to_string()), ("escape", Modifiers::None) => Some("\x1b".to_string()), ("enter", Modifiers::None) => Some("\x0d".to_string()), + ("enter", Modifiers::Shift) => Some("\x0d".to_string()), ("backspace", Modifiers::None) => Some("\x7f".to_string()), //Interesting escape codes ("tab", Modifiers::Shift) => Some("\x1b[Z".to_string()), diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index 504a4b84ab12c8665afdb0b2fa95db199f5d4998..bf83196a97eb4e2e713248b6287565e504a65990 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal.rs @@ -1,6 +1,6 @@ use gpui::{ModelHandle, ViewContext}; use settings::{Settings, WorkingDirectory}; -use workspace::Workspace; +use workspace::{programs::ProgramManager, Workspace}; use crate::{ terminal_container_view::{ @@ -9,24 +9,20 @@ use crate::{ Event, Terminal, }; -#[derive(Debug)] -struct StoredTerminal(ModelHandle); - pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext) { + let window = cx.window_id(); + // Pull the terminal connection out of the global if it has been stored - let possible_terminal = - cx.update_default_global::, _, _>(|possible_connection, _| { - possible_connection.take() - }); + let possible_terminal = ProgramManager::remove::(window, cx); - if let Some(StoredTerminal(stored_terminal)) = possible_terminal { + if let Some(terminal_handle) = possible_terminal { workspace.toggle_modal(cx, |_, cx| { // Create a view from the stored connection if the terminal modal is not already shown - cx.add_view(|cx| TerminalContainer::from_terminal(stored_terminal.clone(), true, cx)) + cx.add_view(|cx| TerminalContainer::from_terminal(terminal_handle.clone(), true, cx)) }); // Toggle Modal will dismiss the terminal modal if it is currently shown, so we must // store the terminal back in the global - cx.set_global::>(Some(StoredTerminal(stored_terminal.clone()))); + ProgramManager::insert_or_replace::(window, terminal_handle, cx); } else { // No connection was stored, create a new terminal if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| { @@ -47,21 +43,19 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon cx.subscribe(&terminal_handle, on_event).detach(); // Set the global immediately if terminal construction was successful, // in case the user opens the command palette - cx.set_global::>(Some(StoredTerminal( - terminal_handle.clone(), - ))); + ProgramManager::insert_or_replace::(window, terminal_handle, cx); } this }) { - // Terminal modal was dismissed. Store terminal if the terminal view is connected + // Terminal modal was dismissed and the terminal view is connected, store the terminal if let TerminalContainerContent::Connected(connected) = &closed_terminal_handle.read(cx).content { let terminal_handle = connected.read(cx).handle(); // Set the global immediately if terminal construction was successful, // in case the user opens the command palette - cx.set_global::>(Some(StoredTerminal(terminal_handle))); + ProgramManager::insert_or_replace::(window, terminal_handle, cx); } } } @@ -75,7 +69,8 @@ pub fn on_event( ) { // Dismiss the modal if the terminal quit if let Event::CloseTerminal = event { - cx.set_global::>(None); + ProgramManager::remove::(cx.window_id(), cx); + if workspace.modal::().is_some() { workspace.dismiss_modal(cx) } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 42ec061290a31a39e0b7bc1dfb07859ee81255bb..633edb5ff689c799630bb526c5e7d2195cc33aa1 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -72,8 +72,8 @@ pub fn init(cx: &mut MutableAppContext) { ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable ///Scroll multiplier that is set to 3 by default. This will be removed when I ///Implement scroll bars. -const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; -const MAX_SEARCH_LINES: usize = 100; +const SCROLL_MULTIPLIER: f32 = 4.; +// const MAX_SEARCH_LINES: usize = 100; const DEBUG_TERMINAL_WIDTH: f32 = 500.; const DEBUG_TERMINAL_HEIGHT: f32 = 30.; const DEBUG_CELL_WIDTH: f32 = 5.; @@ -237,28 +237,12 @@ impl TerminalError { self.shell .clone() .map(|shell| match shell { - Shell::System => { - let mut buf = [0; 1024]; - let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); + Shell::System => "".to_string(), - match pw { - Some(pw) => format!(" {}", pw.shell), - None => "".to_string(), - } - } Shell::Program(s) => s, Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), }) - .unwrap_or_else(|| { - let mut buf = [0; 1024]; - let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); - match pw { - Some(pw) => { - format!(" {}", pw.shell) - } - None => " {}".to_string(), - } - }) + .unwrap_or_else(|| "".to_string()) } } @@ -381,6 +365,7 @@ impl TerminalBuilder { shell_pid, foreground_process_info: None, breadcrumb_text: String::new(), + scroll_px: 0., }; Ok(TerminalBuilder { @@ -500,6 +485,7 @@ pub struct Terminal { shell_pid: u32, shell_fd: u32, foreground_process_info: Option, + scroll_px: f32, } impl Terminal { @@ -535,16 +521,42 @@ impl Terminal { } AlacTermEvent::Wakeup => { cx.emit(Event::Wakeup); - cx.notify(); + + if self.update_process_info() { + cx.emit(Event::TitleChanged) + } } AlacTermEvent::ColorRequest(idx, fun_ptr) => { self.events .push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone())); - cx.notify(); //Immediately schedule a render to respond to the color request } } } + /// Update the cached process info, returns whether the Zed-relevant info has changed + fn update_process_info(&mut self) -> bool { + let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) }; + if pid < 0 { + pid = self.shell_pid as i32; + } + + if let Some(process_info) = LocalProcessInfo::with_root_pid(pid as u32) { + let res = self + .foreground_process_info + .as_ref() + .map(|old_info| { + process_info.cwd != old_info.cwd || process_info.name != old_info.name + }) + .unwrap_or(true); + + self.foreground_process_info = Some(process_info.clone()); + + res + } else { + false + } + } + ///Takes events from Alacritty and translates them to behavior on this view fn process_terminal_event( &mut self, @@ -678,7 +690,7 @@ impl Terminal { let mut terminal = if let Some(term) = term.try_lock_unfair() { term } else if self.last_synced.elapsed().as_secs_f32() > 0.25 { - term.lock_unfair() + term.lock_unfair() //It's been too long, force block } else if let None = self.sync_task { //Skip this frame let delay = cx.background().timer(Duration::from_millis(16)); @@ -699,24 +711,15 @@ impl Terminal { return; }; + if self.update_process_info() { + cx.emit(Event::TitleChanged); + } + //Note that the ordering of events matters for event processing while let Some(e) = self.events.pop_front() { self.process_terminal_event(&e, &mut terminal, cx) } - if let Some(process_info) = self.compute_process_info() { - let should_emit_title_changed = self - .foreground_process_info - .as_ref() - .map(|old_info| { - process_info.cwd != old_info.cwd || process_info.name != old_info.name - }) - .unwrap_or(true); - if should_emit_title_changed { - cx.emit(Event::TitleChanged) - } - self.foreground_process_info = Some(process_info.clone()); - } self.last_content = Self::make_content(&terminal); self.last_synced = Instant::now(); } @@ -893,44 +896,66 @@ impl Terminal { ///Scroll the terminal pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Vector2F) { - if self.mouse_mode(e.shift) { - //TODO: Currently this only sends the current scroll reports as they come in. Alacritty - //Sends the *entire* scroll delta on *every* scroll event, only resetting it when - //The scroll enters 'TouchPhase::Started'. Do I need to replicate this? - //This would be consistent with a scroll model based on 'distance from origin'... - let scroll_lines = (e.delta.y() / self.cur_size.line_height) as i32; - let point = mouse_point( - e.position.sub(origin), - self.cur_size, - self.last_content.display_offset, - ); - - if let Some(scrolls) = - scroll_report(point, scroll_lines as i32, e, self.last_content.mode) + let mouse_mode = self.mouse_mode(e.shift); + + if let Some(scroll_lines) = self.determine_scroll_lines(e, mouse_mode) { + if mouse_mode { + let point = mouse_point( + e.position.sub(origin), + self.cur_size, + self.last_content.display_offset, + ); + + if let Some(scrolls) = + scroll_report(point, scroll_lines as i32, e, self.last_content.mode) + { + for scroll in scrolls { + self.pty_tx.notify(scroll); + } + }; + } else if self + .last_content + .mode + .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL) + && !e.shift { - for scroll in scrolls { - self.pty_tx.notify(scroll); + self.pty_tx.notify(alt_scroll(scroll_lines)) + } else { + if scroll_lines != 0 { + let scroll = AlacScroll::Delta(scroll_lines); + + self.events.push_back(InternalEvent::Scroll(scroll)); } - }; - } else if self - .last_content - .mode - .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL) - && !e.shift - { - //TODO: See above TODO, also applies here. - let scroll_lines = - ((e.delta.y() * ALACRITTY_SCROLL_MULTIPLIER) / self.cur_size.line_height) as i32; - - self.pty_tx.notify(alt_scroll(scroll_lines)) - } else { - let scroll_lines = - ((e.delta.y() * ALACRITTY_SCROLL_MULTIPLIER) / self.cur_size.line_height) as i32; - if scroll_lines != 0 { - let scroll = AlacScroll::Delta(scroll_lines); + } + } + } + + fn determine_scroll_lines(&mut self, e: &ScrollWheelEvent, mouse_mode: bool) -> Option { + let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER }; + + match e.phase { + /* Reset scroll state on started */ + Some(gpui::TouchPhase::Started) => { + self.scroll_px = 0.; + None + } + /* Calculate the appropriate scroll lines */ + Some(gpui::TouchPhase::Moved) => { + let old_offset = (self.scroll_px / self.cur_size.line_height) as i32; - self.events.push_back(InternalEvent::Scroll(scroll)); + self.scroll_px += e.delta.y() * scroll_multiplier; + + let new_offset = (self.scroll_px / self.cur_size.line_height) as i32; + + // Whenever we hit the edges, reset our stored scroll to 0 + // so we can respond to changes in direction quickly + self.scroll_px %= self.cur_size.height; + + Some(new_offset - old_offset) } + /* Fall back to delta / line_height */ + None => Some(((e.delta.y() * scroll_multiplier) / self.cur_size.line_height) as i32), + _ => None, } } @@ -957,17 +982,9 @@ impl Terminal { let term = term.lock(); - make_search_matches(&term, &searcher).collect() + all_search_matches(&term, &searcher).collect() }) } - - fn compute_process_info(&self) -> Option { - let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) }; - if pid < 0 { - pid = self.shell_pid as i32; - } - LocalProcessInfo::with_root_pid(pid as u32) - } } impl Drop for Terminal { @@ -988,102 +1005,32 @@ fn make_selection(range: &RangeInclusive) -> Selection { /// Copied from alacritty/src/display/hint.rs HintMatches::visible_regex_matches() /// Iterate over all visible regex matches. -fn make_search_matches<'a, T>( +// fn visible_search_matches<'a, T>( +// term: &'a Term, +// regex: &'a RegexSearch, +// ) -> impl Iterator + 'a { +// let viewport_start = Line(-(term.grid().display_offset() as i32)); +// let viewport_end = viewport_start + term.bottommost_line(); +// let mut start = term.line_search_left(Point::new(viewport_start, Column(0))); +// let mut end = term.line_search_right(Point::new(viewport_end, Column(0))); +// start.line = start.line.max(viewport_start - MAX_SEARCH_LINES); +// end.line = end.line.min(viewport_end + MAX_SEARCH_LINES); + +// RegexIter::new(start, end, AlacDirection::Right, term, regex) +// .skip_while(move |rm| rm.end().line < viewport_start) +// .take_while(move |rm| rm.start().line <= viewport_end) +// } + +fn all_search_matches<'a, T>( term: &'a Term, regex: &'a RegexSearch, ) -> impl Iterator + 'a { - let viewport_start = Line(-(term.grid().display_offset() as i32)); - let viewport_end = viewport_start + term.bottommost_line(); - let mut start = term.line_search_left(Point::new(viewport_start, Column(0))); - let mut end = term.line_search_right(Point::new(viewport_end, Column(0))); - start.line = start.line.max(viewport_start - MAX_SEARCH_LINES); - end.line = end.line.min(viewport_end + MAX_SEARCH_LINES); - + let start = Point::new(term.grid().topmost_line(), Column(0)); + let end = Point::new(term.grid().bottommost_line(), term.grid().last_column()); RegexIter::new(start, end, AlacDirection::Right, term, regex) - .skip_while(move |rm| rm.end().line < viewport_start) - .take_while(move |rm| rm.start().line <= viewport_end) } #[cfg(test)] mod tests { pub mod terminal_test_context; } - -//TODO Move this around and clean up the code -mod alacritty_unix { - use alacritty_terminal::config::Program; - use gpui::anyhow::{bail, Result}; - - use std::ffi::CStr; - use std::mem::MaybeUninit; - use std::ptr; - - #[derive(Debug)] - pub struct Passwd<'a> { - _name: &'a str, - _dir: &'a str, - pub shell: &'a str, - } - - /// Return a Passwd struct with pointers into the provided buf. - /// - /// # Unsafety - /// - /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen. - pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result> { - // Create zeroed passwd struct. - let mut entry: MaybeUninit = MaybeUninit::uninit(); - - let mut res: *mut libc::passwd = ptr::null_mut(); - - // Try and read the pw file. - let uid = unsafe { libc::getuid() }; - let status = unsafe { - libc::getpwuid_r( - uid, - entry.as_mut_ptr(), - buf.as_mut_ptr() as *mut _, - buf.len(), - &mut res, - ) - }; - let entry = unsafe { entry.assume_init() }; - - if status < 0 { - bail!("getpwuid_r failed"); - } - - if res.is_null() { - bail!("pw not found"); - } - - // Sanity check. - assert_eq!(entry.pw_uid, uid); - - // Build a borrowed Passwd struct. - Ok(Passwd { - _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() }, - _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() }, - shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() }, - }) - } - - #[cfg(target_os = "macos")] - pub fn _default_shell(pw: &Passwd<'_>) -> Program { - let shell_name = pw.shell.rsplit('/').next().unwrap(); - let argv = vec![ - String::from("-c"), - format!("exec -a -{} {}", shell_name, pw.shell), - ]; - - Program::WithArgs { - program: "/bin/bash".to_owned(), - args: argv, - } - } - - #[cfg(not(target_os = "macos"))] - pub fn default_shell(pw: &Passwd<'_>) -> Program { - Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned())) - } -} diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal/src/terminal_container_view.rs index 6e3d85f5f70c8ecf0655d49216c2663d312721b1..5a1d27fb7a8679c5bad2e7a2d3fe9233e1d08f0a 100644 --- a/crates/terminal/src/terminal_container_view.rs +++ b/crates/terminal/src/terminal_container_view.rs @@ -7,6 +7,7 @@ use gpui::{ actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, }; +use util::truncate_and_trailoff; use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}; use workspace::{Item, Workspace}; @@ -149,6 +150,13 @@ impl TerminalContainer { associated_directory: None, } } + + fn connected(&self) -> Option> { + match &self.content { + TerminalContainerContent::Connected(vh) => Some(vh.clone()), + TerminalContainerContent::Error(_) => None, + } + } } impl View for TerminalContainer { @@ -246,12 +254,28 @@ impl Item for TerminalContainer { .as_ref() .map(|fpi| { format!( - "{} - {}", - fpi.cwd - .file_name() - .map(|name| name.to_string_lossy().to_string()) - .unwrap_or_default(), - fpi.name, + "{} — {}", + truncate_and_trailoff( + &fpi.cwd + .file_name() + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or_default(), + 25 + ), + truncate_and_trailoff( + &{ + format!( + "{}{}", + fpi.name, + if fpi.argv.len() >= 1 { + format!(" {}", (&fpi.argv[1..]).join(" ")) + } else { + "".to_string() + } + ) + }, + 25 + ) ) }) .unwrap_or_else(|| "Terminal".to_string()), @@ -324,18 +348,14 @@ impl Item for TerminalContainer { fn is_dirty(&self, cx: &gpui::AppContext) -> bool { if let TerminalContainerContent::Connected(connected) = &self.content { - connected.read(cx).has_new_content() + connected.read(cx).has_bell() } else { false } } - fn has_conflict(&self, cx: &AppContext) -> bool { - if let TerminalContainerContent::Connected(connected) = &self.content { - connected.read(cx).has_bell() - } else { - false - } + fn has_conflict(&self, _cx: &AppContext) -> bool { + false } fn should_update_tab_on_event(event: &Self::Event) -> bool { @@ -431,28 +451,42 @@ impl SearchableItem for TerminalContainer { matches: Vec, cx: &mut ViewContext, ) -> Option { - if let TerminalContainerContent::Connected(connected) = &self.content { - if let Some(selection_head) = connected.read(cx).terminal().read(cx).selection_head { + let connected = self.connected(); + // Selection head might have a value if there's a selection that isn't + // associated with a match. Therefore, if there are no matches, we should + // report None, no matter the state of the terminal + let res = if matches.len() > 0 && connected.is_some() { + if let Some(selection_head) = connected + .unwrap() + .read(cx) + .terminal() + .read(cx) + .selection_head + { // If selection head is contained in a match. Return that match - for (ix, search_match) in matches.iter().enumerate() { - if search_match.contains(&selection_head) { - return Some(ix); - } - - // If not contained, return the next match after the selection head - if search_match.start() > &selection_head { - return Some(ix); - } + if let Some(ix) = matches + .iter() + .enumerate() + .find(|(_, search_match)| { + search_match.contains(&selection_head) + || search_match.start() > &selection_head + }) + .map(|(ix, _)| ix) + { + Some(ix) + } else { + // If no selection after selection head, return the last match + Some(matches.len().saturating_sub(1)) } - - // If no selection after selection head, return the last match - return Some(matches.len().saturating_sub(1)); } else { - Some(0) + // Matches found but no active selection, return the first last one (closest to cursor) + Some(matches.len().saturating_sub(1)) } } else { None - } + }; + + res } } diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs index 1c49f8b3c2b30054f1cb90c732dfcf840f92931b..ec35ba0a5ce84a01994e788d6110152c96d37f80 100644 --- a/crates/terminal/src/terminal_view.rs +++ b/crates/terminal/src/terminal_view.rs @@ -91,8 +91,8 @@ impl TerminalView { if !cx.is_self_focused() { this.has_new_content = true; cx.notify(); - cx.emit(Event::Wakeup); } + cx.emit(Event::Wakeup); } Event::Bell => { this.has_bell = true; diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index 9b6507461576baf0068610b69743477b720e1fb1..97f409f410c36c2b2de7fb14e9cc7ef94f4996b1 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -9,6 +9,23 @@ use std::{ task::{Context, Poll}, }; +pub fn truncate(s: &str, max_chars: usize) -> &str { + match s.char_indices().nth(max_chars) { + None => s, + Some((idx, _)) => &s[..idx], + } +} + +pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String { + debug_assert!(max_chars >= 5); + + if s.len() > max_chars { + format!("{}…", truncate(&s, max_chars.saturating_sub(3))) + } else { + s.to_string() + } +} + pub fn post_inc + AddAssign + Copy>(value: &mut T) -> T { let prev = *value; *value += T::from(1); diff --git a/crates/workspace/src/programs.rs b/crates/workspace/src/programs.rs new file mode 100644 index 0000000000000000000000000000000000000000..36169ea4c70ad174e666669349656ea2c70e9aa9 --- /dev/null +++ b/crates/workspace/src/programs.rs @@ -0,0 +1,77 @@ +// TODO: Need to put this basic structure in workspace, and make 'program handles' +// based off of the 'searchable item' pattern except with models. This way, the workspace's clients +// can register their models as programs with a specific identity and capable of notifying the workspace +// Programs are: +// - Kept alive by the program manager, they need to emit an event to get dropped from it +// - Can be interacted with directly, (closed, activated, etc.) by the program manager, bypassing +// associated view(s) +// - Have special rendering methods that the program manager requires them to implement to fill out +// the status bar +// - Can emit events for the program manager which: +// - Add a jewel (notification, change, etc.) +// - Drop the program +// - ??? +// - Program Manager is kept in a global, listens for window drop so it can drop all it's program handles + +use collections::HashMap; +use gpui::{AnyModelHandle, Entity, ModelHandle, View, ViewContext}; + +/// This struct is going to be the starting point for the 'program manager' feature that will +/// eventually be implemented to provide a collaborative way of engaging with identity-having +/// features like the terminal. +pub struct ProgramManager { + // TODO: Make this a hashset or something + modals: HashMap, +} + +impl ProgramManager { + pub fn insert_or_replace( + window: usize, + program: ModelHandle, + cx: &mut ViewContext, + ) -> Option { + cx.update_global::(|pm, _| { + pm.insert_or_replace_internal::(window, program) + }) + } + + pub fn remove( + window: usize, + cx: &mut ViewContext, + ) -> Option> { + cx.update_global::(|pm, _| pm.remove_internal::(window)) + } + + pub fn new() -> Self { + Self { + modals: Default::default(), + } + } + + /// Inserts or replaces the model at the given location. + fn insert_or_replace_internal( + &mut self, + window: usize, + program: ModelHandle, + ) -> Option { + self.modals.insert(window, AnyModelHandle::from(program)) + } + + /// Remove the program associated with this window, if it's of the given type + fn remove_internal(&mut self, window: usize) -> Option> { + let program = self.modals.remove(&window); + if let Some(program) = program { + if program.is::() { + // Guaranteed to be some, but leave it in the option + // anyway for the API + program.downcast() + } else { + // Model is of the incorrect type, put it back + self.modals.insert(window, program); + None + } + } else { + None + } + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5bc3c1054e95f8512ce98f403f4cf3bebddac03d..c7a122e9db14f79322ba0ebc2842a7da21e10e3e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5,6 +5,7 @@ /// specific locations. pub mod pane; pub mod pane_group; +pub mod programs; pub mod searchable; pub mod sidebar; mod status_bar; @@ -36,6 +37,7 @@ use log::error; pub use pane::*; pub use pane_group::*; use postage::prelude::Stream; +use programs::ProgramManager; use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId}; use searchable::SearchableItemHandle; use serde::Deserialize; @@ -144,6 +146,9 @@ impl_internal_actions!( impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]); pub fn init(app_state: Arc, cx: &mut MutableAppContext) { + // Initialize the program manager immediately + cx.set_global(ProgramManager::new()); + pane::init(cx); cx.add_global_action(open);