Merge pull request #1940 from zed-industries/terminal-collab-kickoff

Mikayla Maki created

WIP - move terminal to project as pre-prep for collaboration

Change summary

Cargo.lock                                         |   30 
crates/collab/src/integration_tests.rs             |    2 
crates/collab_ui/src/collab_ui.rs                  |    2 
crates/db/src/query.rs                             |    4 
crates/editor/src/editor.rs                        |    2 
crates/project/Cargo.toml                          |    1 
crates/project/src/project.rs                      |   29 
crates/settings/src/settings.rs                    |   34 
crates/terminal/Cargo.toml                         |   14 
crates/terminal/src/terminal.rs                    |  192 +-
crates/terminal/src/terminal_container_view.rs     |  711 ----------
crates/terminal/src/terminal_view.rs               |  471 ------
crates/terminal/src/tests/terminal_test_context.rs |  143 --
crates/terminal_view/Cargo.toml                    |   44 
crates/terminal_view/README.md                     |    0 
crates/terminal_view/scripts/print256color.sh      |    0 
crates/terminal_view/scripts/truecolor.sh          |    0 
crates/terminal_view/src/persistence.rs            |   11 
crates/terminal_view/src/terminal_element.rs       |   24 
crates/terminal_view/src/terminal_view.rs          | 1091 ++++++++++++++++
crates/workspace/src/dock.rs                       |   23 
crates/workspace/src/notifications.rs              |   80 
crates/workspace/src/workspace.rs                  |   12 
crates/zed/Cargo.toml                              |    2 
crates/zed/src/main.rs                             |   29 
25 files changed, 1,438 insertions(+), 1,513 deletions(-)

Detailed changes

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",

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);

crates/collab_ui/src/collab_ui.rs 🔗

@@ -54,7 +54,7 @@ pub fn init(app_state: Arc<AppState>, 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);

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),

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::<usize>(transaction)
                                 .all(|range| {
                                     excerpt_range.start <= range.start
                                         && excerpt_range.end >= range.end

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"

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<PathBuf>,
+        window_id: usize,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<ModelHandle<Terminal>> {
+        if self.is_remote() {
+            return Err(anyhow!(
+                "creating terminals as a guest is not supported yet"
+            ));
+        } else {
+            let settings = cx.global::<Settings>();
+            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,

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<F, R: Default + Clone>(&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<String, String> {
+        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 {

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"

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<PathBuf>,
-    pub shell: Option<Shell>,
+    pub shell: Shell,
     pub source: std::io::Error,
 }
 
@@ -238,24 +226,20 @@ impl TerminalError {
             })
     }
 
-    pub fn shell_to_string(&self) -> Option<String> {
-        self.shell.as_ref().map(|shell| match shell {
+    pub fn shell_to_string(&self) -> String {
+        match &self.shell {
             Shell::System => "<system shell>".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 => "<system defined shell>".to_string(),
-
-                Shell::Program(s) => s,
-                Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
-            })
-            .unwrap_or_else(|| "<none specified, using system defined shell>".to_string())
+        match &self.shell {
+            Shell::System => "<system defined shell>".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<PathBuf>,
-        shell: Option<Shell>,
-        env: Option<HashMap<String, String>>,
+        shell: Shell,
+        mut env: HashMap<String, String>,
         blink_settings: Option<TerminalBlink>,
-        alternate_scroll: &AlternateScroll,
+        alternate_scroll: AlternateScroll,
         window_id: usize,
-        item_id: ItemId,
-        workspace_id: WorkspaceId,
     ) -> Result<TerminalBuilder> {
         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<IndexedCell>,
-    mode: TermMode,
-    display_offset: usize,
-    selection_text: Option<String>,
-    selection: Option<SelectionRange>,
-    cursor: RenderableCursor,
-    cursor_char: char,
-    size: TerminalSize,
-    last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
+    pub cells: Vec<IndexedCell>,
+    pub mode: TermMode,
+    pub display_offset: usize,
+    pub selection_text: Option<String>,
+    pub selection: Option<SelectionRange>,
+    pub cursor: RenderableCursor,
+    pub cursor_char: char,
+    pub size: TerminalSize,
+    pub last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
 }
 
 impl Default for TerminalContent {
@@ -525,19 +505,17 @@ pub struct Terminal {
     /// This is only used for terminal hyperlink checking
     last_mouse_position: Option<Vector2F>,
     pub matches: Vec<RangeInclusive<Point>>,
-    last_content: TerminalContent,
+    pub last_content: TerminalContent,
     last_synced: Instant,
     sync_task: Option<Task<()>>,
-    selection_head: Option<Point>,
-    breadcrumb_text: String,
+    pub selection_head: Option<Point>,
+    pub breadcrumb_text: String,
     shell_pid: u32,
     shell_fd: u32,
-    foreground_process_info: Option<LocalProcessInfo>,
+    pub foreground_process_info: Option<LocalProcessInfo>,
     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<Self>,
     ) -> Task<Vec<RangeInclusive<Point>>> {
         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<Vec<char>>) {
+        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,
+        )
+    }
 }

crates/terminal/src/terminal_container_view.rs 🔗

@@ -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::<TerminalContainer>(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<TerminalView>),
-    Error(ViewHandle<ErrorView>),
-}
-
-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<PathBuf>,
-}
-
-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<Workspace>,
-    ) {
-        let strategy = cx
-            .global::<Settings>()
-            .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<PathBuf>,
-        modal: bool,
-        workspace_id: WorkspaceId,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        let settings = cx.global::<Settings>();
-        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::<TerminalError>().unwrap(),
-                });
-                TerminalContainerContent::Error(view)
-            }
-        };
-
-        TerminalContainer {
-            content,
-            associated_directory: working_directory,
-        }
-    }
-
-    fn connected(&self) -> Option<ViewHandle<TerminalView>> {
-        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<Self>) {
-        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::<Settings>();
-        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<usize>,
-        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<Self>,
-    ) -> Option<Self> {
-        //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<ProjectPath> {
-        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<Self>) {}
-
-    fn can_save(&self, _cx: &gpui::AppContext) -> bool {
-        false
-    }
-
-    fn save(
-        &mut self,
-        _project: gpui::ModelHandle<Project>,
-        _cx: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        unreachable!("save should not have been called");
-    }
-
-    fn save_as(
-        &mut self,
-        _project: gpui::ModelHandle<Project>,
-        _abs_path: std::path::PathBuf,
-        _cx: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        unreachable!("save_as should not have been called");
-    }
-
-    fn reload(
-        &mut self,
-        _project: gpui::ModelHandle<Project>,
-        _cx: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        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<Self>) -> Option<Box<dyn SearchableItemHandle>> {
-        Some(Box::new(handle.clone()))
-    }
-
-    fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
-        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<Vec<ElementBox>> {
-        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<Project>,
-        _workspace: WeakViewHandle<Workspace>,
-        workspace_id: workspace::WorkspaceId,
-        item_id: workspace::ItemId,
-        cx: &mut ViewContext<Pane>,
-    ) -> Task<anyhow::Result<ViewHandle<Self>>> {
-        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<Self>) {
-        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<Point>;
-
-    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<SearchEvent> {
-        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>) {
-        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<Self::Match>, cx: &mut ViewContext<Self>) {
-        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<Self>) -> 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<Self::Match>, cx: &mut ViewContext<Self>) {
-        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<Self>,
-    ) -> Task<Vec<Self::Match>> {
-        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<Self::Match>,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<usize> {
-        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<PathBuf> {
-    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<PathBuf> {
-    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<PathBuf> {
-    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<PathBuf> {
-    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()));
-        });
-    }
-}

