mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-12 21:32:40 +00:00
Merge pull request #1603 from zed-industries/terminal-polishing
Terminal Polishing
This commit is contained in:
commit
8af1e11632
11 changed files with 335 additions and 218 deletions
|
@ -5083,6 +5083,7 @@ impl Drop for AnyModelHandle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Hash, PartialEq, Eq, Debug)]
|
||||||
pub struct AnyWeakModelHandle {
|
pub struct AnyWeakModelHandle {
|
||||||
model_id: usize,
|
model_id: usize,
|
||||||
model_type: TypeId,
|
model_type: TypeId,
|
||||||
|
@ -5092,6 +5093,26 @@ impl AnyWeakModelHandle {
|
||||||
pub fn upgrade(&self, cx: &impl UpgradeModelHandle) -> Option<AnyModelHandle> {
|
pub fn upgrade(&self, cx: &impl UpgradeModelHandle) -> Option<AnyModelHandle> {
|
||||||
cx.upgrade_any_model_handle(self)
|
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 {
|
impl<T: Entity> From<WeakModelHandle<T>> for AnyWeakModelHandle {
|
||||||
|
|
|
@ -19,6 +19,15 @@ pub struct ModifiersChangedEvent {
|
||||||
pub cmd: bool,
|
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)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
pub struct ScrollWheelEvent {
|
pub struct ScrollWheelEvent {
|
||||||
pub position: Vector2F,
|
pub position: Vector2F,
|
||||||
|
@ -28,6 +37,8 @@ pub struct ScrollWheelEvent {
|
||||||
pub alt: bool,
|
pub alt: bool,
|
||||||
pub shift: bool,
|
pub shift: bool,
|
||||||
pub cmd: 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)]
|
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
|
||||||
|
|
|
@ -3,10 +3,10 @@ use crate::{
|
||||||
keymap::Keystroke,
|
keymap::Keystroke,
|
||||||
platform::{Event, NavigationDirection},
|
platform::{Event, NavigationDirection},
|
||||||
KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
|
KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
|
||||||
MouseMovedEvent, ScrollWheelEvent,
|
MouseMovedEvent, ScrollWheelEvent, TouchPhase,
|
||||||
};
|
};
|
||||||
use cocoa::{
|
use cocoa::{
|
||||||
appkit::{NSEvent, NSEventModifierFlags, NSEventType},
|
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
|
||||||
base::{id, YES},
|
base::{id, YES},
|
||||||
foundation::NSString as _,
|
foundation::NSString as _,
|
||||||
};
|
};
|
||||||
|
@ -150,6 +150,14 @@ impl Event {
|
||||||
NSEventType::NSScrollWheel => window_height.map(|window_height| {
|
NSEventType::NSScrollWheel => window_height.map(|window_height| {
|
||||||
let modifiers = native_event.modifierFlags();
|
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 {
|
Self::ScrollWheel(ScrollWheelEvent {
|
||||||
position: vec2f(
|
position: vec2f(
|
||||||
native_event.locationInWindow().x as f32,
|
native_event.locationInWindow().x as f32,
|
||||||
|
@ -159,6 +167,7 @@ impl Event {
|
||||||
native_event.scrollingDeltaX() as f32,
|
native_event.scrollingDeltaX() as f32,
|
||||||
native_event.scrollingDeltaY() as f32,
|
native_event.scrollingDeltaY() as f32,
|
||||||
),
|
),
|
||||||
|
phase,
|
||||||
precise: native_event.hasPreciseScrollingDeltas() == YES,
|
precise: native_event.hasPreciseScrollingDeltas() == YES,
|
||||||
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
||||||
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
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()),
|
("tab", Modifiers::None) => Some("\x09".to_string()),
|
||||||
("escape", Modifiers::None) => Some("\x1b".to_string()),
|
("escape", Modifiers::None) => Some("\x1b".to_string()),
|
||||||
("enter", Modifiers::None) => Some("\x0d".to_string()),
|
("enter", Modifiers::None) => Some("\x0d".to_string()),
|
||||||
|
("enter", Modifiers::Shift) => Some("\x0d".to_string()),
|
||||||
("backspace", Modifiers::None) => Some("\x7f".to_string()),
|
("backspace", Modifiers::None) => Some("\x7f".to_string()),
|
||||||
//Interesting escape codes
|
//Interesting escape codes
|
||||||
("tab", Modifiers::Shift) => Some("\x1b[Z".to_string()),
|
("tab", Modifiers::Shift) => Some("\x1b[Z".to_string()),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use gpui::{ModelHandle, ViewContext};
|
use gpui::{ModelHandle, ViewContext};
|
||||||
use settings::{Settings, WorkingDirectory};
|
use settings::{Settings, WorkingDirectory};
|
||||||
use workspace::Workspace;
|
use workspace::{programs::ProgramManager, Workspace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
terminal_container_view::{
|
terminal_container_view::{
|
||||||
|
@ -9,24 +9,20 @@ use crate::{
|
||||||
Event, Terminal,
|
Event, Terminal,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct StoredTerminal(ModelHandle<Terminal>);
|
|
||||||
|
|
||||||
pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
|
pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
|
||||||
// Pull the terminal connection out of the global if it has been stored
|
let window = cx.window_id();
|
||||||
let possible_terminal =
|
|
||||||
cx.update_default_global::<Option<StoredTerminal>, _, _>(|possible_connection, _| {
|
|
||||||
possible_connection.take()
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(StoredTerminal(stored_terminal)) = possible_terminal {
|
// Pull the terminal connection out of the global if it has been stored
|
||||||
|
let possible_terminal = ProgramManager::remove::<Terminal, _>(window, cx);
|
||||||
|
|
||||||
|
if let Some(terminal_handle) = possible_terminal {
|
||||||
workspace.toggle_modal(cx, |_, cx| {
|
workspace.toggle_modal(cx, |_, cx| {
|
||||||
// Create a view from the stored connection if the terminal modal is not already shown
|
// 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
|
// Toggle Modal will dismiss the terminal modal if it is currently shown, so we must
|
||||||
// store the terminal back in the global
|
// 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 {
|
} else {
|
||||||
// No connection was stored, create a new terminal
|
// No connection was stored, create a new terminal
|
||||||
if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| {
|
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();
|
cx.subscribe(&terminal_handle, on_event).detach();
|
||||||
// Set the global immediately if terminal construction was successful,
|
// Set the global immediately if terminal construction was successful,
|
||||||
// in case the user opens the command palette
|
// in case the user opens the command palette
|
||||||
cx.set_global::<Option<StoredTerminal>>(Some(StoredTerminal(
|
ProgramManager::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
|
||||||
terminal_handle.clone(),
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this
|
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) =
|
if let TerminalContainerContent::Connected(connected) =
|
||||||
&closed_terminal_handle.read(cx).content
|
&closed_terminal_handle.read(cx).content
|
||||||
{
|
{
|
||||||
let terminal_handle = connected.read(cx).handle();
|
let terminal_handle = connected.read(cx).handle();
|
||||||
// Set the global immediately if terminal construction was successful,
|
// Set the global immediately if terminal construction was successful,
|
||||||
// in case the user opens the command palette
|
// 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
|
// Dismiss the modal if the terminal quit
|
||||||
if let Event::CloseTerminal = event {
|
if let Event::CloseTerminal = event {
|
||||||
cx.set_global::<Option<StoredTerminal>>(None);
|
ProgramManager::remove::<Terminal, _>(cx.window_id(), cx);
|
||||||
|
|
||||||
if workspace.modal::<TerminalContainer>().is_some() {
|
if workspace.modal::<TerminalContainer>().is_some() {
|
||||||
workspace.dismiss_modal(cx)
|
workspace.dismiss_modal(cx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,8 +72,8 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
|
///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
|
///Scroll multiplier that is set to 3 by default. This will be removed when I
|
||||||
///Implement scroll bars.
|
///Implement scroll bars.
|
||||||
const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
|
const SCROLL_MULTIPLIER: f32 = 4.;
|
||||||
const MAX_SEARCH_LINES: usize = 100;
|
// const MAX_SEARCH_LINES: usize = 100;
|
||||||
const DEBUG_TERMINAL_WIDTH: f32 = 500.;
|
const DEBUG_TERMINAL_WIDTH: f32 = 500.;
|
||||||
const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
|
const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
|
||||||
const DEBUG_CELL_WIDTH: f32 = 5.;
|
const DEBUG_CELL_WIDTH: f32 = 5.;
|
||||||
|
@ -237,28 +237,12 @@ impl TerminalError {
|
||||||
self.shell
|
self.shell
|
||||||
.clone()
|
.clone()
|
||||||
.map(|shell| match shell {
|
.map(|shell| match shell {
|
||||||
Shell::System => {
|
Shell::System => "<system defined shell>".to_string(),
|
||||||
let mut buf = [0; 1024];
|
|
||||||
let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
|
|
||||||
|
|
||||||
match pw {
|
|
||||||
Some(pw) => format!("<system defined shell> {}", pw.shell),
|
|
||||||
None => "<could not access the password file>".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Shell::Program(s) => s,
|
Shell::Program(s) => s,
|
||||||
Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
|
Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| "<none specified, using system defined shell>".to_string())
|
||||||
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(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,6 +365,7 @@ impl TerminalBuilder {
|
||||||
shell_pid,
|
shell_pid,
|
||||||
foreground_process_info: None,
|
foreground_process_info: None,
|
||||||
breadcrumb_text: String::new(),
|
breadcrumb_text: String::new(),
|
||||||
|
scroll_px: 0.,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(TerminalBuilder {
|
Ok(TerminalBuilder {
|
||||||
|
@ -500,6 +485,7 @@ pub struct Terminal {
|
||||||
shell_pid: u32,
|
shell_pid: u32,
|
||||||
shell_fd: u32,
|
shell_fd: u32,
|
||||||
foreground_process_info: Option<LocalProcessInfo>,
|
foreground_process_info: Option<LocalProcessInfo>,
|
||||||
|
scroll_px: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Terminal {
|
impl Terminal {
|
||||||
|
@ -535,16 +521,42 @@ impl Terminal {
|
||||||
}
|
}
|
||||||
AlacTermEvent::Wakeup => {
|
AlacTermEvent::Wakeup => {
|
||||||
cx.emit(Event::Wakeup);
|
cx.emit(Event::Wakeup);
|
||||||
cx.notify();
|
|
||||||
|
if self.update_process_info() {
|
||||||
|
cx.emit(Event::TitleChanged)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AlacTermEvent::ColorRequest(idx, fun_ptr) => {
|
AlacTermEvent::ColorRequest(idx, fun_ptr) => {
|
||||||
self.events
|
self.events
|
||||||
.push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
|
.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
|
///Takes events from Alacritty and translates them to behavior on this view
|
||||||
fn process_terminal_event(
|
fn process_terminal_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -678,7 +690,7 @@ impl Terminal {
|
||||||
let mut terminal = if let Some(term) = term.try_lock_unfair() {
|
let mut terminal = if let Some(term) = term.try_lock_unfair() {
|
||||||
term
|
term
|
||||||
} else if self.last_synced.elapsed().as_secs_f32() > 0.25 {
|
} 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 {
|
} else if let None = self.sync_task {
|
||||||
//Skip this frame
|
//Skip this frame
|
||||||
let delay = cx.background().timer(Duration::from_millis(16));
|
let delay = cx.background().timer(Duration::from_millis(16));
|
||||||
|
@ -699,24 +711,15 @@ impl Terminal {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if self.update_process_info() {
|
||||||
|
cx.emit(Event::TitleChanged);
|
||||||
|
}
|
||||||
|
|
||||||
//Note that the ordering of events matters for event processing
|
//Note that the ordering of events matters for event processing
|
||||||
while let Some(e) = self.events.pop_front() {
|
while let Some(e) = self.events.pop_front() {
|
||||||
self.process_terminal_event(&e, &mut terminal, cx)
|
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_content = Self::make_content(&terminal);
|
||||||
self.last_synced = Instant::now();
|
self.last_synced = Instant::now();
|
||||||
}
|
}
|
||||||
|
@ -893,47 +896,69 @@ impl Terminal {
|
||||||
|
|
||||||
///Scroll the terminal
|
///Scroll the terminal
|
||||||
pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Vector2F) {
|
pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Vector2F) {
|
||||||
if self.mouse_mode(e.shift) {
|
let mouse_mode = 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) =
|
if let Some(scroll_lines) = self.determine_scroll_lines(e, mouse_mode) {
|
||||||
scroll_report(point, scroll_lines as i32, e, self.last_content.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(alt_scroll(scroll_lines))
|
||||||
self.pty_tx.notify(scroll);
|
} 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);
|
|
||||||
|
|
||||||
self.events.push_back(InternalEvent::Scroll(scroll));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find_matches(
|
pub fn find_matches(
|
||||||
&mut self,
|
&mut self,
|
||||||
query: project::search::SearchQuery,
|
query: project::search::SearchQuery,
|
||||||
|
@ -957,17 +982,9 @@ impl Terminal {
|
||||||
|
|
||||||
let term = term.lock();
|
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 {
|
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()
|
/// Copied from alacritty/src/display/hint.rs HintMatches::visible_regex_matches()
|
||||||
/// Iterate over all 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>,
|
term: &'a Term<T>,
|
||||||
regex: &'a RegexSearch,
|
regex: &'a RegexSearch,
|
||||||
) -> impl Iterator<Item = Match> + 'a {
|
) -> impl Iterator<Item = Match> + 'a {
|
||||||
let viewport_start = Line(-(term.grid().display_offset() as i32));
|
let start = Point::new(term.grid().topmost_line(), Column(0));
|
||||||
let viewport_end = viewport_start + term.bottommost_line();
|
let end = Point::new(term.grid().bottommost_line(), term.grid().last_column());
|
||||||
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)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
pub mod terminal_test_context;
|
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,
|
actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, Task,
|
||||||
View, ViewContext, ViewHandle,
|
View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
|
use util::truncate_and_trailoff;
|
||||||
use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
|
use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
|
||||||
use workspace::{Item, Workspace};
|
use workspace::{Item, Workspace};
|
||||||
|
|
||||||
|
@ -149,6 +150,13 @@ impl TerminalContainer {
|
||||||
associated_directory: None,
|
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 {
|
impl View for TerminalContainer {
|
||||||
|
@ -246,12 +254,28 @@ impl Item for TerminalContainer {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|fpi| {
|
.map(|fpi| {
|
||||||
format!(
|
format!(
|
||||||
"{} - {}",
|
"{} — {}",
|
||||||
fpi.cwd
|
truncate_and_trailoff(
|
||||||
.file_name()
|
&fpi.cwd
|
||||||
.map(|name| name.to_string_lossy().to_string())
|
.file_name()
|
||||||
.unwrap_or_default(),
|
.map(|name| name.to_string_lossy().to_string())
|
||||||
fpi.name,
|
.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()),
|
.unwrap_or_else(|| "Terminal".to_string()),
|
||||||
|
@ -324,18 +348,14 @@ impl Item for TerminalContainer {
|
||||||
|
|
||||||
fn is_dirty(&self, cx: &gpui::AppContext) -> bool {
|
fn is_dirty(&self, cx: &gpui::AppContext) -> bool {
|
||||||
if let TerminalContainerContent::Connected(connected) = &self.content {
|
if let TerminalContainerContent::Connected(connected) = &self.content {
|
||||||
connected.read(cx).has_new_content()
|
connected.read(cx).has_bell()
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_conflict(&self, cx: &AppContext) -> bool {
|
fn has_conflict(&self, _cx: &AppContext) -> bool {
|
||||||
if let TerminalContainerContent::Connected(connected) = &self.content {
|
false
|
||||||
connected.read(cx).has_bell()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_update_tab_on_event(event: &Self::Event) -> bool {
|
fn should_update_tab_on_event(event: &Self::Event) -> bool {
|
||||||
|
@ -431,28 +451,42 @@ impl SearchableItem for TerminalContainer {
|
||||||
matches: Vec<Self::Match>,
|
matches: Vec<Self::Match>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
if let TerminalContainerContent::Connected(connected) = &self.content {
|
let connected = self.connected();
|
||||||
if let Some(selection_head) = connected.read(cx).terminal().read(cx).selection_head {
|
// 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
|
// If selection head is contained in a match. Return that match
|
||||||
for (ix, search_match) in matches.iter().enumerate() {
|
if let Some(ix) = matches
|
||||||
if search_match.contains(&selection_head) {
|
.iter()
|
||||||
return Some(ix);
|
.enumerate()
|
||||||
}
|
.find(|(_, search_match)| {
|
||||||
|
search_match.contains(&selection_head)
|
||||||
// If not contained, return the next match after the selection head
|
|| search_match.start() > &selection_head
|
||||||
if search_match.start() > &selection_head {
|
})
|
||||||
return Some(ix);
|
.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 {
|
} else {
|
||||||
Some(0)
|
// Matches found but no active selection, return the first last one (closest to cursor)
|
||||||
|
Some(matches.len().saturating_sub(1))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
};
|
||||||
|
|
||||||
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -91,8 +91,8 @@ impl TerminalView {
|
||||||
if !cx.is_self_focused() {
|
if !cx.is_self_focused() {
|
||||||
this.has_new_content = true;
|
this.has_new_content = true;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
cx.emit(Event::Wakeup);
|
|
||||||
}
|
}
|
||||||
|
cx.emit(Event::Wakeup);
|
||||||
}
|
}
|
||||||
Event::Bell => {
|
Event::Bell => {
|
||||||
this.has_bell = true;
|
this.has_bell = true;
|
||||||
|
|
|
@ -9,6 +9,23 @@ use std::{
|
||||||
task::{Context, Poll},
|
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 {
|
pub fn post_inc<T: From<u8> + AddAssign<T> + Copy>(value: &mut T) -> T {
|
||||||
let prev = *value;
|
let prev = *value;
|
||||||
*value += T::from(1);
|
*value += T::from(1);
|
||||||
|
|
77
crates/workspace/src/programs.rs
Normal file
77
crates/workspace/src/programs.rs
Normal file
|
@ -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.
|
/// specific locations.
|
||||||
pub mod pane;
|
pub mod pane;
|
||||||
pub mod pane_group;
|
pub mod pane_group;
|
||||||
|
pub mod programs;
|
||||||
pub mod searchable;
|
pub mod searchable;
|
||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
mod status_bar;
|
mod status_bar;
|
||||||
|
@ -36,6 +37,7 @@ use log::error;
|
||||||
pub use pane::*;
|
pub use pane::*;
|
||||||
pub use pane_group::*;
|
pub use pane_group::*;
|
||||||
use postage::prelude::Stream;
|
use postage::prelude::Stream;
|
||||||
|
use programs::ProgramManager;
|
||||||
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
|
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
|
||||||
use searchable::SearchableItemHandle;
|
use searchable::SearchableItemHandle;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -144,6 +146,9 @@ impl_internal_actions!(
|
||||||
impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
|
impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
|
||||||
|
|
||||||
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
|
// Initialize the program manager immediately
|
||||||
|
cx.set_global(ProgramManager::new());
|
||||||
|
|
||||||
pane::init(cx);
|
pane::init(cx);
|
||||||
|
|
||||||
cx.add_global_action(open);
|
cx.add_global_action(open);
|
||||||
|
|
Loading…
Reference in a new issue