Detailed changes
@@ -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<AnyModelHandle> {
cx.upgrade_any_model_handle(self)
}
+ pub fn model_type(&self) -> TypeId {
+ self.model_type
+ }
+
+ fn is<T: 'static>(&self) -> bool {
+ TypeId::of::<T>() == self.model_type
+ }
+
+ pub fn downcast<T: Entity>(&self) -> Option<WeakModelHandle<T>> {
+ if self.is::<T>() {
+ let result = Some(WeakModelHandle {
+ model_id: self.model_id,
+ model_type: PhantomData,
+ });
+
+ result
+ } else {
+ None
+ }
+ }
}
impl<T: Entity> From<WeakModelHandle<T>> for AnyWeakModelHandle {
@@ -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<TouchPhase>,
}
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
@@ -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),
@@ -57,6 +57,7 @@ pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode) -> Option<String> {
("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()),
@@ -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<Terminal>);
-
pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
+ 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::<Option<StoredTerminal>, _, _>(|possible_connection, _| {
- possible_connection.take()
- });
+ let possible_terminal = ProgramManager::remove::<Terminal, _>(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::<Option<StoredTerminal>>(Some(StoredTerminal(stored_terminal.clone())));
+ ProgramManager::insert_or_replace::<Terminal, _>(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::<Option<StoredTerminal>>(Some(StoredTerminal(
- terminal_handle.clone(),
- )));
+ ProgramManager::insert_or_replace::<Terminal, _>(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::<Option<StoredTerminal>>(Some(StoredTerminal(terminal_handle)));
+ ProgramManager::insert_or_replace::<Terminal, _>(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::<Option<StoredTerminal>>(None);
+ ProgramManager::remove::<Terminal, _>(cx.window_id(), cx);
+
if workspace.modal::<TerminalContainer>().is_some() {
workspace.dismiss_modal(cx)
}
@@ -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 => "<system defined shell>".to_string(),
- match pw {
- Some(pw) => format!("<system defined shell> {}", pw.shell),
- None => "<could not access the password file>".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!("<none specified, using system defined shell> {}", pw.shell)
- }
- None => "<none specified, could not access the password file> {}".to_string(),
- }
- })
+ .unwrap_or_else(|| "<none specified, using system defined shell>".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<LocalProcessInfo>,
+ 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<i32> {
+ 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<LocalProcessInfo> {
- 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<Point>) -> 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<T>,
+// regex: &'a RegexSearch,
+// ) -> impl Iterator<Item = Match> + '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<T>,
regex: &'a RegexSearch,
) -> impl Iterator<Item = Match> + '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<Passwd<'_>> {
- // Create zeroed passwd struct.
- let mut entry: MaybeUninit<libc::passwd> = 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()))
- }
-}
@@ -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<ViewHandle<TerminalView>> {
+ 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<Self::Match>,
cx: &mut ViewContext<Self>,
) -> Option<usize> {
- 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
}
}
@@ -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;
@@ -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<T: From<u8> + AddAssign<T> + Copy>(value: &mut T) -> T {
let prev = *value;
*value += T::from(1);
@@ -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<usize, AnyModelHandle>,
+}
+
+impl ProgramManager {
+ pub fn insert_or_replace<T: Entity, V: View>(
+ window: usize,
+ program: ModelHandle<T>,
+ cx: &mut ViewContext<V>,
+ ) -> Option<AnyModelHandle> {
+ cx.update_global::<ProgramManager, _, _>(|pm, _| {
+ pm.insert_or_replace_internal::<T>(window, program)
+ })
+ }
+
+ pub fn remove<T: Entity, V: View>(
+ window: usize,
+ cx: &mut ViewContext<V>,
+ ) -> Option<ModelHandle<T>> {
+ cx.update_global::<ProgramManager, _, _>(|pm, _| pm.remove_internal::<T>(window))
+ }
+
+ pub fn new() -> Self {
+ Self {
+ modals: Default::default(),
+ }
+ }
+
+ /// Inserts or replaces the model at the given location.
+ fn insert_or_replace_internal<T: Entity>(
+ &mut self,
+ window: usize,
+ program: ModelHandle<T>,
+ ) -> Option<AnyModelHandle> {
+ self.modals.insert(window, AnyModelHandle::from(program))
+ }
+
+ /// Remove the program associated with this window, if it's of the given type
+ fn remove_internal<T: Entity>(&mut self, window: usize) -> Option<ModelHandle<T>> {
+ let program = self.modals.remove(&window);
+ if let Some(program) = program {
+ if program.is::<T>() {
+ // 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
+ }
+ }
+}
@@ -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<AppState>, cx: &mut MutableAppContext) {
+ // Initialize the program manager immediately
+ cx.set_global(ProgramManager::new());
+
pane::init(cx);
cx.add_global_action(open);