crates/terminal/src/terminal_view.rs 🔗

@@ -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<Terminal>,
-    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<ContextMenu>,
-    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<Terminal>,
-        modal: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> 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<Terminal> {
-        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<TerminalView>) {
-        self.has_bell = false;
-        cx.emit(Event::Wakeup);
-    }
-
-    pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
-        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<Self>) {
-        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::<Settings>()
-                        .terminal_overrides
-                        .option_as_meta
-                        .unwrap_or(false),
-                )
-            });
-        }
-    }
-
-    fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
-        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>();
-            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<Self>) {
-        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>) {
-        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<Self>,
-    ) -> Task<Vec<RangeInclusive<Point>>> {
-        self.terminal
-            .update(cx, |term, cx| term.find_matches(query, cx))
-    }
-
-    pub fn terminal(&self) -> &ModelHandle<Terminal> {
-        &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<Self>) {
-        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>) {
-        self.terminal.update(cx, |term, _| term.copy())
-    }
-
-    ///Attempt to paste the clipboard into the terminal
-    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
-        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>) {
-        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<Self>) {
-        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::<Settings>()
-                        .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>) {
-        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>) {
-        self.terminal.update(cx, |terminal, _| {
-            terminal.focus_out();
-        });
-        cx.notify();
-    }
-
-    fn key_down(&mut self, event: &gpui::KeyDownEvent, cx: &mut ViewContext<Self>) -> bool {
-        self.clear_bel(cx);
-        self.pause_cursor_blinking(cx);
-
-        self.terminal.update(cx, |term, cx| {
-            term.try_keystroke(
-                &event.keystroke,
-                cx.global::<Settings>()
-                    .terminal_overrides
-                    .option_as_meta
-                    .unwrap_or(false),
-            )
-        })
-    }
-
-    //IME stuff
-    fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
-        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<std::ops::Range<usize>>,
-        text: &str,
-        cx: &mut ViewContext<Self>,
-    ) {
-        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
-    }
-}

