diff --git a/Cargo.lock b/Cargo.lock index 7d7dc42bea70cb3715c39cbddfa8ed7b5c1025a2..ddd7a0f7fdf7f621dec1db82fe5791973e037cdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4463,6 +4463,7 @@ dependencies = [ "smol", "sum_tree", "tempdir", + "terminal", "text", "thiserror", "toml", @@ -6259,6 +6260,32 @@ name = "terminal" version = "0.1.0" dependencies = [ "alacritty_terminal", + "anyhow", + "db", + "dirs 4.0.0", + "futures 0.3.25", + "gpui", + "itertools", + "lazy_static", + "libc", + "mio-extras", + "ordered-float", + "procinfo", + "rand 0.8.5", + "serde", + "settings", + "shellexpand", + "smallvec", + "smol", + "theme", + "thiserror", + "util", +] + +[[package]] +name = "terminal_view" +version = "0.1.0" +dependencies = [ "anyhow", "client", "context_menu", @@ -6281,6 +6308,7 @@ dependencies = [ "shellexpand", "smallvec", "smol", + "terminal", "theme", "thiserror", "util", @@ -8166,7 +8194,7 @@ dependencies = [ "smol", "sum_tree", "tempdir", - "terminal", + "terminal_view", "text", "theme", "theme_selector", diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 3639afd47c78899d9a98a24fd5ddcceb6e028a5c..a77ae4925d0d0f4fc5af80225385eb59ef2c9af5 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -6022,7 +6022,7 @@ impl TestServer { fs: fs.clone(), build_window_options: Default::default, initialize_workspace: |_, _, _| unimplemented!(), - default_item_factory: |_, _| unimplemented!(), + dock_default_item_factory: |_, _| unimplemented!(), }); Project::init(&client); diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index abc62605f93cc34046e3663e41c0e561f14b3cb6..1b851c3f7595e000e3242bec529b9ff4c8d7bc45 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -54,7 +54,7 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { Default::default(), 0, project, - app_state.default_item_factory, + app_state.dock_default_item_factory, cx, ); (app_state.initialize_workspace)(&mut workspace, &app_state, cx); diff --git a/crates/db/src/query.rs b/crates/db/src/query.rs index 731fca15cb5c47b58e89aac1eb2a7b42189829c2..01132d383c2928937d26d7733870f2c430798a39 100644 --- a/crates/db/src/query.rs +++ b/crates/db/src/query.rs @@ -199,10 +199,10 @@ macro_rules! query { use $crate::anyhow::Context; - self.write(|connection| { + self.write(move |connection| { let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); - connection.select_row_bound::<($($arg_type),+), $return_type>(indoc! { $sql })?(($($arg),+)) + connection.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+)) .context(::std::format!( "Error in {}, select_row_bound failed to execute or parse for: {}", ::std::stringify!($id), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 417b60bc5bb99dc20bc128e57e5b7708962456ff..ad21622fd9eb6d52f59a26dc85e3881165a44eef 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2422,7 +2422,7 @@ impl Editor { let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| { let excerpt_range = excerpt_range.to_offset(buffer); buffer - .edited_ranges_for_transaction(transaction) + .edited_ranges_for_transaction::(transaction) .all(|range| { excerpt_range.start <= range.start && excerpt_range.end >= range.end diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 76c60f9556235605e59a62cc5bd8c70aa9aaff1b..dd4d2be5b6847b24d2cf952a73b313c7d1676f49 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -32,6 +32,7 @@ lsp = { path = "../lsp" } rpc = { path = "../rpc" } settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } +terminal = { path = "../terminal" } util = { path = "../util" } aho-corasick = "0.7" anyhow = "1.0.57" diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 512ac702d062be924ac5183a9e98d34be80a45f7..9b4a163af4e6f78e397c730f444a4f697ece1e22 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -62,6 +62,7 @@ use std::{ }, time::Instant, }; +use terminal::{Terminal, TerminalBuilder}; use thiserror::Error; use util::{defer, post_inc, ResultExt, TryFutureExt as _}; @@ -1193,6 +1194,34 @@ impl Project { !self.is_local() } + pub fn create_terminal( + &mut self, + working_directory: Option, + window_id: usize, + cx: &mut ModelContext, + ) -> Result> { + if self.is_remote() { + return Err(anyhow!( + "creating terminals as a guest is not supported yet" + )); + } else { + let settings = cx.global::(); + let shell = settings.terminal_shell(); + let envs = settings.terminal_env(); + let scroll = settings.terminal_scroll(); + + TerminalBuilder::new( + working_directory.clone(), + shell, + envs, + settings.terminal_overrides.blinking.clone(), + scroll, + window_id, + ) + .map(|builder| cx.add_model(|cx| builder.subscribe(cx))) + } + } + pub fn create_buffer( &mut self, text: &str, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 5137751579e90fc52aa8efbd0f049975fd79cc54..f0c64a1bb995f3a710301de16fd8c00e02dc0088 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -199,7 +199,7 @@ impl Default for Shell { } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum AlternateScroll { On, @@ -221,6 +221,12 @@ pub enum WorkingDirectory { Always { directory: String }, } +impl Default for WorkingDirectory { + fn default() -> Self { + Self::CurrentProjectDirectory + } +} + #[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum DockAnchor { @@ -473,6 +479,32 @@ impl Settings { }) } + fn terminal_setting(&self, f: F) -> R + where + F: Fn(&TerminalSettings) -> Option<&R>, + { + f(&self.terminal_overrides) + .or_else(|| f(&self.terminal_defaults)) + .cloned() + .unwrap_or_else(|| R::default()) + } + + pub fn terminal_scroll(&self) -> AlternateScroll { + self.terminal_setting(|terminal_setting| terminal_setting.alternate_scroll.as_ref()) + } + + pub fn terminal_shell(&self) -> Shell { + self.terminal_setting(|terminal_setting| terminal_setting.shell.as_ref()) + } + + pub fn terminal_env(&self) -> HashMap { + self.terminal_setting(|terminal_setting| terminal_setting.env.as_ref()) + } + + pub fn terminal_strategy(&self) -> WorkingDirectory { + self.terminal_setting(|terminal_setting| terminal_setting.working_directory.as_ref()) + } + #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &gpui::AppContext) -> Settings { Settings { diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 5593ee92d4dc4fc4135c8f30a6dbaeee6753eb6d..0dea7bfbcfa80ef39150f68d26f5b74978c53b65 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -7,17 +7,13 @@ edition = "2021" path = "src/terminal.rs" doctest = false + [dependencies] -context_menu = { path = "../context_menu" } -editor = { path = "../editor" } -language = { path = "../language" } gpui = { path = "../gpui" } -project = { path = "../project" } settings = { path = "../settings" } +db = { path = "../db" } theme = { path = "../theme" } util = { path = "../util" } -workspace = { path = "../workspace" } -db = { path = "../db" } alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "a51dbe25d67e84d6ed4261e640d3954fbdd9be45" } procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } smallvec = { version = "1.6", features = ["union"] } @@ -34,11 +30,5 @@ thiserror = "1.0" lazy_static = "1.4.0" serde = { version = "1.0", features = ["derive"] } - - [dev-dependencies] -gpui = { path = "../gpui", features = ["test-support"] } -client = { path = "../client", features = ["test-support"]} -project = { path = "../project", features = ["test-support"]} -workspace = { path = "../workspace", features = ["test-support"] } rand = "0.8.5" diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 0cbb6d36b11bffd07c04f5fae65504b0dac29136..7cdac33cda45da3f9c85f3144f1be04e7abadbf0 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,8 +1,5 @@ pub mod mappings; -mod persistence; -pub mod terminal_container_view; -pub mod terminal_element; -pub mod terminal_view; +pub use alacritty_terminal; use alacritty_terminal::{ ansi::{ClearMode, Handler}, @@ -33,11 +30,9 @@ use mappings::mouse::{ alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report, }; -use persistence::TERMINAL_CONNECTION; use procinfo::LocalProcessInfo; use settings::{AlternateScroll, Settings, Shell, TerminalBlink}; use util::ResultExt; -use workspace::{ItemId, WorkspaceId}; use std::{ cmp::min, @@ -57,8 +52,7 @@ use gpui::{ geometry::vector::{vec2f, Vector2F}, keymap::Keystroke, scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp}, - AppContext, ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, - MutableAppContext, Task, + ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, Task, }; use crate::mappings::{ @@ -67,12 +61,6 @@ use crate::mappings::{ }; use lazy_static::lazy_static; -///Initialize and register all of our action handlers -pub fn init(cx: &mut MutableAppContext) { - terminal_view::init(cx); - terminal_container_view::init(cx); -} - ///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. @@ -128,10 +116,10 @@ impl EventListener for ZedListener { #[derive(Clone, Copy, Debug)] pub struct TerminalSize { - cell_width: f32, - line_height: f32, - height: f32, - width: f32, + pub cell_width: f32, + pub line_height: f32, + pub height: f32, + pub width: f32, } impl TerminalSize { @@ -210,7 +198,7 @@ impl Dimensions for TerminalSize { #[derive(Error, Debug)] pub struct TerminalError { pub directory: Option, - pub shell: Option, + pub shell: Shell, pub source: std::io::Error, } @@ -238,24 +226,20 @@ impl TerminalError { }) } - pub fn shell_to_string(&self) -> Option { - self.shell.as_ref().map(|shell| match shell { + pub fn shell_to_string(&self) -> String { + match &self.shell { Shell::System => "".to_string(), Shell::Program(p) => p.to_string(), Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), - }) + } } pub fn fmt_shell(&self) -> String { - self.shell - .clone() - .map(|shell| match shell { - Shell::System => "".to_string(), - - Shell::Program(s) => s, - Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), - }) - .unwrap_or_else(|| "".to_string()) + match &self.shell { + Shell::System => "".to_string(), + Shell::Program(s) => s.to_string(), + Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), + } } } @@ -280,20 +264,18 @@ pub struct TerminalBuilder { impl TerminalBuilder { pub fn new( working_directory: Option, - shell: Option, - env: Option>, + shell: Shell, + mut env: HashMap, blink_settings: Option, - alternate_scroll: &AlternateScroll, + alternate_scroll: AlternateScroll, window_id: usize, - item_id: ItemId, - workspace_id: WorkspaceId, ) -> Result { let pty_config = { - let alac_shell = shell.clone().and_then(|shell| match shell { + let alac_shell = match shell.clone() { Shell::System => None, Shell::Program(program) => Some(Program::Just(program)), Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }), - }); + }; PtyConfig { shell: alac_shell, @@ -302,10 +284,9 @@ impl TerminalBuilder { } }; - let mut env = env.unwrap_or_default(); - //TODO: Properly set the current locale, env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string()); + env.insert("ZED_TERM".to_string(), true.to_string()); let alac_scrolling = Scrolling::default(); // alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32); @@ -391,8 +372,6 @@ impl TerminalBuilder { last_mouse_position: None, next_link_id: 0, selection_phase: SelectionPhase::Ended, - workspace_id, - item_id, }; Ok(TerminalBuilder { @@ -464,9 +443,9 @@ impl TerminalBuilder { } #[derive(Debug, Clone)] -struct IndexedCell { - point: Point, - cell: Cell, +pub struct IndexedCell { + pub point: Point, + pub cell: Cell, } impl Deref for IndexedCell { @@ -478,17 +457,18 @@ impl Deref for IndexedCell { } } +// TODO: Un-pub #[derive(Clone)] pub struct TerminalContent { - cells: Vec, - mode: TermMode, - display_offset: usize, - selection_text: Option, - selection: Option, - cursor: RenderableCursor, - cursor_char: char, - size: TerminalSize, - last_hovered_hyperlink: Option<(String, RangeInclusive, usize)>, + pub cells: Vec, + pub mode: TermMode, + pub display_offset: usize, + pub selection_text: Option, + pub selection: Option, + pub cursor: RenderableCursor, + pub cursor_char: char, + pub size: TerminalSize, + pub last_hovered_hyperlink: Option<(String, RangeInclusive, usize)>, } impl Default for TerminalContent { @@ -525,19 +505,17 @@ pub struct Terminal { /// This is only used for terminal hyperlink checking last_mouse_position: Option, pub matches: Vec>, - last_content: TerminalContent, + pub last_content: TerminalContent, last_synced: Instant, sync_task: Option>, - selection_head: Option, - breadcrumb_text: String, + pub selection_head: Option, + pub breadcrumb_text: String, shell_pid: u32, shell_fd: u32, - foreground_process_info: Option, + pub foreground_process_info: Option, scroll_px: f32, next_link_id: usize, selection_phase: SelectionPhase, - workspace_id: WorkspaceId, - item_id: ItemId, } impl Terminal { @@ -578,20 +556,6 @@ impl Terminal { if self.update_process_info() { cx.emit(Event::TitleChanged); - - if let Some(foreground_info) = &self.foreground_process_info { - let cwd = foreground_info.cwd.clone(); - let item_id = self.item_id; - let workspace_id = self.workspace_id; - cx.background() - .spawn(async move { - TERMINAL_CONNECTION - .save_working_directory(item_id, workspace_id, cwd) - .await - .log_err(); - }) - .detach(); - } } } AlacTermEvent::ColorRequest(idx, fun_ptr) => { @@ -1194,42 +1158,13 @@ impl Terminal { } } - pub fn set_workspace_id(&mut self, id: WorkspaceId, cx: &AppContext) { - let old_workspace_id = self.workspace_id; - let item_id = self.item_id; - cx.background() - .spawn(async move { - TERMINAL_CONNECTION - .update_workspace_id(id, old_workspace_id, item_id) - .await - .log_err() - }) - .detach(); - - self.workspace_id = id; - } - pub fn find_matches( &mut self, - query: project::search::SearchQuery, + searcher: RegexSearch, cx: &mut ModelContext, ) -> Task>> { let term = self.term.clone(); cx.background().spawn(async move { - let searcher = match query { - project::search::SearchQuery::Text { query, .. } => { - RegexSearch::new(query.as_ref()) - } - project::search::SearchQuery::Regex { query, .. } => { - RegexSearch::new(query.as_ref()) - } - }; - - if searcher.is_err() { - return Vec::new(); - } - let searcher = searcher.unwrap(); - let term = term.lock(); all_search_matches(&term, &searcher).collect() @@ -1326,14 +1261,14 @@ fn open_uri(uri: &str) -> Result<(), std::io::Error> { #[cfg(test)] mod tests { + use alacritty_terminal::{ + index::{Column, Line, Point}, + term::cell::Cell, + }; use gpui::geometry::vector::vec2f; - use rand::{thread_rng, Rng}; - - use crate::content_index_for_mouse; - - use self::terminal_test_context::TerminalTestContext; + use rand::{rngs::ThreadRng, thread_rng, Rng}; - pub mod terminal_test_context; + use crate::{content_index_for_mouse, IndexedCell, TerminalContent, TerminalSize}; #[test] fn test_mouse_to_cell() { @@ -1350,7 +1285,7 @@ mod tests { width: cell_size * (viewport_cells as f32), }; - let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng); + let (content, cells) = create_terminal_content(size, &mut rng); for i in 0..(viewport_cells - 1) { let i = i as usize; @@ -1386,7 +1321,7 @@ mod tests { width: 100., }; - let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng); + let (content, cells) = create_terminal_content(size, &mut rng); assert_eq!( content.cells[content_index_for_mouse(vec2f(-10., -10.), &content)].c, @@ -1397,4 +1332,37 @@ mod tests { cells[9][9] ); } + + fn create_terminal_content( + size: TerminalSize, + rng: &mut ThreadRng, + ) -> (TerminalContent, Vec>) { + let mut ic = Vec::new(); + let mut cells = Vec::new(); + + for row in 0..((size.height() / size.line_height()) as usize) { + let mut row_vec = Vec::new(); + for col in 0..((size.width() / size.cell_width()) as usize) { + let cell_char = rng.gen(); + ic.push(IndexedCell { + point: Point::new(Line(row as i32), Column(col)), + cell: Cell { + c: cell_char, + ..Default::default() + }, + }); + row_vec.push(cell_char) + } + cells.push(row_vec) + } + + ( + TerminalContent { + cells: ic, + size, + ..Default::default() + }, + cells, + ) + } } diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal/src/terminal_container_view.rs deleted file mode 100644 index 8f4bfeeb5364d6c3c49f20976230c17efb148379..0000000000000000000000000000000000000000 --- a/crates/terminal/src/terminal_container_view.rs +++ /dev/null @@ -1,711 +0,0 @@ -use crate::persistence::TERMINAL_CONNECTION; -use crate::terminal_view::TerminalView; -use crate::{Event, TerminalBuilder, TerminalError}; - -use alacritty_terminal::index::Point; -use dirs::home_dir; -use gpui::{ - actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, Task, - View, ViewContext, ViewHandle, WeakViewHandle, -}; -use util::{truncate_and_trailoff, ResultExt}; -use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}; -use workspace::{ - item::{Item, ItemEvent}, - ToolbarItemLocation, Workspace, -}; -use workspace::{register_deserializable_item, Pane, WorkspaceId}; - -use project::{LocalWorktree, Project, ProjectPath}; -use settings::{AlternateScroll, Settings, WorkingDirectory}; -use smallvec::SmallVec; -use std::ops::RangeInclusive; -use std::path::{Path, PathBuf}; - -use crate::terminal_element::TerminalElement; - -actions!(terminal, [DeployModal]); - -pub fn init(cx: &mut MutableAppContext) { - cx.add_action(TerminalContainer::deploy); - - register_deserializable_item::(cx); -} - -//Make terminal view an enum, that can give you views for the error and non-error states -//Take away all the result unwrapping in the current TerminalView by making it 'infallible' -//Bubble up to deploy(_modal)() calls - -pub enum TerminalContainerContent { - Connected(ViewHandle), - Error(ViewHandle), -} - -impl TerminalContainerContent { - fn handle(&self) -> AnyViewHandle { - match self { - Self::Connected(handle) => handle.into(), - Self::Error(handle) => handle.into(), - } - } -} - -pub struct TerminalContainer { - pub content: TerminalContainerContent, - associated_directory: Option, -} - -pub struct ErrorView { - error: TerminalError, -} - -impl Entity for TerminalContainer { - type Event = Event; -} - -impl Entity for ErrorView { - type Event = Event; -} - -impl TerminalContainer { - ///Create a new Terminal in the current working directory or the user's home directory - pub fn deploy( - workspace: &mut Workspace, - _: &workspace::NewTerminal, - cx: &mut ViewContext, - ) { - let strategy = cx - .global::() - .terminal_overrides - .working_directory - .clone() - .unwrap_or(WorkingDirectory::CurrentProjectDirectory); - - let working_directory = get_working_directory(workspace, cx, strategy); - let view = cx.add_view(|cx| { - TerminalContainer::new(working_directory, false, workspace.database_id(), cx) - }); - workspace.add_item(Box::new(view), cx); - } - - ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices - pub fn new( - working_directory: Option, - modal: bool, - workspace_id: WorkspaceId, - cx: &mut ViewContext, - ) -> Self { - let settings = cx.global::(); - let shell = settings.terminal_overrides.shell.clone(); - let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap. - - //TODO: move this pattern to settings - let scroll = settings - .terminal_overrides - .alternate_scroll - .as_ref() - .unwrap_or( - settings - .terminal_defaults - .alternate_scroll - .as_ref() - .unwrap_or_else(|| &AlternateScroll::On), - ); - - let content = match TerminalBuilder::new( - working_directory.clone(), - shell, - envs, - settings.terminal_overrides.blinking.clone(), - scroll, - cx.window_id(), - cx.view_id(), - workspace_id, - ) { - Ok(terminal) => { - let terminal = cx.add_model(|cx| terminal.subscribe(cx)); - let view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx)); - - cx.subscribe(&view, |_this, _content, event, cx| cx.emit(*event)) - .detach(); - TerminalContainerContent::Connected(view) - } - Err(error) => { - let view = cx.add_view(|_| ErrorView { - error: error.downcast::().unwrap(), - }); - TerminalContainerContent::Error(view) - } - }; - - TerminalContainer { - content, - associated_directory: working_directory, - } - } - - fn connected(&self) -> Option> { - match &self.content { - TerminalContainerContent::Connected(vh) => Some(vh.clone()), - TerminalContainerContent::Error(_) => None, - } - } -} - -impl View for TerminalContainer { - fn ui_name() -> &'static str { - "Terminal" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - match &self.content { - TerminalContainerContent::Connected(connected) => ChildView::new(connected, cx), - TerminalContainerContent::Error(error) => ChildView::new(error, cx), - } - .boxed() - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - cx.focus(self.content.handle()); - } - } -} - -impl View for ErrorView { - fn ui_name() -> &'static str { - "Terminal Error" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let settings = cx.global::(); - let style = TerminalElement::make_text_style(cx.font_cache(), settings); - - //TODO: - //We want markdown style highlighting so we can format the program and working directory with `` - //We want a max-width of 75% with word-wrap - //We want to be able to select the text - //Want to be able to scroll if the error message is massive somehow (resiliency) - - let program_text = { - match self.error.shell_to_string() { - Some(shell_txt) => format!("Shell Program: `{}`", shell_txt), - None => "No program specified".to_string(), - } - }; - - let directory_text = { - match self.error.directory.as_ref() { - Some(path) => format!("Working directory: `{}`", path.to_string_lossy()), - None => "No working directory specified".to_string(), - } - }; - - let error_text = self.error.source.to_string(); - - Flex::column() - .with_child( - Text::new("Failed to open the terminal.".to_string(), style.clone()) - .contained() - .boxed(), - ) - .with_child(Text::new(program_text, style.clone()).contained().boxed()) - .with_child(Text::new(directory_text, style.clone()).contained().boxed()) - .with_child(Text::new(error_text, style).contained().boxed()) - .aligned() - .boxed() - } -} - -impl Item for TerminalContainer { - fn tab_content( - &self, - _detail: Option, - tab_theme: &theme::Tab, - cx: &gpui::AppContext, - ) -> ElementBox { - let title = match &self.content { - TerminalContainerContent::Connected(connected) => connected - .read(cx) - .handle() - .read(cx) - .foreground_process_info - .as_ref() - .map(|fpi| { - format!( - "{} — {}", - 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()), - TerminalContainerContent::Error(_) => "Terminal".to_string(), - }; - - Flex::row() - .with_child( - Label::new(title, tab_theme.label.clone()) - .aligned() - .contained() - .boxed(), - ) - .boxed() - } - - fn clone_on_split( - &self, - workspace_id: WorkspaceId, - cx: &mut ViewContext, - ) -> Option { - //From what I can tell, there's no way to tell the current working - //Directory of the terminal from outside the shell. There might be - //solutions to this, but they are non-trivial and require more IPC - Some(TerminalContainer::new( - self.associated_directory.clone(), - false, - workspace_id, - cx, - )) - } - - fn project_path(&self, _cx: &gpui::AppContext) -> Option { - None - } - - fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { - SmallVec::new() - } - - fn is_singleton(&self, _cx: &gpui::AppContext) -> bool { - false - } - - fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} - - fn can_save(&self, _cx: &gpui::AppContext) -> bool { - false - } - - fn save( - &mut self, - _project: gpui::ModelHandle, - _cx: &mut ViewContext, - ) -> gpui::Task> { - unreachable!("save should not have been called"); - } - - fn save_as( - &mut self, - _project: gpui::ModelHandle, - _abs_path: std::path::PathBuf, - _cx: &mut ViewContext, - ) -> gpui::Task> { - unreachable!("save_as should not have been called"); - } - - fn reload( - &mut self, - _project: gpui::ModelHandle, - _cx: &mut ViewContext, - ) -> gpui::Task> { - gpui::Task::ready(Ok(())) - } - - fn is_dirty(&self, cx: &gpui::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 as_searchable(&self, handle: &ViewHandle) -> Option> { - Some(Box::new(handle.clone())) - } - - fn to_item_events(event: &Self::Event) -> Vec { - match event { - Event::BreadcrumbsChanged => vec![ItemEvent::UpdateBreadcrumbs], - Event::TitleChanged | Event::Wakeup => vec![ItemEvent::UpdateTab], - Event::CloseTerminal => vec![ItemEvent::CloseItem], - _ => vec![], - } - } - - fn breadcrumb_location(&self) -> ToolbarItemLocation { - if self.connected().is_some() { - ToolbarItemLocation::PrimaryLeft { flex: None } - } else { - ToolbarItemLocation::Hidden - } - } - - fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { - let connected = self.connected()?; - - Some(vec![Text::new( - connected - .read(cx) - .terminal() - .read(cx) - .breadcrumb_text - .to_string(), - theme.breadcrumbs.text.clone(), - ) - .boxed()]) - } - - fn serialized_item_kind() -> Option<&'static str> { - Some("Terminal") - } - - fn deserialize( - _project: ModelHandle, - _workspace: WeakViewHandle, - workspace_id: workspace::WorkspaceId, - item_id: workspace::ItemId, - cx: &mut ViewContext, - ) -> Task>> { - let working_directory = TERMINAL_CONNECTION.get_working_directory(item_id, workspace_id); - Task::ready(Ok(cx.add_view(|cx| { - TerminalContainer::new( - working_directory.log_err().flatten(), - false, - workspace_id, - cx, - ) - }))) - } - - fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { - if let Some(connected) = self.connected() { - let id = workspace.database_id(); - let terminal_handle = connected.read(cx).terminal().clone(); - terminal_handle.update(cx, |terminal, cx| terminal.set_workspace_id(id, cx)) - } - } -} - -impl SearchableItem for TerminalContainer { - type Match = RangeInclusive; - - fn supported_options() -> SearchOptions { - SearchOptions { - case: false, - word: false, - regex: false, - } - } - - /// Convert events raised by this item into search-relevant events (if applicable) - fn to_search_event(event: &Self::Event) -> Option { - match event { - Event::Wakeup => Some(SearchEvent::MatchesInvalidated), - Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged), - _ => None, - } - } - - /// Clear stored matches - fn clear_matches(&mut self, cx: &mut ViewContext) { - if let TerminalContainerContent::Connected(connected) = &self.content { - let terminal = connected.read(cx).terminal().clone(); - terminal.update(cx, |term, _| term.matches.clear()) - } - } - - /// Store matches returned from find_matches somewhere for rendering - fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext) { - if let TerminalContainerContent::Connected(connected) = &self.content { - let terminal = connected.read(cx).terminal().clone(); - terminal.update(cx, |term, _| term.matches = matches) - } - } - - /// Return the selection content to pre-load into this search - fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { - if let TerminalContainerContent::Connected(connected) = &self.content { - let terminal = connected.read(cx).terminal().clone(); - terminal - .read(cx) - .last_content - .selection_text - .clone() - .unwrap_or_default() - } else { - Default::default() - } - } - - /// Focus match at given index into the Vec of matches - fn activate_match(&mut self, index: usize, _: Vec, cx: &mut ViewContext) { - if let TerminalContainerContent::Connected(connected) = &self.content { - let terminal = connected.read(cx).terminal().clone(); - terminal.update(cx, |term, _| term.activate_match(index)); - cx.notify(); - } - } - - /// Get all of the matches for this query, should be done on the background - fn find_matches( - &mut self, - query: project::search::SearchQuery, - cx: &mut ViewContext, - ) -> Task> { - if let TerminalContainerContent::Connected(connected) = &self.content { - let terminal = connected.read(cx).terminal().clone(); - terminal.update(cx, |term, cx| term.find_matches(query, cx)) - } else { - Task::ready(Vec::new()) - } - } - - /// Reports back to the search toolbar what the active match should be (the selection) - fn active_match_index( - &mut self, - matches: Vec, - cx: &mut ViewContext, - ) -> Option { - 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 - 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)) - } - } else { - // Matches found but no active selection, return the first last one (closest to cursor) - Some(matches.len().saturating_sub(1)) - } - } else { - None - }; - - res - } -} - -///Get's the working directory for the given workspace, respecting the user's settings. -pub fn get_working_directory( - workspace: &Workspace, - cx: &AppContext, - strategy: WorkingDirectory, -) -> Option { - let res = match strategy { - WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx) - .or_else(|| first_project_directory(workspace, cx)), - WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), - WorkingDirectory::AlwaysHome => None, - WorkingDirectory::Always { directory } => { - shellexpand::full(&directory) //TODO handle this better - .ok() - .map(|dir| Path::new(&dir.to_string()).to_path_buf()) - .filter(|dir| dir.is_dir()) - } - }; - res.or_else(home_dir) -} - -///Get's the first project's home directory, or the home directory -fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { - workspace - .worktrees(cx) - .next() - .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) - .and_then(get_path_from_wt) -} - -///Gets the intuitively correct working directory from the given workspace -///If there is an active entry for this project, returns that entry's worktree root. -///If there's no active entry but there is a worktree, returns that worktrees root. -///If either of these roots are files, or if there are any other query failures, -/// returns the user's home directory -fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { - let project = workspace.project().read(cx); - - project - .active_entry() - .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) - .or_else(|| workspace.worktrees(cx).next()) - .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) - .and_then(get_path_from_wt) -} - -fn get_path_from_wt(wt: &LocalWorktree) -> Option { - wt.root_entry() - .filter(|re| re.is_dir()) - .map(|_| wt.abs_path().to_path_buf()) -} - -#[cfg(test)] -mod tests { - - use super::*; - use gpui::TestAppContext; - - use std::path::Path; - - use crate::tests::terminal_test_context::TerminalTestContext; - - ///Working directory calculation tests - - ///No Worktrees in project -> home_dir() - #[gpui::test] - async fn no_worktree(cx: &mut TestAppContext) { - //Setup variables - let mut cx = TerminalTestContext::new(cx); - let (project, workspace) = cx.blank_workspace().await; - //Test - cx.cx.read(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - //Make sure enviroment is as expeted - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_none()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, None); - }); - } - - ///No active entry, but a worktree, worktree is a file -> home_dir() - #[gpui::test] - async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) { - //Setup variables - - let mut cx = TerminalTestContext::new(cx); - let (project, workspace) = cx.blank_workspace().await; - cx.create_file_wt(project.clone(), "/root.txt").await; - - cx.cx.read(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - //Make sure enviroment is as expeted - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, None); - }); - } - - //No active entry, but a worktree, worktree is a folder -> worktree_folder - #[gpui::test] - async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) { - //Setup variables - let mut cx = TerminalTestContext::new(cx); - let (project, workspace) = cx.blank_workspace().await; - let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root/").await; - - //Test - cx.cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); - }); - } - - //Active entry with a work tree, worktree is a file -> home_dir() - #[gpui::test] - async fn active_entry_worktree_is_file(cx: &mut TestAppContext) { - //Setup variables - let mut cx = TerminalTestContext::new(cx); - let (project, workspace) = cx.blank_workspace().await; - let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await; - let (wt2, entry2) = cx.create_file_wt(project.clone(), "/root2.txt").await; - cx.insert_active_entry_for(wt2, entry2, project.clone()); - - //Test - cx.cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - }); - } - - //Active entry, with a worktree, worktree is a folder -> worktree_folder - #[gpui::test] - async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) { - //Setup variables - let mut cx = TerminalTestContext::new(cx); - let (project, workspace) = cx.blank_workspace().await; - let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await; - let (wt2, entry2) = cx.create_folder_wt(project.clone(), "/root2/").await; - cx.insert_active_entry_for(wt2, entry2, project.clone()); - - //Test - cx.cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root2/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - }); - } -} diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs deleted file mode 100644 index 21e055319af62bfa42b168635fdbb5129c061492..0000000000000000000000000000000000000000 --- a/crates/terminal/src/terminal_view.rs +++ /dev/null @@ -1,471 +0,0 @@ -use std::{ops::RangeInclusive, time::Duration}; - -use alacritty_terminal::{index::Point, term::TermMode}; -use context_menu::{ContextMenu, ContextMenuItem}; -use gpui::{ - actions, - elements::{AnchorCorner, ChildView, ParentElement, Stack}, - geometry::vector::Vector2F, - impl_actions, impl_internal_actions, - keymap::Keystroke, - AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task, - View, ViewContext, ViewHandle, -}; -use serde::Deserialize; -use settings::{Settings, TerminalBlink}; -use smol::Timer; -use util::ResultExt; -use workspace::pane; - -use crate::{terminal_element::TerminalElement, Event, Terminal}; - -const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); - -///Event to transmit the scroll from the element to the view -#[derive(Clone, Debug, PartialEq)] -pub struct ScrollTerminal(pub i32); - -#[derive(Clone, PartialEq)] -pub struct DeployContextMenu { - pub position: Vector2F, -} - -#[derive(Clone, Default, Deserialize, PartialEq)] -pub struct SendText(String); - -#[derive(Clone, Default, Deserialize, PartialEq)] -pub struct SendKeystroke(String); - -actions!( - terminal, - [Clear, Copy, Paste, ShowCharacterPalette, SearchTest] -); - -impl_actions!(terminal, [SendText, SendKeystroke]); - -impl_internal_actions!(project_panel, [DeployContextMenu]); - -pub fn init(cx: &mut MutableAppContext) { - //Useful terminal views - cx.add_action(TerminalView::send_text); - cx.add_action(TerminalView::send_keystroke); - cx.add_action(TerminalView::deploy_context_menu); - cx.add_action(TerminalView::copy); - cx.add_action(TerminalView::paste); - cx.add_action(TerminalView::clear); - cx.add_action(TerminalView::show_character_palette); -} - -///A terminal view, maintains the PTY's file handles and communicates with the terminal -pub struct TerminalView { - terminal: ModelHandle, - has_new_content: bool, - //Currently using iTerm bell, show bell emoji in tab until input is received - has_bell: bool, - // Only for styling purposes. Doesn't effect behavior - modal: bool, - context_menu: ViewHandle, - blink_state: bool, - blinking_on: bool, - blinking_paused: bool, - blink_epoch: usize, -} - -impl Entity for TerminalView { - type Event = Event; -} - -impl TerminalView { - pub fn from_terminal( - terminal: ModelHandle, - modal: bool, - cx: &mut ViewContext, - ) -> Self { - cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); - cx.subscribe(&terminal, |this, _, event, cx| match event { - Event::Wakeup => { - if !cx.is_self_focused() { - this.has_new_content = true; - cx.notify(); - } - cx.emit(Event::Wakeup); - } - Event::Bell => { - this.has_bell = true; - cx.emit(Event::Wakeup); - } - Event::BlinkChanged => this.blinking_on = !this.blinking_on, - _ => cx.emit(*event), - }) - .detach(); - - Self { - terminal, - has_new_content: true, - has_bell: false, - modal, - context_menu: cx.add_view(ContextMenu::new), - blink_state: true, - blinking_on: false, - blinking_paused: false, - blink_epoch: 0, - } - } - - pub fn handle(&self) -> ModelHandle { - self.terminal.clone() - } - - pub fn has_new_content(&self) -> bool { - self.has_new_content - } - - pub fn has_bell(&self) -> bool { - self.has_bell - } - - pub fn clear_bel(&mut self, cx: &mut ViewContext) { - self.has_bell = false; - cx.emit(Event::Wakeup); - } - - pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext) { - let menu_entries = vec![ - ContextMenuItem::item("Clear", Clear), - ContextMenuItem::item("Close", pane::CloseActiveItem), - ]; - - self.context_menu.update(cx, |menu, cx| { - menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx) - }); - - cx.notify(); - } - - fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext) { - if !self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - cx.show_character_palette(); - } else { - self.terminal.update(cx, |term, cx| { - term.try_keystroke( - &Keystroke::parse("ctrl-cmd-space").unwrap(), - cx.global::() - .terminal_overrides - .option_as_meta - .unwrap_or(false), - ) - }); - } - } - - fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { - self.terminal.update(cx, |term, _| term.clear()); - cx.notify(); - } - - pub fn should_show_cursor( - &self, - focused: bool, - cx: &mut gpui::RenderContext<'_, Self>, - ) -> bool { - //Don't blink the cursor when not focused, blinking is disabled, or paused - if !focused - || !self.blinking_on - || self.blinking_paused - || self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - return true; - } - - let setting = { - let settings = cx.global::(); - settings - .terminal_overrides - .blinking - .clone() - .unwrap_or(TerminalBlink::TerminalControlled) - }; - - match setting { - //If the user requested to never blink, don't blink it. - TerminalBlink::Off => true, - //If the terminal is controlling it, check terminal mode - TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state, - } - } - - fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext) { - if epoch == self.blink_epoch && !self.blinking_paused { - self.blink_state = !self.blink_state; - cx.notify(); - - let epoch = self.next_blink_epoch(); - cx.spawn(|this, mut cx| { - let this = this.downgrade(); - async move { - Timer::after(CURSOR_BLINK_INTERVAL).await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx)); - } - } - }) - .detach(); - } - } - - pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext) { - self.blink_state = true; - cx.notify(); - - let epoch = self.next_blink_epoch(); - cx.spawn(|this, mut cx| { - let this = this.downgrade(); - async move { - Timer::after(CURSOR_BLINK_INTERVAL).await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx)) - } - } - }) - .detach(); - } - - pub fn find_matches( - &mut self, - query: project::search::SearchQuery, - cx: &mut ViewContext, - ) -> Task>> { - self.terminal - .update(cx, |term, cx| term.find_matches(query, cx)) - } - - pub fn terminal(&self) -> &ModelHandle { - &self.terminal - } - - fn next_blink_epoch(&mut self) -> usize { - self.blink_epoch += 1; - self.blink_epoch - } - - fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext) { - if epoch == self.blink_epoch { - self.blinking_paused = false; - self.blink_cursors(epoch, cx); - } - } - - ///Attempt to paste the clipboard into the terminal - fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { - self.terminal.update(cx, |term, _| term.copy()) - } - - ///Attempt to paste the clipboard into the terminal - fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { - if let Some(item) = cx.read_from_clipboard() { - self.terminal - .update(cx, |terminal, _cx| terminal.paste(item.text())); - } - } - - fn send_text(&mut self, text: &SendText, cx: &mut ViewContext) { - self.clear_bel(cx); - self.terminal.update(cx, |term, _| { - term.input(text.0.to_string()); - }); - } - - fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext) { - if let Some(keystroke) = Keystroke::parse(&text.0).log_err() { - self.clear_bel(cx); - self.terminal.update(cx, |term, cx| { - term.try_keystroke( - &keystroke, - cx.global::() - .terminal_overrides - .option_as_meta - .unwrap_or(false), - ); - }); - } - } -} - -impl View for TerminalView { - fn ui_name() -> &'static str { - "Terminal" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let terminal_handle = self.terminal.clone().downgrade(); - - let self_id = cx.view_id(); - let focused = cx - .focused_view_id(cx.window_id()) - .filter(|view_id| *view_id == self_id) - .is_some(); - - Stack::new() - .with_child( - TerminalElement::new( - cx.handle(), - terminal_handle, - focused, - self.should_show_cursor(focused, cx), - ) - .contained() - .boxed(), - ) - .with_child(ChildView::new(&self.context_menu, cx).boxed()) - .boxed() - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.has_new_content = false; - self.terminal.read(cx).focus_in(); - self.blink_cursors(self.blink_epoch, cx); - cx.notify(); - } - - fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.terminal.update(cx, |terminal, _| { - terminal.focus_out(); - }); - cx.notify(); - } - - fn key_down(&mut self, event: &gpui::KeyDownEvent, cx: &mut ViewContext) -> bool { - self.clear_bel(cx); - self.pause_cursor_blinking(cx); - - self.terminal.update(cx, |term, cx| { - term.try_keystroke( - &event.keystroke, - cx.global::() - .terminal_overrides - .option_as_meta - .unwrap_or(false), - ) - }) - } - - //IME stuff - fn selected_text_range(&self, cx: &AppContext) -> Option> { - if self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - None - } else { - Some(0..0) - } - } - - fn replace_text_in_range( - &mut self, - _: Option>, - text: &str, - cx: &mut ViewContext, - ) { - self.terminal.update(cx, |terminal, _| { - terminal.input(text.into()); - }); - } - - fn keymap_context(&self, cx: &gpui::AppContext) -> gpui::keymap::Context { - let mut context = Self::default_keymap_context(); - if self.modal { - context.set.insert("ModalTerminal".into()); - } - let mode = self.terminal.read(cx).last_content.mode; - context.map.insert( - "screen".to_string(), - (if mode.contains(TermMode::ALT_SCREEN) { - "alt" - } else { - "normal" - }) - .to_string(), - ); - - if mode.contains(TermMode::APP_CURSOR) { - context.set.insert("DECCKM".to_string()); - } - if mode.contains(TermMode::APP_KEYPAD) { - context.set.insert("DECPAM".to_string()); - } - //Note the ! here - if !mode.contains(TermMode::APP_KEYPAD) { - context.set.insert("DECPNM".to_string()); - } - if mode.contains(TermMode::SHOW_CURSOR) { - context.set.insert("DECTCEM".to_string()); - } - if mode.contains(TermMode::LINE_WRAP) { - context.set.insert("DECAWM".to_string()); - } - if mode.contains(TermMode::ORIGIN) { - context.set.insert("DECOM".to_string()); - } - if mode.contains(TermMode::INSERT) { - context.set.insert("IRM".to_string()); - } - //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html - if mode.contains(TermMode::LINE_FEED_NEW_LINE) { - context.set.insert("LNM".to_string()); - } - if mode.contains(TermMode::FOCUS_IN_OUT) { - context.set.insert("report_focus".to_string()); - } - if mode.contains(TermMode::ALTERNATE_SCROLL) { - context.set.insert("alternate_scroll".to_string()); - } - if mode.contains(TermMode::BRACKETED_PASTE) { - context.set.insert("bracketed_paste".to_string()); - } - if mode.intersects(TermMode::MOUSE_MODE) { - context.set.insert("any_mouse_reporting".to_string()); - } - { - let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) { - "click" - } else if mode.contains(TermMode::MOUSE_DRAG) { - "drag" - } else if mode.contains(TermMode::MOUSE_MOTION) { - "motion" - } else { - "off" - }; - context - .map - .insert("mouse_reporting".to_string(), mouse_reporting.to_string()); - } - { - let format = if mode.contains(TermMode::SGR_MOUSE) { - "sgr" - } else if mode.contains(TermMode::UTF8_MOUSE) { - "utf8" - } else { - "normal" - }; - context - .map - .insert("mouse_format".to_string(), format.to_string()); - } - context - } -} diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs deleted file mode 100644 index 67ebb558052f40a7f0d273034c3f7802fe8586ba..0000000000000000000000000000000000000000 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::{path::Path, time::Duration}; - -use alacritty_terminal::{ - index::{Column, Line, Point}, - term::cell::Cell, -}; -use gpui::{ModelHandle, TestAppContext, ViewHandle}; - -use project::{Entry, Project, ProjectPath, Worktree}; -use rand::{rngs::ThreadRng, Rng}; -use workspace::{AppState, Workspace}; - -use crate::{IndexedCell, TerminalContent, TerminalSize}; - -pub struct TerminalTestContext<'a> { - pub cx: &'a mut TestAppContext, -} - -impl<'a> TerminalTestContext<'a> { - pub fn new(cx: &'a mut TestAppContext) -> Self { - cx.set_condition_duration(Some(Duration::from_secs(5))); - - TerminalTestContext { cx } - } - - ///Creates a worktree with 1 file: /root.txt - pub async fn blank_workspace(&mut self) -> (ModelHandle, ViewHandle) { - let params = self.cx.update(AppState::test); - - let project = Project::test(params.fs.clone(), [], self.cx).await; - let (_, workspace) = self.cx.add_window(|cx| { - Workspace::new( - Default::default(), - 0, - project.clone(), - |_, _| unimplemented!(), - cx, - ) - }); - - (project, workspace) - } - - ///Creates a worktree with 1 folder: /root{suffix}/ - pub async fn create_folder_wt( - &mut self, - project: ModelHandle, - path: impl AsRef, - ) -> (ModelHandle, Entry) { - self.create_wt(project, true, path).await - } - - ///Creates a worktree with 1 file: /root{suffix}.txt - pub async fn create_file_wt( - &mut self, - project: ModelHandle, - path: impl AsRef, - ) -> (ModelHandle, Entry) { - self.create_wt(project, false, path).await - } - - async fn create_wt( - &mut self, - project: ModelHandle, - is_dir: bool, - path: impl AsRef, - ) -> (ModelHandle, Entry) { - let (wt, _) = project - .update(self.cx, |project, cx| { - project.find_or_create_local_worktree(path, true, cx) - }) - .await - .unwrap(); - - let entry = self - .cx - .update(|cx| { - wt.update(cx, |wt, cx| { - wt.as_local() - .unwrap() - .create_entry(Path::new(""), is_dir, cx) - }) - }) - .await - .unwrap(); - - (wt, entry) - } - - pub fn insert_active_entry_for( - &mut self, - wt: ModelHandle, - entry: Entry, - project: ModelHandle, - ) { - self.cx.update(|cx| { - let p = ProjectPath { - worktree_id: wt.read(cx).id(), - path: entry.path, - }; - project.update(cx, |project, cx| project.set_active_path(Some(p), cx)); - }); - } - - pub fn create_terminal_content( - size: TerminalSize, - rng: &mut ThreadRng, - ) -> (TerminalContent, Vec>) { - let mut ic = Vec::new(); - let mut cells = Vec::new(); - - for row in 0..((size.height() / size.line_height()) as usize) { - let mut row_vec = Vec::new(); - for col in 0..((size.width() / size.cell_width()) as usize) { - let cell_char = rng.gen(); - ic.push(IndexedCell { - point: Point::new(Line(row as i32), Column(col)), - cell: Cell { - c: cell_char, - ..Default::default() - }, - }); - row_vec.push(cell_char) - } - cells.push(row_vec) - } - - ( - TerminalContent { - cells: ic, - size, - ..Default::default() - }, - cells, - ) - } -} - -impl<'a> Drop for TerminalTestContext<'a> { - fn drop(&mut self) { - self.cx.set_condition_duration(None); - } -} diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..05fda2c75f6893c5d502b6b223044f8bceaa5ad6 --- /dev/null +++ b/crates/terminal_view/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "terminal_view" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/terminal_view.rs" +doctest = false + +[dependencies] +context_menu = { path = "../context_menu" } +editor = { path = "../editor" } +language = { path = "../language" } +gpui = { path = "../gpui" } +project = { path = "../project" } +settings = { path = "../settings" } +theme = { path = "../theme" } +util = { path = "../util" } +workspace = { path = "../workspace" } +db = { path = "../db" } +procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } +terminal = { path = "../terminal" } +smallvec = { version = "1.6", features = ["union"] } +smol = "1.2.5" +mio-extras = "2.0.6" +futures = "0.3" +ordered-float = "2.1.1" +itertools = "0.10" +dirs = "4.0.0" +shellexpand = "2.1.0" +libc = "0.2" +anyhow = "1" +thiserror = "1.0" +lazy_static = "1.4.0" +serde = { version = "1.0", features = ["derive"] } + + + +[dev-dependencies] +gpui = { path = "../gpui", features = ["test-support"] } +client = { path = "../client", features = ["test-support"]} +project = { path = "../project", features = ["test-support"]} +workspace = { path = "../workspace", features = ["test-support"] } +rand = "0.8.5" diff --git a/crates/terminal/README.md b/crates/terminal_view/README.md similarity index 100% rename from crates/terminal/README.md rename to crates/terminal_view/README.md diff --git a/crates/terminal/scripts/print256color.sh b/crates/terminal_view/scripts/print256color.sh similarity index 100% rename from crates/terminal/scripts/print256color.sh rename to crates/terminal_view/scripts/print256color.sh diff --git a/crates/terminal/scripts/truecolor.sh b/crates/terminal_view/scripts/truecolor.sh similarity index 100% rename from crates/terminal/scripts/truecolor.sh rename to crates/terminal_view/scripts/truecolor.sh diff --git a/crates/terminal/src/persistence.rs b/crates/terminal_view/src/persistence.rs similarity index 81% rename from crates/terminal/src/persistence.rs rename to crates/terminal_view/src/persistence.rs index 1669a3a546773fa461d94152953e962d4ac6ec7c..f090b384a44ae1c0a0e5743ed9060e14ccf6d052 100644 --- a/crates/terminal/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -1,11 +1,10 @@ use std::path::PathBuf; use db::{define_connection, query, sqlez_macros::sql}; - use workspace::{ItemId, WorkspaceDb, WorkspaceId}; define_connection! { - pub static ref TERMINAL_CONNECTION: TerminalDb = + pub static ref TERMINAL_DB: TerminalDb = &[sql!( CREATE TABLE terminals ( workspace_id INTEGER, @@ -13,7 +12,7 @@ define_connection! { working_directory BLOB, PRIMARY KEY(workspace_id, item_id), FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE + ON DELETE CASCADE ) STRICT; )]; } @@ -43,10 +42,10 @@ impl TerminalDb { } query! { - pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { - SELECT working_directory - FROM terminals + pub async fn take_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { + DELETE FROM terminals WHERE item_id = ? AND workspace_id = ? + RETURNING working_directory } } } diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs similarity index 98% rename from crates/terminal/src/terminal_element.rs rename to crates/terminal_view/src/terminal_element.rs index adfcb47024c91c79e420edbad98101599745bcfb..08ed3ecc2d1da04352dac73a9438a5f3087c8b74 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1,9 +1,3 @@ -use alacritty_terminal::{ - ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor}, - grid::Dimensions, - index::Point, - term::{cell::Flags, TermMode}, -}; use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; use gpui::{ color::Color, @@ -22,17 +16,23 @@ use itertools::Itertools; use language::CursorShape; use ordered_float::OrderedFloat; use settings::Settings; +use terminal::{ + alacritty_terminal::{ + ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor}, + grid::Dimensions, + index::Point, + term::{cell::Flags, TermMode}, + }, + mappings::colors::convert_color, + IndexedCell, Terminal, TerminalContent, TerminalSize, +}; use theme::TerminalStyle; use util::ResultExt; use std::{fmt::Debug, ops::RangeInclusive}; use std::{mem, ops::Range}; -use crate::{ - mappings::colors::convert_color, - terminal_view::{DeployContextMenu, TerminalView}, - IndexedCell, Terminal, TerminalContent, TerminalSize, -}; +use crate::{DeployContextMenu, TerminalView}; ///The information generated during layout that is nescessary for painting pub struct LayoutState { @@ -299,7 +299,7 @@ impl TerminalElement { ///Convert the Alacritty cell styles to GPUI text styles and background color fn cell_style( indexed: &IndexedCell, - fg: AnsiColor, + fg: terminal::alacritty_terminal::ansi::Color, style: &TerminalStyle, text_style: &TextStyle, font_cache: &FontCache, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..7602a3db2223651d95b03ded49936da1d273888c --- /dev/null +++ b/crates/terminal_view/src/terminal_view.rs @@ -0,0 +1,1091 @@ +mod persistence; +pub mod terminal_element; + +use std::{ + ops::RangeInclusive, + path::{Path, PathBuf}, + time::Duration, +}; + +use context_menu::{ContextMenu, ContextMenuItem}; +use dirs::home_dir; +use gpui::{ + actions, + elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack, Text}, + geometry::vector::Vector2F, + impl_actions, impl_internal_actions, + keymap::Keystroke, + AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task, + View, ViewContext, ViewHandle, WeakViewHandle, +}; +use project::{LocalWorktree, Project, ProjectPath}; +use serde::Deserialize; +use settings::{Settings, TerminalBlink, WorkingDirectory}; +use smallvec::SmallVec; +use smol::Timer; +use terminal::{ + alacritty_terminal::{ + index::Point, + term::{search::RegexSearch, TermMode}, + }, + Event, Terminal, +}; +use util::{truncate_and_trailoff, ResultExt}; +use workspace::{ + item::{Item, ItemEvent}, + notifications::NotifyResultExt, + pane, register_deserializable_item, + searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, + Pane, ToolbarItemLocation, Workspace, WorkspaceId, +}; + +use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement}; + +const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); + +///Event to transmit the scroll from the element to the view +#[derive(Clone, Debug, PartialEq)] +pub struct ScrollTerminal(pub i32); + +#[derive(Clone, PartialEq)] +pub struct DeployContextMenu { + pub position: Vector2F, +} + +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct SendText(String); + +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct SendKeystroke(String); + +actions!( + terminal, + [Clear, Copy, Paste, ShowCharacterPalette, SearchTest] +); + +impl_actions!(terminal, [SendText, SendKeystroke]); + +impl_internal_actions!(project_panel, [DeployContextMenu]); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(TerminalView::deploy); + + register_deserializable_item::(cx); + + //Useful terminal views + cx.add_action(TerminalView::send_text); + cx.add_action(TerminalView::send_keystroke); + cx.add_action(TerminalView::deploy_context_menu); + cx.add_action(TerminalView::copy); + cx.add_action(TerminalView::paste); + cx.add_action(TerminalView::clear); + cx.add_action(TerminalView::show_character_palette); +} + +///A terminal view, maintains the PTY's file handles and communicates with the terminal +pub struct TerminalView { + terminal: ModelHandle, + has_new_content: bool, + //Currently using iTerm bell, show bell emoji in tab until input is received + has_bell: bool, + context_menu: ViewHandle, + blink_state: bool, + blinking_on: bool, + blinking_paused: bool, + blink_epoch: usize, + workspace_id: WorkspaceId, +} + +impl Entity for TerminalView { + type Event = Event; +} + +impl TerminalView { + ///Create a new Terminal in the current working directory or the user's home directory + pub fn deploy( + workspace: &mut Workspace, + _: &workspace::NewTerminal, + cx: &mut ViewContext, + ) { + let strategy = cx.global::().terminal_strategy(); + + let working_directory = get_working_directory(workspace, cx, strategy); + + let window_id = cx.window_id(); + let terminal = workspace + .project() + .update(cx, |project, cx| { + project.create_terminal(working_directory, window_id, cx) + }) + .notify_err(workspace, cx); + + if let Some(terminal) = terminal { + let view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx)); + workspace.add_item(Box::new(view), cx) + } + } + + pub fn new( + terminal: ModelHandle, + workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); + cx.subscribe(&terminal, |this, _, event, cx| match event { + Event::Wakeup => { + if !cx.is_self_focused() { + this.has_new_content = true; + cx.notify(); + } + cx.emit(Event::Wakeup); + } + Event::Bell => { + this.has_bell = true; + cx.emit(Event::Wakeup); + } + Event::BlinkChanged => this.blinking_on = !this.blinking_on, + Event::TitleChanged => { + if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info { + let cwd = foreground_info.cwd.clone(); + + let item_id = cx.view_id(); + let workspace_id = this.workspace_id; + cx.background() + .spawn(async move { + TERMINAL_DB + .save_working_directory(item_id, workspace_id, cwd) + .await + .log_err(); + }) + .detach(); + } + } + _ => cx.emit(*event), + }) + .detach(); + + Self { + terminal, + has_new_content: true, + has_bell: false, + context_menu: cx.add_view(ContextMenu::new), + blink_state: true, + blinking_on: false, + blinking_paused: false, + blink_epoch: 0, + workspace_id, + } + } + + pub fn handle(&self) -> ModelHandle { + self.terminal.clone() + } + + pub fn has_new_content(&self) -> bool { + self.has_new_content + } + + pub fn has_bell(&self) -> bool { + self.has_bell + } + + pub fn clear_bel(&mut self, cx: &mut ViewContext) { + self.has_bell = false; + cx.emit(Event::Wakeup); + } + + pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext) { + let menu_entries = vec![ + ContextMenuItem::item("Clear", Clear), + ContextMenuItem::item("Close", pane::CloseActiveItem), + ]; + + self.context_menu.update(cx, |menu, cx| { + menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx) + }); + + cx.notify(); + } + + fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext) { + if !self + .terminal + .read(cx) + .last_content + .mode + .contains(TermMode::ALT_SCREEN) + { + cx.show_character_palette(); + } else { + self.terminal.update(cx, |term, cx| { + term.try_keystroke( + &Keystroke::parse("ctrl-cmd-space").unwrap(), + cx.global::() + .terminal_overrides + .option_as_meta + .unwrap_or(false), + ) + }); + } + } + + fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { + self.terminal.update(cx, |term, _| term.clear()); + cx.notify(); + } + + pub fn should_show_cursor( + &self, + focused: bool, + cx: &mut gpui::RenderContext<'_, Self>, + ) -> bool { + //Don't blink the cursor when not focused, blinking is disabled, or paused + if !focused + || !self.blinking_on + || self.blinking_paused + || self + .terminal + .read(cx) + .last_content + .mode + .contains(TermMode::ALT_SCREEN) + { + return true; + } + + let setting = { + let settings = cx.global::(); + settings + .terminal_overrides + .blinking + .clone() + .unwrap_or(TerminalBlink::TerminalControlled) + }; + + match setting { + //If the user requested to never blink, don't blink it. + TerminalBlink::Off => true, + //If the terminal is controlling it, check terminal mode + TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state, + } + } + + fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext) { + if epoch == self.blink_epoch && !self.blinking_paused { + self.blink_state = !self.blink_state; + cx.notify(); + + let epoch = self.next_blink_epoch(); + cx.spawn(|this, mut cx| { + let this = this.downgrade(); + async move { + Timer::after(CURSOR_BLINK_INTERVAL).await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx)); + } + } + }) + .detach(); + } + } + + pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext) { + self.blink_state = true; + cx.notify(); + + let epoch = self.next_blink_epoch(); + cx.spawn(|this, mut cx| { + let this = this.downgrade(); + async move { + Timer::after(CURSOR_BLINK_INTERVAL).await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx)) + } + } + }) + .detach(); + } + + pub fn find_matches( + &mut self, + query: project::search::SearchQuery, + cx: &mut ViewContext, + ) -> Task>> { + let searcher = regex_search_for_query(query); + + if let Some(searcher) = searcher { + self.terminal + .update(cx, |term, cx| term.find_matches(searcher, cx)) + } else { + cx.background().spawn(async { Vec::new() }) + } + } + + pub fn terminal(&self) -> &ModelHandle { + &self.terminal + } + + fn next_blink_epoch(&mut self) -> usize { + self.blink_epoch += 1; + self.blink_epoch + } + + fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext) { + if epoch == self.blink_epoch { + self.blinking_paused = false; + self.blink_cursors(epoch, cx); + } + } + + ///Attempt to paste the clipboard into the terminal + fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + self.terminal.update(cx, |term, _| term.copy()) + } + + ///Attempt to paste the clipboard into the terminal + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + if let Some(item) = cx.read_from_clipboard() { + self.terminal + .update(cx, |terminal, _cx| terminal.paste(item.text())); + } + } + + fn send_text(&mut self, text: &SendText, cx: &mut ViewContext) { + self.clear_bel(cx); + self.terminal.update(cx, |term, _| { + term.input(text.0.to_string()); + }); + } + + fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext) { + if let Some(keystroke) = Keystroke::parse(&text.0).log_err() { + self.clear_bel(cx); + self.terminal.update(cx, |term, cx| { + term.try_keystroke( + &keystroke, + cx.global::() + .terminal_overrides + .option_as_meta + .unwrap_or(false), + ); + }); + } + } +} + +pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option { + let searcher = match query { + project::search::SearchQuery::Text { query, .. } => RegexSearch::new(&query), + project::search::SearchQuery::Regex { query, .. } => RegexSearch::new(&query), + }; + searcher.ok() +} + +impl View for TerminalView { + fn ui_name() -> &'static str { + "Terminal" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let terminal_handle = self.terminal.clone().downgrade(); + + let self_id = cx.view_id(); + let focused = cx + .focused_view_id(cx.window_id()) + .filter(|view_id| *view_id == self_id) + .is_some(); + + Stack::new() + .with_child( + TerminalElement::new( + cx.handle(), + terminal_handle, + focused, + self.should_show_cursor(focused, cx), + ) + .contained() + .boxed(), + ) + .with_child(ChildView::new(&self.context_menu, cx).boxed()) + .boxed() + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + self.has_new_content = false; + self.terminal.read(cx).focus_in(); + self.blink_cursors(self.blink_epoch, cx); + cx.notify(); + } + + fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + self.terminal.update(cx, |terminal, _| { + terminal.focus_out(); + }); + cx.notify(); + } + + fn key_down(&mut self, event: &gpui::KeyDownEvent, cx: &mut ViewContext) -> bool { + self.clear_bel(cx); + self.pause_cursor_blinking(cx); + + self.terminal.update(cx, |term, cx| { + term.try_keystroke( + &event.keystroke, + cx.global::() + .terminal_overrides + .option_as_meta + .unwrap_or(false), + ) + }) + } + + //IME stuff + fn selected_text_range(&self, cx: &AppContext) -> Option> { + if self + .terminal + .read(cx) + .last_content + .mode + .contains(TermMode::ALT_SCREEN) + { + None + } else { + Some(0..0) + } + } + + fn replace_text_in_range( + &mut self, + _: Option>, + text: &str, + cx: &mut ViewContext, + ) { + self.terminal.update(cx, |terminal, _| { + terminal.input(text.into()); + }); + } + + fn keymap_context(&self, cx: &gpui::AppContext) -> gpui::keymap::Context { + let mut context = Self::default_keymap_context(); + + let mode = self.terminal.read(cx).last_content.mode; + context.map.insert( + "screen".to_string(), + (if mode.contains(TermMode::ALT_SCREEN) { + "alt" + } else { + "normal" + }) + .to_string(), + ); + + if mode.contains(TermMode::APP_CURSOR) { + context.set.insert("DECCKM".to_string()); + } + if mode.contains(TermMode::APP_KEYPAD) { + context.set.insert("DECPAM".to_string()); + } + //Note the ! here + if !mode.contains(TermMode::APP_KEYPAD) { + context.set.insert("DECPNM".to_string()); + } + if mode.contains(TermMode::SHOW_CURSOR) { + context.set.insert("DECTCEM".to_string()); + } + if mode.contains(TermMode::LINE_WRAP) { + context.set.insert("DECAWM".to_string()); + } + if mode.contains(TermMode::ORIGIN) { + context.set.insert("DECOM".to_string()); + } + if mode.contains(TermMode::INSERT) { + context.set.insert("IRM".to_string()); + } + //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html + if mode.contains(TermMode::LINE_FEED_NEW_LINE) { + context.set.insert("LNM".to_string()); + } + if mode.contains(TermMode::FOCUS_IN_OUT) { + context.set.insert("report_focus".to_string()); + } + if mode.contains(TermMode::ALTERNATE_SCROLL) { + context.set.insert("alternate_scroll".to_string()); + } + if mode.contains(TermMode::BRACKETED_PASTE) { + context.set.insert("bracketed_paste".to_string()); + } + if mode.intersects(TermMode::MOUSE_MODE) { + context.set.insert("any_mouse_reporting".to_string()); + } + { + let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) { + "click" + } else if mode.contains(TermMode::MOUSE_DRAG) { + "drag" + } else if mode.contains(TermMode::MOUSE_MOTION) { + "motion" + } else { + "off" + }; + context + .map + .insert("mouse_reporting".to_string(), mouse_reporting.to_string()); + } + { + let format = if mode.contains(TermMode::SGR_MOUSE) { + "sgr" + } else if mode.contains(TermMode::UTF8_MOUSE) { + "utf8" + } else { + "normal" + }; + context + .map + .insert("mouse_format".to_string(), format.to_string()); + } + context + } +} + +impl Item for TerminalView { + fn tab_content( + &self, + _detail: Option, + tab_theme: &theme::Tab, + cx: &gpui::AppContext, + ) -> ElementBox { + let title = self + .terminal() + .read(cx) + .foreground_process_info + .as_ref() + .map(|fpi| { + format!( + "{} — {}", + 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()); + + Flex::row() + .with_child( + Label::new(title, tab_theme.label.clone()) + .aligned() + .contained() + .boxed(), + ) + .boxed() + } + + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + _cx: &mut ViewContext, + ) -> Option { + //From what I can tell, there's no way to tell the current working + //Directory of the terminal from outside the shell. There might be + //solutions to this, but they are non-trivial and require more IPC + + // Some(TerminalContainer::new( + // Err(anyhow::anyhow!("failed to instantiate terminal")), + // workspace_id, + // cx, + // )) + + // TODO + None + } + + fn project_path(&self, _cx: &gpui::AppContext) -> Option { + None + } + + fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { + SmallVec::new() + } + + fn is_singleton(&self, _cx: &gpui::AppContext) -> bool { + false + } + + fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} + + fn can_save(&self, _cx: &gpui::AppContext) -> bool { + false + } + + fn save( + &mut self, + _project: gpui::ModelHandle, + _cx: &mut ViewContext, + ) -> gpui::Task> { + unreachable!("save should not have been called"); + } + + fn save_as( + &mut self, + _project: gpui::ModelHandle, + _abs_path: std::path::PathBuf, + _cx: &mut ViewContext, + ) -> gpui::Task> { + unreachable!("save_as should not have been called"); + } + + fn reload( + &mut self, + _project: gpui::ModelHandle, + _cx: &mut ViewContext, + ) -> gpui::Task> { + gpui::Task::ready(Ok(())) + } + + fn is_dirty(&self, _cx: &gpui::AppContext) -> bool { + self.has_bell() + } + + fn has_conflict(&self, _cx: &AppContext) -> bool { + false + } + + fn as_searchable(&self, handle: &ViewHandle) -> Option> { + Some(Box::new(handle.clone())) + } + + fn to_item_events(event: &Self::Event) -> Vec { + match event { + Event::BreadcrumbsChanged => vec![ItemEvent::UpdateBreadcrumbs], + Event::TitleChanged | Event::Wakeup => vec![ItemEvent::UpdateTab], + Event::CloseTerminal => vec![ItemEvent::CloseItem], + _ => vec![], + } + } + + fn breadcrumb_location(&self) -> ToolbarItemLocation { + ToolbarItemLocation::PrimaryLeft { flex: None } + } + + fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { + Some(vec![Text::new( + self.terminal().read(cx).breadcrumb_text.to_string(), + theme.breadcrumbs.text.clone(), + ) + .boxed()]) + } + + fn serialized_item_kind() -> Option<&'static str> { + Some("Terminal") + } + + fn deserialize( + project: ModelHandle, + _workspace: WeakViewHandle, + workspace_id: workspace::WorkspaceId, + item_id: workspace::ItemId, + cx: &mut ViewContext, + ) -> Task>> { + let window_id = cx.window_id(); + cx.spawn(|pane, mut cx| async move { + let cwd = TERMINAL_DB + .take_working_directory(item_id, workspace_id) + .await + .log_err() + .flatten(); + + cx.update(|cx| { + let terminal = project.update(cx, |project, cx| { + project.create_terminal(cwd, window_id, cx) + })?; + + Ok(cx.add_view(pane, |cx| TerminalView::new(terminal, workspace_id, cx))) + }) + }) + } + + fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { + cx.background() + .spawn(TERMINAL_DB.update_workspace_id( + workspace.database_id(), + self.workspace_id, + cx.view_id(), + )) + .detach(); + self.workspace_id = workspace.database_id(); + } +} + +impl SearchableItem for TerminalView { + type Match = RangeInclusive; + + fn supported_options() -> SearchOptions { + SearchOptions { + case: false, + word: false, + regex: false, + } + } + + /// Convert events raised by this item into search-relevant events (if applicable) + fn to_search_event(event: &Self::Event) -> Option { + match event { + Event::Wakeup => Some(SearchEvent::MatchesInvalidated), + Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged), + _ => None, + } + } + + /// Clear stored matches + fn clear_matches(&mut self, cx: &mut ViewContext) { + self.terminal().update(cx, |term, _| term.matches.clear()) + } + + /// Store matches returned from find_matches somewhere for rendering + fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext) { + self.terminal().update(cx, |term, _| term.matches = matches) + } + + /// Return the selection content to pre-load into this search + fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { + self.terminal() + .read(cx) + .last_content + .selection_text + .clone() + .unwrap_or_default() + } + + /// Focus match at given index into the Vec of matches + fn activate_match(&mut self, index: usize, _: Vec, cx: &mut ViewContext) { + self.terminal() + .update(cx, |term, _| term.activate_match(index)); + cx.notify(); + } + + /// Get all of the matches for this query, should be done on the background + fn find_matches( + &mut self, + query: project::search::SearchQuery, + cx: &mut ViewContext, + ) -> Task> { + if let Some(searcher) = regex_search_for_query(query) { + self.terminal() + .update(cx, |term, cx| term.find_matches(searcher, cx)) + } else { + Task::ready(vec![]) + } + } + + /// Reports back to the search toolbar what the active match should be (the selection) + fn active_match_index( + &mut self, + matches: Vec, + cx: &mut ViewContext, + ) -> Option { + // 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 { + if let Some(selection_head) = self.terminal().read(cx).selection_head { + // If selection head is contained in a match. Return that match + 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)) + } + } else { + // Matches found but no active selection, return the first last one (closest to cursor) + Some(matches.len().saturating_sub(1)) + } + } else { + None + }; + + res + } +} + +///Get's the working directory for the given workspace, respecting the user's settings. +pub fn get_working_directory( + workspace: &Workspace, + cx: &AppContext, + strategy: WorkingDirectory, +) -> Option { + let res = match strategy { + WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx) + .or_else(|| first_project_directory(workspace, cx)), + WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), + WorkingDirectory::AlwaysHome => None, + WorkingDirectory::Always { directory } => { + shellexpand::full(&directory) //TODO handle this better + .ok() + .map(|dir| Path::new(&dir.to_string()).to_path_buf()) + .filter(|dir| dir.is_dir()) + } + }; + res.or_else(home_dir) +} + +///Get's the first project's home directory, or the home directory +fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { + workspace + .worktrees(cx) + .next() + .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) + .and_then(get_path_from_wt) +} + +///Gets the intuitively correct working directory from the given workspace +///If there is an active entry for this project, returns that entry's worktree root. +///If there's no active entry but there is a worktree, returns that worktrees root. +///If either of these roots are files, or if there are any other query failures, +/// returns the user's home directory +fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { + let project = workspace.project().read(cx); + + project + .active_entry() + .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) + .or_else(|| workspace.worktrees(cx).next()) + .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) + .and_then(get_path_from_wt) +} + +fn get_path_from_wt(wt: &LocalWorktree) -> Option { + wt.root_entry() + .filter(|re| re.is_dir()) + .map(|_| wt.abs_path().to_path_buf()) +} + +#[cfg(test)] +mod tests { + + use super::*; + use gpui::TestAppContext; + use project::{Entry, Project, ProjectPath, Worktree}; + use workspace::AppState; + + use std::path::Path; + + ///Working directory calculation tests + + ///No Worktrees in project -> home_dir() + #[gpui::test] + async fn no_worktree(cx: &mut TestAppContext) { + //Setup variables + let (project, workspace) = blank_workspace(cx).await; + //Test + cx.read(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + //Make sure enviroment is as expeted + assert!(active_entry.is_none()); + assert!(workspace.worktrees(cx).next().is_none()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, None); + let res = first_project_directory(workspace, cx); + assert_eq!(res, None); + }); + } + + ///No active entry, but a worktree, worktree is a file -> home_dir() + #[gpui::test] + async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) { + //Setup variables + + let (project, workspace) = blank_workspace(cx).await; + create_file_wt(project.clone(), "/root.txt", cx).await; + + cx.read(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + //Make sure enviroment is as expeted + assert!(active_entry.is_none()); + assert!(workspace.worktrees(cx).next().is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, None); + let res = first_project_directory(workspace, cx); + assert_eq!(res, None); + }); + } + + //No active entry, but a worktree, worktree is a folder -> worktree_folder + #[gpui::test] + async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) { + //Setup variables + let (project, workspace) = blank_workspace(cx).await; + let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await; + + //Test + cx.update(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + assert!(active_entry.is_none()); + assert!(workspace.worktrees(cx).next().is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); + let res = first_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); + }); + } + + //Active entry with a work tree, worktree is a file -> home_dir() + #[gpui::test] + async fn active_entry_worktree_is_file(cx: &mut TestAppContext) { + //Setup variables + + let (project, workspace) = blank_workspace(cx).await; + let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await; + let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await; + insert_active_entry_for(wt2, entry2, project.clone(), cx); + + //Test + cx.update(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + assert!(active_entry.is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, None); + let res = first_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); + }); + } + + //Active entry, with a worktree, worktree is a folder -> worktree_folder + #[gpui::test] + async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) { + //Setup variables + let (project, workspace) = blank_workspace(cx).await; + let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await; + let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await; + insert_active_entry_for(wt2, entry2, project.clone(), cx); + + //Test + cx.update(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + assert!(active_entry.is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root2/")).to_path_buf())); + let res = first_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); + }); + } + + ///Creates a worktree with 1 file: /root.txt + pub async fn blank_workspace( + cx: &mut TestAppContext, + ) -> (ModelHandle, ViewHandle) { + let params = cx.update(AppState::test); + + let project = Project::test(params.fs.clone(), [], cx).await; + let (_, workspace) = cx.add_window(|cx| { + Workspace::new( + Default::default(), + 0, + project.clone(), + |_, _| unimplemented!(), + cx, + ) + }); + + (project, workspace) + } + + ///Creates a worktree with 1 folder: /root{suffix}/ + async fn create_folder_wt( + project: ModelHandle, + path: impl AsRef, + cx: &mut TestAppContext, + ) -> (ModelHandle, Entry) { + create_wt(project, true, path, cx).await + } + + ///Creates a worktree with 1 file: /root{suffix}.txt + async fn create_file_wt( + project: ModelHandle, + path: impl AsRef, + cx: &mut TestAppContext, + ) -> (ModelHandle, Entry) { + create_wt(project, false, path, cx).await + } + + async fn create_wt( + project: ModelHandle, + is_dir: bool, + path: impl AsRef, + cx: &mut TestAppContext, + ) -> (ModelHandle, Entry) { + let (wt, _) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree(path, true, cx) + }) + .await + .unwrap(); + + let entry = cx + .update(|cx| { + wt.update(cx, |wt, cx| { + wt.as_local() + .unwrap() + .create_entry(Path::new(""), is_dir, cx) + }) + }) + .await + .unwrap(); + + (wt, entry) + } + + pub fn insert_active_entry_for( + wt: ModelHandle, + entry: Entry, + project: ModelHandle, + cx: &mut TestAppContext, + ) { + cx.update(|cx| { + let p = ProjectPath { + worktree_id: wt.read(cx).id(), + path: entry.path, + }; + project.update(cx, |project, cx| project.set_active_path(Some(p), cx)); + }); + } +} diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 0879166bbe733faf5b9ee0e86695cf3bfe391e39..78ee56f18882e336e5584af7baaf683dacbe7448 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -126,18 +126,21 @@ impl DockPosition { } } -pub type DefaultItemFactory = - fn(&mut Workspace, &mut ViewContext) -> Box; +pub type DockDefaultItemFactory = + fn(workspace: &mut Workspace, cx: &mut ViewContext) -> Option>; pub struct Dock { position: DockPosition, panel_sizes: HashMap, pane: ViewHandle, - default_item_factory: DefaultItemFactory, + default_item_factory: DockDefaultItemFactory, } impl Dock { - pub fn new(default_item_factory: DefaultItemFactory, cx: &mut ViewContext) -> Self { + pub fn new( + default_item_factory: DockDefaultItemFactory, + cx: &mut ViewContext, + ) -> Self { let position = DockPosition::Hidden(cx.global::().default_dock_anchor); let pane = cx.add_view(|cx| Pane::new(Some(position.anchor()), cx)); @@ -192,9 +195,11 @@ impl Dock { // Ensure that the pane has at least one item or construct a default item to put in it let pane = workspace.dock.pane.clone(); if pane.read(cx).items().next().is_none() { - let item_to_add = (workspace.dock.default_item_factory)(workspace, cx); - // Adding the item focuses the pane by default - Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx); + if let Some(item_to_add) = (workspace.dock.default_item_factory)(workspace, cx) { + Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx); + } else { + workspace.dock.position = workspace.dock.position.hide(); + } } else { cx.focus(pane); } @@ -465,8 +470,8 @@ mod tests { pub fn default_item_factory( _workspace: &mut Workspace, cx: &mut ViewContext, - ) -> Box { - Box::new(cx.add_view(|_| TestItem::new())) + ) -> Option> { + Some(Box::new(cx.add_view(|_| TestItem::new()))) } #[gpui::test] diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 91656727d0efc6f161b24490f29eb99c326a8d94..0e76d45518d71d13039f1f378418bfedec7a9f50 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -161,8 +161,8 @@ pub mod simple_message_notification { pub struct MessageNotification { message: String, - click_action: Box, - click_message: String, + click_action: Option>, + click_message: Option, } pub enum MessageNotificationEvent { @@ -174,6 +174,14 @@ pub mod simple_message_notification { } impl MessageNotification { + pub fn new_messsage>(message: S) -> MessageNotification { + Self { + message: message.as_ref().to_string(), + click_action: None, + click_message: None, + } + } + pub fn new, A: Action, S2: AsRef>( message: S1, click_action: A, @@ -181,8 +189,8 @@ pub mod simple_message_notification { ) -> Self { Self { message: message.as_ref().to_string(), - click_action: Box::new(click_action) as Box, - click_message: click_message.as_ref().to_string(), + click_action: Some(Box::new(click_action) as Box), + click_message: Some(click_message.as_ref().to_string()), } } @@ -202,8 +210,11 @@ pub mod simple_message_notification { enum MessageNotificationTag {} - let click_action = self.click_action.boxed_clone(); - let click_message = self.click_message.clone(); + let click_action = self + .click_action + .as_ref() + .map(|action| action.boxed_clone()); + let click_message = self.click_message.as_ref().map(|message| message.clone()); let message = self.message.clone(); MouseEventHandler::::new(0, cx, |state, cx| { @@ -251,20 +262,28 @@ pub mod simple_message_notification { ) .boxed(), ) - .with_child({ + .with_children({ let style = theme.action_message.style_for(state, false); - - Text::new(click_message, style.text.clone()) - .contained() - .with_style(style.container) - .boxed() + if let Some(click_message) = click_message { + Some( + Text::new(click_message, style.text.clone()) + .contained() + .with_style(style.container) + .boxed(), + ) + } else { + None + } + .into_iter() }) .contained() .boxed() }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_any_action(click_action.boxed_clone()) + if let Some(click_action) = click_action.as_ref() { + cx.dispatch_any_action(click_action.boxed_clone()) + } }) .boxed() } @@ -278,3 +297,38 @@ pub mod simple_message_notification { } } } + +pub trait NotifyResultExt { + type Ok; + + fn notify_err( + self, + workspace: &mut Workspace, + cx: &mut ViewContext, + ) -> Option; +} + +impl NotifyResultExt for Result +where + E: std::fmt::Debug, +{ + type Ok = T; + + fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext) -> Option { + match self { + Ok(value) => Some(value), + Err(err) => { + workspace.show_notification(0, cx, |cx| { + cx.add_view(|_cx| { + simple_message_notification::MessageNotification::new_messsage(format!( + "Error: {:?}", + err, + )) + }) + }); + + None + } + } + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a0c353b3f808bf1f1a5c9a9909f2047139916449..d38cf96ed29ae312475a303bbb151bb155733466 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -27,7 +27,7 @@ use anyhow::{anyhow, Context, Result}; use call::ActiveCall; use client::{proto, Client, PeerId, TypedEnvelope, UserStore}; use collections::{hash_map, HashMap, HashSet}; -use dock::{DefaultItemFactory, Dock, ToggleDockButton}; +use dock::{Dock, DockDefaultItemFactory, ToggleDockButton}; use drag_and_drop::DragAndDrop; use fs::{self, Fs}; use futures::{channel::oneshot, FutureExt, StreamExt}; @@ -375,7 +375,7 @@ pub struct AppState { pub fs: Arc, pub build_window_options: fn() -> WindowOptions<'static>, pub initialize_workspace: fn(&mut Workspace, &Arc, &mut ViewContext), - pub default_item_factory: DefaultItemFactory, + pub dock_default_item_factory: DockDefaultItemFactory, } impl AppState { @@ -401,7 +401,7 @@ impl AppState { user_store, initialize_workspace: |_, _, _| {}, build_window_options: Default::default, - default_item_factory: |_, _| unimplemented!(), + dock_default_item_factory: |_, _| unimplemented!(), }) } } @@ -515,7 +515,7 @@ impl Workspace { serialized_workspace: Option, workspace_id: WorkspaceId, project: ModelHandle, - dock_default_factory: DefaultItemFactory, + dock_default_factory: DockDefaultItemFactory, cx: &mut ViewContext, ) -> Self { cx.observe_fullscreen(|_, _, cx| cx.notify()).detach(); @@ -703,7 +703,7 @@ impl Workspace { serialized_workspace, workspace_id, project_handle, - app_state.default_item_factory, + app_state.dock_default_item_factory, cx, ); (app_state.initialize_workspace)(&mut workspace, &app_state, cx); @@ -2694,7 +2694,7 @@ mod tests { pub fn default_item_factory( _workspace: &mut Workspace, _cx: &mut ViewContext, - ) -> Box { + ) -> Option> { unimplemented!(); } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6dfb739f3abc07322a180920e4aff6abc35161c6..a07c0c899c9f5aea692334c0d1f0e83dbba317db 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -48,7 +48,7 @@ rpc = { path = "../rpc" } settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } text = { path = "../text" } -terminal = { path = "../terminal" } +terminal_view = { path = "../terminal_view" } theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } theme_testbench = { path = "../theme_testbench" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 4163841d455d3e4e30620a28b80a0e5fdba16ca9..09a20b566002158d6f9f4e3471fb9e39c8852a73 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -32,13 +32,15 @@ use settings::{ use smol::process::Command; use std::fs::OpenOptions; use std::{env, ffi::OsStr, panic, path::PathBuf, sync::Arc, thread, time::Duration}; -use terminal::terminal_container_view::{get_working_directory, TerminalContainer}; +use terminal_view::{get_working_directory, TerminalView}; use fs::RealFs; use settings::watched_json::{watch_keymap_file, watch_settings_file, WatchedJsonFile}; use theme::ThemeRegistry; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; -use workspace::{self, item::ItemHandle, AppState, NewFile, OpenPaths, Workspace}; +use workspace::{ + self, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile, OpenPaths, Workspace, +}; use zed::{self, build_window_options, initialize_workspace, languages, menus}; fn main() { @@ -119,7 +121,7 @@ fn main() { diagnostics::init(cx); search::init(cx); vim::init(cx); - terminal::init(cx); + terminal_view::init(cx); theme_testbench::init(cx); cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx)) @@ -150,7 +152,7 @@ fn main() { fs, build_window_options, initialize_workspace, - default_item_factory, + dock_default_item_factory, }); auto_update::init(http, client::ZED_SERVER_URL.clone(), cx); @@ -581,10 +583,10 @@ async fn handle_cli_connection( } } -pub fn default_item_factory( +pub fn dock_default_item_factory( workspace: &mut Workspace, cx: &mut ViewContext, -) -> Box { +) -> Option> { let strategy = cx .global::() .terminal_overrides @@ -594,8 +596,15 @@ pub fn default_item_factory( let working_directory = get_working_directory(workspace, cx, strategy); - let terminal_handle = cx.add_view(|cx| { - TerminalContainer::new(working_directory, false, workspace.database_id(), cx) - }); - Box::new(terminal_handle) + let window_id = cx.window_id(); + let terminal = workspace + .project() + .update(cx, |project, cx| { + project.create_terminal(working_directory, window_id, cx) + }) + .notify_err(workspace, cx)?; + + let terminal_view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx)); + + Some(Box::new(terminal_view)) }