crates/terminal/src/tests/terminal_test_context.rs 🔗

@@ -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<Project>, ViewHandle<Workspace>) {
-        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<Project>,
-        path: impl AsRef<Path>,
-    ) -> (ModelHandle<Worktree>, 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<Project>,
-        path: impl AsRef<Path>,
-    ) -> (ModelHandle<Worktree>, Entry) {
-        self.create_wt(project, false, path).await
-    }
-
-    async fn create_wt(
-        &mut self,
-        project: ModelHandle<Project>,
-        is_dir: bool,
-        path: impl AsRef<Path>,
-    ) -> (ModelHandle<Worktree>, 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<Worktree>,
-        entry: Entry,
-        project: ModelHandle<Project>,
-    ) {
-        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<Vec<char>>) {
-        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);
-    }
-}

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"

crates/terminal/src/persistence.rs → 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<WorkspaceDb> =
+    pub static ref TERMINAL_DB: TerminalDb<WorkspaceDb> =
         &[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<Option<PathBuf>> {
-            SELECT working_directory
-            FROM terminals
+        pub async fn take_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
+            DELETE FROM terminals
             WHERE item_id = ? AND workspace_id = ?
+            RETURNING working_directory
         }
     }
 }

crates/terminal/src/terminal_element.rs → 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,

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::<TerminalView>(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<Terminal>,
+    has_new_content: bool,
+    //Currently using iTerm bell, show bell emoji in tab until input is received
+    has_bell: bool,
+    context_menu: ViewHandle<ContextMenu>,
+    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<Workspace>,
+    ) {
+        let strategy = cx.global::<Settings>().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<Terminal>,
+        workspace_id: WorkspaceId,
+        cx: &mut ViewContext<Self>,
+    ) -> 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<Terminal> {
+        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<TerminalView>) {
+        self.has_bell = false;
+        cx.emit(Event::Wakeup);
+    }
+
+    pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
+        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<Self>) {
+        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::<Settings>()
+                        .terminal_overrides
+                        .option_as_meta
+                        .unwrap_or(false),
+                )
+            });
+        }
+    }
+
+    fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
+        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>();
+            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<Self>) {
+        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>) {
+        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<Self>,
+    ) -> Task<Vec<RangeInclusive<Point>>> {
+        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<Terminal> {
+        &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<Self>) {
+        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>) {
+        self.terminal.update(cx, |term, _| term.copy())
+    }
+
+    ///Attempt to paste the clipboard into the terminal
+    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+        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>) {
+        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<Self>) {
+        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::<Settings>()
+                        .terminal_overrides
+                        .option_as_meta
+                        .unwrap_or(false),
+                );
+            });
+        }
+    }
+}
+
+pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option<RegexSearch> {
+    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>) {
+        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>) {
+        self.terminal.update(cx, |terminal, _| {
+            terminal.focus_out();
+        });
+        cx.notify();
+    }
+
+    fn key_down(&mut self, event: &gpui::KeyDownEvent, cx: &mut ViewContext<Self>) -> bool {
+        self.clear_bel(cx);
+        self.pause_cursor_blinking(cx);
+
+        self.terminal.update(cx, |term, cx| {
+            term.try_keystroke(
+                &event.keystroke,
+                cx.global::<Settings>()
+                    .terminal_overrides
+                    .option_as_meta
+                    .unwrap_or(false),
+            )
+        })
+    }
+
+    //IME stuff
+    fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
+        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<std::ops::Range<usize>>,
+        text: &str,
+        cx: &mut ViewContext<Self>,
+    ) {
+        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<usize>,
+        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<Self>,
+    ) -> Option<Self> {
+        //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<ProjectPath> {
+        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<Self>) {}
+
+    fn can_save(&self, _cx: &gpui::AppContext) -> bool {
+        false
+    }
+
+    fn save(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        unreachable!("save should not have been called");
+    }
+
+    fn save_as(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _abs_path: std::path::PathBuf,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        unreachable!("save_as should not have been called");
+    }
+
+    fn reload(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        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<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+        Some(Box::new(handle.clone()))
+    }
+
+    fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
+        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<Vec<ElementBox>> {
+        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<Project>,
+        _workspace: WeakViewHandle<Workspace>,
+        workspace_id: workspace::WorkspaceId,
+        item_id: workspace::ItemId,
+        cx: &mut ViewContext<Pane>,
+    ) -> Task<anyhow::Result<ViewHandle<Self>>> {
+        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<Self>) {
+        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<Point>;
+
+    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<SearchEvent> {
+        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>) {
+        self.terminal().update(cx, |term, _| term.matches.clear())
+    }
+
+    /// Store matches returned from find_matches somewhere for rendering
+    fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+        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<Self>) -> 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<Self::Match>, cx: &mut ViewContext<Self>) {
+        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<Self>,
+    ) -> Task<Vec<Self::Match>> {
+        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<Self::Match>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<usize> {
+        // 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<PathBuf> {
+    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<PathBuf> {
+    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<PathBuf> {
+    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<PathBuf> {
+    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<Project>, ViewHandle<Workspace>) {
+        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<Project>,
+        path: impl AsRef<Path>,
+        cx: &mut TestAppContext,
+    ) -> (ModelHandle<Worktree>, Entry) {
+        create_wt(project, true, path, cx).await
+    }
+
+    ///Creates a worktree with 1 file: /root{suffix}.txt
+    async fn create_file_wt(
+        project: ModelHandle<Project>,
+        path: impl AsRef<Path>,
+        cx: &mut TestAppContext,
+    ) -> (ModelHandle<Worktree>, Entry) {
+        create_wt(project, false, path, cx).await
+    }
+
+    async fn create_wt(
+        project: ModelHandle<Project>,
+        is_dir: bool,
+        path: impl AsRef<Path>,
+        cx: &mut TestAppContext,
+    ) -> (ModelHandle<Worktree>, 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<Worktree>,
+        entry: Entry,
+        project: ModelHandle<Project>,
+        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));
+        });
+    }
+}

crates/workspace/src/dock.rs 🔗

@@ -126,18 +126,21 @@ impl DockPosition {
     }
 }
 
-pub type DefaultItemFactory =
-    fn(&mut Workspace, &mut ViewContext<Workspace>) -> Box<dyn ItemHandle>;
+pub type DockDefaultItemFactory =
+    fn(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<Box<dyn ItemHandle>>;
 
 pub struct Dock {
     position: DockPosition,
     panel_sizes: HashMap<DockAnchor, f32>,
     pane: ViewHandle<Pane>,
-    default_item_factory: DefaultItemFactory,
+    default_item_factory: DockDefaultItemFactory,
 }
 
 impl Dock {
-    pub fn new(default_item_factory: DefaultItemFactory, cx: &mut ViewContext<Workspace>) -> Self {
+    pub fn new(
+        default_item_factory: DockDefaultItemFactory,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Self {
         let position = DockPosition::Hidden(cx.global::<Settings>().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<Workspace>,
-    ) -> Box<dyn ItemHandle> {
-        Box::new(cx.add_view(|_| TestItem::new()))
+    ) -> Option<Box<dyn ItemHandle>> {
+        Some(Box::new(cx.add_view(|_| TestItem::new())))
     }
 
     #[gpui::test]

crates/workspace/src/notifications.rs 🔗

@@ -161,8 +161,8 @@ pub mod simple_message_notification {
 
     pub struct MessageNotification {
         message: String,
-        click_action: Box<dyn Action>,
-        click_message: String,
+        click_action: Option<Box<dyn Action>>,
+        click_message: Option<String>,
     }
 
     pub enum MessageNotificationEvent {
@@ -174,6 +174,14 @@ pub mod simple_message_notification {
     }
 
     impl MessageNotification {
+        pub fn new_messsage<S: AsRef<str>>(message: S) -> MessageNotification {
+            Self {
+                message: message.as_ref().to_string(),
+                click_action: None,
+                click_message: None,
+            }
+        }
+
         pub fn new<S1: AsRef<str>, A: Action, S2: AsRef<str>>(
             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<dyn Action>,
-                click_message: click_message.as_ref().to_string(),
+                click_action: Some(Box::new(click_action) as Box<dyn Action>),
+                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::<MessageNotificationTag>::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<Workspace>,
+    ) -> Option<Self::Ok>;
+}
+
+impl<T, E> NotifyResultExt for Result<T, E>
+where
+    E: std::fmt::Debug,
+{
+    type Ok = T;
+
+    fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
+        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
+            }
+        }
+    }
+}

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<dyn fs::Fs>,
     pub build_window_options: fn() -> WindowOptions<'static>,
     pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
-    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<SerializedWorkspace>,
         workspace_id: WorkspaceId,
         project: ModelHandle<Project>,
-        dock_default_factory: DefaultItemFactory,
+        dock_default_factory: DockDefaultItemFactory,
         cx: &mut ViewContext<Self>,
     ) -> 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<Workspace>,
-    ) -> Box<dyn ItemHandle> {
+    ) -> Option<Box<dyn ItemHandle>> {
         unimplemented!();
     }
 

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" }

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<Workspace>,
-) -> Box<dyn ItemHandle> {
+) -> Option<Box<dyn ItemHandle>> {
     let strategy = cx
         .global::<Settings>()
         .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))
 }