From 1b8763d0cfeadb60791c2cfe7e5d84619f4c8d50 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 6 Dec 2022 11:28:56 -0800 Subject: [PATCH 1/6] WIP - move terminal to project as pre-prep for collaboration --- Cargo.lock | 28 +++++++++++- crates/project/Cargo.toml | 1 + crates/project/src/project.rs | 12 +++++ crates/terminal/Cargo.toml | 20 ++------- crates/terminal/src/persistence.rs | 14 +++--- crates/terminal/src/terminal.rs | 4 -- crates/terminal_view/Cargo.toml | 44 +++++++++++++++++++ crates/{terminal => terminal_view}/README.md | 0 .../scripts/print256color.sh | 0 .../scripts/truecolor.sh | 0 .../src/terminal_container_view.rs | 0 .../src/terminal_element.rs | 0 .../src/terminal_view.rs | 0 crates/zed/Cargo.toml | 2 +- 14 files changed, 95 insertions(+), 30 deletions(-) create mode 100644 crates/terminal_view/Cargo.toml rename crates/{terminal => terminal_view}/README.md (100%) rename crates/{terminal => terminal_view}/scripts/print256color.sh (100%) rename crates/{terminal => terminal_view}/scripts/truecolor.sh (100%) rename crates/{terminal => terminal_view}/src/terminal_container_view.rs (100%) rename crates/{terminal => terminal_view}/src/terminal_element.rs (100%) rename crates/{terminal => terminal_view}/src/terminal_view.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 7d7dc42bea70cb3715c39cbddfa8ed7b5c1025a2..1868959a09420e253f71610f77f421a358188f2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4463,6 +4463,7 @@ dependencies = [ "smol", "sum_tree", "tempdir", + "terminal", "text", "thiserror", "toml", @@ -6257,6 +6258,31 @@ dependencies = [ [[package]] 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", + "serde", + "settings", + "shellexpand", + "smallvec", + "smol", + "theme", + "thiserror", +] + +[[package]] +name = "terminal_view" +version = "0.1.0" dependencies = [ "alacritty_terminal", "anyhow", @@ -8166,7 +8192,7 @@ dependencies = [ "smol", "sum_tree", "tempdir", - "terminal", + "terminal_view", "text", "theme", "theme_selector", diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 76c60f9556235605e59a62cc5bd8c70aa9aaff1b..dd4d2be5b6847b24d2cf952a73b313c7d1676f49 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -32,6 +32,7 @@ lsp = { path = "../lsp" } rpc = { path = "../rpc" } settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } +terminal = { path = "../terminal" } util = { path = "../util" } aho-corasick = "0.7" anyhow = "1.0.57" diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 512ac702d062be924ac5183a9e98d34be80a45f7..e61f0fe0b7e346321602ab8d5149c3edbd65d31e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -60,6 +60,7 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, + thread::panicking, time::Instant, }; use thiserror::Error; @@ -1193,6 +1194,17 @@ impl Project { !self.is_local() } + pub fn create_terminal_connection( + &mut self, + cx: &mut ModelContext, + ) -> Result> { + if self.is_remote() { + return Err(anyhow!( + "creating terminals as a guest is not supported yet" + )); + } + } + pub fn create_buffer( &mut self, text: &str, diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 5593ee92d4dc4fc4135c8f30a6dbaeee6753eb6d..2948eaec69a68cc71a3a59f3461e0a84d9130f8e 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -7,17 +7,12 @@ 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" } -theme = { path = "../theme" } -util = { path = "../util" } -workspace = { path = "../workspace" } db = { path = "../db" } +theme = { path = "../theme" } 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"] } @@ -32,13 +27,4 @@ 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" +serde = { version = "1.0", features = ["derive"] } \ No newline at end of file diff --git a/crates/terminal/src/persistence.rs b/crates/terminal/src/persistence.rs index 1669a3a546773fa461d94152953e962d4ac6ec7c..333911ee6d61fca768de10e6191c26a43db4bce0 100644 --- a/crates/terminal/src/persistence.rs +++ b/crates/terminal/src/persistence.rs @@ -2,16 +2,16 @@ use std::path::PathBuf; use db::{define_connection, query, sqlez_macros::sql}; -use workspace::{ItemId, WorkspaceDb, WorkspaceId}; +type ModelId = usize; define_connection! { - pub static ref TERMINAL_CONNECTION: TerminalDb = + pub static ref TERMINAL_CONNECTION: TerminalDb<()> = &[sql!( CREATE TABLE terminals ( workspace_id INTEGER, - item_id INTEGER UNIQUE, + model_id INTEGER UNIQUE, working_directory BLOB, - PRIMARY KEY(workspace_id, item_id), + PRIMARY KEY(workspace_id, model_id), FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; @@ -23,7 +23,7 @@ impl TerminalDb { pub async fn update_workspace_id( new_id: WorkspaceId, old_id: WorkspaceId, - item_id: ItemId + item_id: ModelId ) -> Result<()> { UPDATE terminals SET workspace_id = ? @@ -33,7 +33,7 @@ impl TerminalDb { query! { pub async fn save_working_directory( - item_id: ItemId, + item_id: ModelId, workspace_id: WorkspaceId, working_directory: PathBuf ) -> Result<()> { @@ -43,7 +43,7 @@ impl TerminalDb { } query! { - pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { + pub fn get_working_directory(item_id: ModelId, workspace_id: WorkspaceId) -> Result> { SELECT working_directory FROM terminals WHERE item_id = ? AND workspace_id = ? diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 0cbb6d36b11bffd07c04f5fae65504b0dac29136..62df8aca827bc2a940edcb0428c6a555163907f1 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,8 +1,5 @@ pub mod mappings; mod persistence; -pub mod terminal_container_view; -pub mod terminal_element; -pub mod terminal_view; use alacritty_terminal::{ ansi::{ClearMode, Handler}, @@ -37,7 +34,6 @@ use persistence::TERMINAL_CONNECTION; use procinfo::LocalProcessInfo; use settings::{AlternateScroll, Settings, Shell, TerminalBlink}; use util::ResultExt; -use workspace::{ItemId, WorkspaceId}; use std::{ cmp::min, diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..181ed606e0bcf36f38c227735409fd0ea0a6bf64 --- /dev/null +++ b/crates/terminal_view/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "terminal_view" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/terminal_container_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" } +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"] } +smol = "1.2.5" +mio-extras = "2.0.6" +futures = "0.3" +ordered-float = "2.1.1" +itertools = "0.10" +dirs = "4.0.0" +shellexpand = "2.1.0" +libc = "0.2" +anyhow = "1" +thiserror = "1.0" +lazy_static = "1.4.0" +serde = { version = "1.0", features = ["derive"] } + + + +[dev-dependencies] +gpui = { path = "../gpui", features = ["test-support"] } +client = { path = "../client", features = ["test-support"]} +project = { path = "../project", features = ["test-support"]} +workspace = { path = "../workspace", features = ["test-support"] } +rand = "0.8.5" diff --git a/crates/terminal/README.md b/crates/terminal_view/README.md similarity index 100% rename from crates/terminal/README.md rename to crates/terminal_view/README.md diff --git a/crates/terminal/scripts/print256color.sh b/crates/terminal_view/scripts/print256color.sh similarity index 100% rename from crates/terminal/scripts/print256color.sh rename to crates/terminal_view/scripts/print256color.sh diff --git a/crates/terminal/scripts/truecolor.sh b/crates/terminal_view/scripts/truecolor.sh similarity index 100% rename from crates/terminal/scripts/truecolor.sh rename to crates/terminal_view/scripts/truecolor.sh diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal_view/src/terminal_container_view.rs similarity index 100% rename from crates/terminal/src/terminal_container_view.rs rename to crates/terminal_view/src/terminal_container_view.rs diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs similarity index 100% rename from crates/terminal/src/terminal_element.rs rename to crates/terminal_view/src/terminal_element.rs diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs similarity index 100% rename from crates/terminal/src/terminal_view.rs rename to crates/terminal_view/src/terminal_view.rs diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6dfb739f3abc07322a180920e4aff6abc35161c6..a07c0c899c9f5aea692334c0d1f0e83dbba317db 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -48,7 +48,7 @@ rpc = { path = "../rpc" } settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } text = { path = "../text" } -terminal = { path = "../terminal" } +terminal_view = { path = "../terminal_view" } theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } theme_testbench = { path = "../theme_testbench" } From 83aefffa38a630651104cce729efef77737cebab Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 8 Dec 2022 10:48:28 -0800 Subject: [PATCH 2/6] Rearrange the terminal code to not have a cyclic dependency with the project --- Cargo.lock | 4 +- crates/editor/src/editor.rs | 2 +- crates/project/src/project.rs | 8 +- crates/terminal/Cargo.toml | 6 +- crates/terminal/src/terminal.rs | 153 +++++++--------- .../src/tests/terminal_test_context.rs | 143 --------------- crates/terminal_view/Cargo.toml | 2 +- .../src/persistence.rs | 5 +- .../src/terminal_container_view.rs | 164 ++++++++++++++---- crates/terminal_view/src/terminal_element.rs | 31 ++-- crates/terminal_view/src/terminal_view.rs | 46 ++++- crates/zed/src/main.rs | 4 +- 12 files changed, 269 insertions(+), 299 deletions(-) delete mode 100644 crates/terminal/src/tests/terminal_test_context.rs rename crates/{terminal => terminal_view}/src/persistence.rs (91%) diff --git a/Cargo.lock b/Cargo.lock index 1868959a09420e253f71610f77f421a358188f2c..ddd7a0f7fdf7f621dec1db82fe5791973e037cdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6271,6 +6271,7 @@ dependencies = [ "mio-extras", "ordered-float", "procinfo", + "rand 0.8.5", "serde", "settings", "shellexpand", @@ -6278,13 +6279,13 @@ dependencies = [ "smol", "theme", "thiserror", + "util", ] [[package]] name = "terminal_view" version = "0.1.0" dependencies = [ - "alacritty_terminal", "anyhow", "client", "context_menu", @@ -6307,6 +6308,7 @@ dependencies = [ "shellexpand", "smallvec", "smol", + "terminal", "theme", "thiserror", "util", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 417b60bc5bb99dc20bc128e57e5b7708962456ff..ad21622fd9eb6d52f59a26dc85e3881165a44eef 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2422,7 +2422,7 @@ impl Editor { let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| { let excerpt_range = excerpt_range.to_offset(buffer); buffer - .edited_ranges_for_transaction(transaction) + .edited_ranges_for_transaction::(transaction) .all(|range| { excerpt_range.start <= range.start && excerpt_range.end >= range.end diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e61f0fe0b7e346321602ab8d5149c3edbd65d31e..40f1c93e5113d53be6967315e5ec9b25d890a497 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -60,9 +60,9 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, - thread::panicking, time::Instant, }; +use terminal::Terminal; use thiserror::Error; use util::{defer, post_inc, ResultExt, TryFutureExt as _}; @@ -1196,12 +1196,14 @@ impl Project { pub fn create_terminal_connection( &mut self, - cx: &mut ModelContext, - ) -> Result> { + _cx: &mut ModelContext, + ) -> Result> { if self.is_remote() { return Err(anyhow!( "creating terminals as a guest is not supported yet" )); + } else { + unimplemented!() } } diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 2948eaec69a68cc71a3a59f3461e0a84d9130f8e..0dea7bfbcfa80ef39150f68d26f5b74978c53b65 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -13,6 +13,7 @@ gpui = { path = "../gpui" } settings = { path = "../settings" } db = { path = "../db" } theme = { path = "../theme" } +util = { path = "../util" } 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"] } @@ -27,4 +28,7 @@ libc = "0.2" anyhow = "1" thiserror = "1.0" lazy_static = "1.4.0" -serde = { version = "1.0", features = ["derive"] } \ No newline at end of file +serde = { version = "1.0", features = ["derive"] } + +[dev-dependencies] +rand = "0.8.5" diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 62df8aca827bc2a940edcb0428c6a555163907f1..937678df0ba47f4e068d9ca4206cbd8faa431adc 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,5 +1,5 @@ pub mod mappings; -mod persistence; +pub use alacritty_terminal; use alacritty_terminal::{ ansi::{ClearMode, Handler}, @@ -30,7 +30,6 @@ 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; @@ -53,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::{ @@ -63,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. @@ -124,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 { @@ -281,8 +273,6 @@ impl TerminalBuilder { blink_settings: Option, alternate_scroll: &AlternateScroll, window_id: usize, - item_id: ItemId, - workspace_id: WorkspaceId, ) -> Result { let pty_config = { let alac_shell = shell.clone().and_then(|shell| match shell { @@ -387,8 +377,6 @@ impl TerminalBuilder { last_mouse_position: None, next_link_id: 0, selection_phase: SelectionPhase::Ended, - workspace_id, - item_id, }; Ok(TerminalBuilder { @@ -460,9 +448,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 { @@ -474,17 +462,18 @@ impl Deref for IndexedCell { } } +// TODO: Un-pub #[derive(Clone)] pub struct TerminalContent { - cells: Vec, - mode: TermMode, - display_offset: usize, - selection_text: Option, - selection: Option, - cursor: RenderableCursor, - cursor_char: char, - size: TerminalSize, - last_hovered_hyperlink: Option<(String, RangeInclusive, usize)>, + pub cells: Vec, + pub mode: TermMode, + pub display_offset: usize, + pub selection_text: Option, + pub selection: Option, + pub cursor: RenderableCursor, + pub cursor_char: char, + pub size: TerminalSize, + pub last_hovered_hyperlink: Option<(String, RangeInclusive, usize)>, } impl Default for TerminalContent { @@ -521,19 +510,17 @@ pub struct Terminal { /// This is only used for terminal hyperlink checking last_mouse_position: Option, pub matches: Vec>, - last_content: TerminalContent, + pub last_content: TerminalContent, last_synced: Instant, sync_task: Option>, - selection_head: Option, - breadcrumb_text: String, + pub selection_head: Option, + pub breadcrumb_text: String, shell_pid: u32, shell_fd: u32, - foreground_process_info: Option, + pub foreground_process_info: Option, scroll_px: f32, next_link_id: usize, selection_phase: SelectionPhase, - workspace_id: WorkspaceId, - item_id: ItemId, } impl Terminal { @@ -574,20 +561,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) => { @@ -1190,42 +1163,13 @@ impl Terminal { } } - pub fn set_workspace_id(&mut self, id: WorkspaceId, cx: &AppContext) { - let old_workspace_id = self.workspace_id; - let item_id = self.item_id; - cx.background() - .spawn(async move { - TERMINAL_CONNECTION - .update_workspace_id(id, old_workspace_id, item_id) - .await - .log_err() - }) - .detach(); - - self.workspace_id = id; - } - pub fn find_matches( &mut self, - query: project::search::SearchQuery, + searcher: RegexSearch, cx: &mut ModelContext, ) -> Task>> { let term = self.term.clone(); cx.background().spawn(async move { - let searcher = match query { - project::search::SearchQuery::Text { query, .. } => { - RegexSearch::new(query.as_ref()) - } - project::search::SearchQuery::Regex { query, .. } => { - RegexSearch::new(query.as_ref()) - } - }; - - if searcher.is_err() { - return Vec::new(); - } - let searcher = searcher.unwrap(); - let term = term.lock(); all_search_matches(&term, &searcher).collect() @@ -1322,14 +1266,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 rand::{rngs::ThreadRng, thread_rng, Rng}; - use self::terminal_test_context::TerminalTestContext; - - pub mod terminal_test_context; + use crate::{content_index_for_mouse, IndexedCell, TerminalContent, TerminalSize}; #[test] fn test_mouse_to_cell() { @@ -1346,7 +1290,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; @@ -1382,7 +1326,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, @@ -1393,4 +1337,37 @@ mod tests { cells[9][9] ); } + + fn create_terminal_content( + size: TerminalSize, + rng: &mut ThreadRng, + ) -> (TerminalContent, Vec>) { + let mut ic = Vec::new(); + let mut cells = Vec::new(); + + for row in 0..((size.height() / size.line_height()) as usize) { + let mut row_vec = Vec::new(); + for col in 0..((size.width() / size.cell_width()) as usize) { + let cell_char = rng.gen(); + ic.push(IndexedCell { + point: Point::new(Line(row as i32), Column(col)), + cell: Cell { + c: cell_char, + ..Default::default() + }, + }); + row_vec.push(cell_char) + } + cells.push(row_vec) + } + + ( + TerminalContent { + cells: ic, + size, + ..Default::default() + }, + cells, + ) + } } diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs deleted file mode 100644 index 67ebb558052f40a7f0d273034c3f7802fe8586ba..0000000000000000000000000000000000000000 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::{path::Path, time::Duration}; - -use alacritty_terminal::{ - index::{Column, Line, Point}, - term::cell::Cell, -}; -use gpui::{ModelHandle, TestAppContext, ViewHandle}; - -use project::{Entry, Project, ProjectPath, Worktree}; -use rand::{rngs::ThreadRng, Rng}; -use workspace::{AppState, Workspace}; - -use crate::{IndexedCell, TerminalContent, TerminalSize}; - -pub struct TerminalTestContext<'a> { - pub cx: &'a mut TestAppContext, -} - -impl<'a> TerminalTestContext<'a> { - pub fn new(cx: &'a mut TestAppContext) -> Self { - cx.set_condition_duration(Some(Duration::from_secs(5))); - - TerminalTestContext { cx } - } - - ///Creates a worktree with 1 file: /root.txt - pub async fn blank_workspace(&mut self) -> (ModelHandle, ViewHandle) { - let params = self.cx.update(AppState::test); - - let project = Project::test(params.fs.clone(), [], self.cx).await; - let (_, workspace) = self.cx.add_window(|cx| { - Workspace::new( - Default::default(), - 0, - project.clone(), - |_, _| unimplemented!(), - cx, - ) - }); - - (project, workspace) - } - - ///Creates a worktree with 1 folder: /root{suffix}/ - pub async fn create_folder_wt( - &mut self, - project: ModelHandle, - path: impl AsRef, - ) -> (ModelHandle, Entry) { - self.create_wt(project, true, path).await - } - - ///Creates a worktree with 1 file: /root{suffix}.txt - pub async fn create_file_wt( - &mut self, - project: ModelHandle, - path: impl AsRef, - ) -> (ModelHandle, Entry) { - self.create_wt(project, false, path).await - } - - async fn create_wt( - &mut self, - project: ModelHandle, - is_dir: bool, - path: impl AsRef, - ) -> (ModelHandle, Entry) { - let (wt, _) = project - .update(self.cx, |project, cx| { - project.find_or_create_local_worktree(path, true, cx) - }) - .await - .unwrap(); - - let entry = self - .cx - .update(|cx| { - wt.update(cx, |wt, cx| { - wt.as_local() - .unwrap() - .create_entry(Path::new(""), is_dir, cx) - }) - }) - .await - .unwrap(); - - (wt, entry) - } - - pub fn insert_active_entry_for( - &mut self, - wt: ModelHandle, - entry: Entry, - project: ModelHandle, - ) { - self.cx.update(|cx| { - let p = ProjectPath { - worktree_id: wt.read(cx).id(), - path: entry.path, - }; - project.update(cx, |project, cx| project.set_active_path(Some(p), cx)); - }); - } - - pub fn create_terminal_content( - size: TerminalSize, - rng: &mut ThreadRng, - ) -> (TerminalContent, Vec>) { - let mut ic = Vec::new(); - let mut cells = Vec::new(); - - for row in 0..((size.height() / size.line_height()) as usize) { - let mut row_vec = Vec::new(); - for col in 0..((size.width() / size.cell_width()) as usize) { - let cell_char = rng.gen(); - ic.push(IndexedCell { - point: Point::new(Line(row as i32), Column(col)), - cell: Cell { - c: cell_char, - ..Default::default() - }, - }); - row_vec.push(cell_char) - } - cells.push(row_vec) - } - - ( - TerminalContent { - cells: ic, - size, - ..Default::default() - }, - cells, - ) - } -} - -impl<'a> Drop for TerminalTestContext<'a> { - fn drop(&mut self) { - self.cx.set_condition_duration(None); - } -} diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 181ed606e0bcf36f38c227735409fd0ea0a6bf64..fae60a943d4d32720e855d6f5fb1a2830e76e166 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -18,8 +18,8 @@ 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 } +terminal = { path = "../terminal" } smallvec = { version = "1.6", features = ["union"] } smol = "1.2.5" mio-extras = "2.0.6" diff --git a/crates/terminal/src/persistence.rs b/crates/terminal_view/src/persistence.rs similarity index 91% rename from crates/terminal/src/persistence.rs rename to crates/terminal_view/src/persistence.rs index 333911ee6d61fca768de10e6191c26a43db4bce0..db715aeef70636abe4cbe02042dc22560441c04d 100644 --- a/crates/terminal/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -1,11 +1,12 @@ use std::path::PathBuf; use db::{define_connection, query, sqlez_macros::sql}; +use workspace::{WorkspaceDb, WorkspaceId}; type ModelId = usize; define_connection! { - pub static ref TERMINAL_CONNECTION: TerminalDb<()> = + pub static ref TERMINAL_DB: TerminalDb = &[sql!( CREATE TABLE terminals ( workspace_id INTEGER, @@ -34,7 +35,7 @@ impl TerminalDb { query! { pub async fn save_working_directory( item_id: ModelId, - workspace_id: WorkspaceId, + workspace_id: i64, working_directory: PathBuf ) -> Result<()> { INSERT OR REPLACE INTO terminals(item_id, workspace_id, working_directory) diff --git a/crates/terminal_view/src/terminal_container_view.rs b/crates/terminal_view/src/terminal_container_view.rs index 8f4bfeeb5364d6c3c49f20976230c17efb148379..bf1e7bbddb8f26ed445e002b17f3293bc8769b2b 100644 --- a/crates/terminal_view/src/terminal_container_view.rs +++ b/crates/terminal_view/src/terminal_container_view.rs @@ -1,13 +1,18 @@ -use crate::persistence::TERMINAL_CONNECTION; +mod persistence; +pub mod terminal_element; +pub mod terminal_view; + +use crate::persistence::TERMINAL_DB; use crate::terminal_view::TerminalView; -use crate::{Event, TerminalBuilder, TerminalError}; +use terminal::alacritty_terminal::index::Point; +use terminal::{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 terminal_view::regex_search_for_query; use util::{truncate_and_trailoff, ResultExt}; use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}; use workspace::{ @@ -30,6 +35,8 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(TerminalContainer::deploy); register_deserializable_item::(cx); + + terminal_view::init(cx); } //Make terminal view an enum, that can give you views for the error and non-error states @@ -92,7 +99,7 @@ impl TerminalContainer { pub fn new( working_directory: Option, modal: bool, - workspace_id: WorkspaceId, + _workspace_id: WorkspaceId, cx: &mut ViewContext, ) -> Self { let settings = cx.global::(); @@ -119,8 +126,6 @@ impl TerminalContainer { 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)); @@ -389,7 +394,7 @@ impl Item for TerminalContainer { item_id: workspace::ItemId, cx: &mut ViewContext, ) -> Task>> { - let working_directory = TERMINAL_CONNECTION.get_working_directory(item_id, workspace_id); + let working_directory = TERMINAL_DB.get_working_directory(item_id, workspace_id); Task::ready(Ok(cx.add_view(|cx| { TerminalContainer::new( working_directory.log_err().flatten(), @@ -400,11 +405,14 @@ impl Item for TerminalContainer { }))) } - fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { - if let Some(connected) = self.connected() { - let id = workspace.database_id(); - let terminal_handle = connected.read(cx).terminal().clone(); - terminal_handle.update(cx, |terminal, cx| terminal.set_workspace_id(id, cx)) + fn added_to_workspace(&mut self, _workspace: &mut Workspace, cx: &mut ViewContext) { + if let Some(_connected) = self.connected() { + // let id = workspace.database_id(); + // let terminal_handle = connected.read(cx).terminal().clone(); + //TODO + cx.background() + .spawn(TERMINAL_DB.update_workspace_id(0, 0, 0)) + .detach(); } } } @@ -477,7 +485,11 @@ impl SearchableItem for TerminalContainer { ) -> Task> { if let TerminalContainerContent::Connected(connected) = &self.content { let terminal = connected.read(cx).terminal().clone(); - terminal.update(cx, |term, cx| term.find_matches(query, cx)) + if let Some(searcher) = regex_search_for_query(query) { + terminal.update(cx, |term, cx| term.find_matches(searcher, cx)) + } else { + cx.background().spawn(async { Vec::new() }) + } } else { Task::ready(Vec::new()) } @@ -585,21 +597,20 @@ mod tests { use super::*; use gpui::TestAppContext; + use project::{Entry, Worktree}; + use workspace::AppState; 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; + let (project, workspace) = blank_workspace(cx).await; //Test - cx.cx.read(|cx| { + cx.read(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -619,11 +630,10 @@ mod tests { 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; + let (project, workspace) = blank_workspace(cx).await; + create_file_wt(project.clone(), "/root.txt", cx).await; - cx.cx.read(|cx| { + cx.read(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -642,12 +652,11 @@ mod tests { #[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; + let (project, workspace) = blank_workspace(cx).await; + let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await; //Test - cx.cx.update(|cx| { + cx.update(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -665,14 +674,14 @@ mod tests { #[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()); + + 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.cx.update(|cx| { + cx.update(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -689,14 +698,13 @@ mod tests { #[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()); + 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.cx.update(|cx| { + cx.update(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -708,4 +716,84 @@ mod tests { assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); }); } + + ///Creates a worktree with 1 file: /root.txt + pub async fn blank_workspace( + cx: &mut TestAppContext, + ) -> (ModelHandle, ViewHandle) { + let params = cx.update(AppState::test); + + let project = Project::test(params.fs.clone(), [], cx).await; + let (_, workspace) = cx.add_window(|cx| { + Workspace::new( + Default::default(), + 0, + project.clone(), + |_, _| unimplemented!(), + cx, + ) + }); + + (project, workspace) + } + + ///Creates a worktree with 1 folder: /root{suffix}/ + async fn create_folder_wt( + project: ModelHandle, + path: impl AsRef, + cx: &mut TestAppContext, + ) -> (ModelHandle, Entry) { + create_wt(project, true, path, cx).await + } + + ///Creates a worktree with 1 file: /root{suffix}.txt + async fn create_file_wt( + project: ModelHandle, + path: impl AsRef, + cx: &mut TestAppContext, + ) -> (ModelHandle, Entry) { + create_wt(project, false, path, cx).await + } + + async fn create_wt( + project: ModelHandle, + is_dir: bool, + path: impl AsRef, + cx: &mut TestAppContext, + ) -> (ModelHandle, Entry) { + let (wt, _) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree(path, true, cx) + }) + .await + .unwrap(); + + let entry = cx + .update(|cx| { + wt.update(cx, |wt, cx| { + wt.as_local() + .unwrap() + .create_entry(Path::new(""), is_dir, cx) + }) + }) + .await + .unwrap(); + + (wt, entry) + } + + pub fn insert_active_entry_for( + wt: ModelHandle, + entry: Entry, + project: ModelHandle, + cx: &mut TestAppContext, + ) { + cx.update(|cx| { + let p = ProjectPath { + worktree_id: wt.read(cx).id(), + path: entry.path, + }; + project.update(cx, |project, cx| project.set_active_path(Some(p), cx)); + }); + } } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index adfcb47024c91c79e420edbad98101599745bcfb..53a38ec20a95e1f1bc3def84be65123305e1f6f8 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1,9 +1,3 @@ -use alacritty_terminal::{ - ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor}, - grid::Dimensions, - index::Point, - term::{cell::Flags, TermMode}, -}; use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; use gpui::{ color::Color, @@ -22,17 +16,23 @@ use itertools::Itertools; use language::CursorShape; use ordered_float::OrderedFloat; use settings::Settings; +use terminal::{ + alacritty_terminal::{ + ansi::{Color as AnsiColor, 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::terminal_view::{DeployContextMenu, TerminalView}; ///The information generated during layout that is nescessary for painting pub struct LayoutState { @@ -198,7 +198,10 @@ impl TerminalElement { //Expand background rect range { - if matches!(bg, Named(NamedColor::Background)) { + if matches!( + bg, + terminal::alacritty_terminal::ansi::Color::Named(NamedColor::Background) + ) { //Continue to next cell, resetting variables if nescessary cur_alac_color = None; if let Some(rect) = cur_rect { @@ -299,7 +302,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, @@ -636,7 +639,7 @@ impl Element for TerminalElement { //Layout cursor. Rectangle is used for IME, so we should lay it out even //if we don't end up showing it. - let cursor = if let AlacCursorShape::Hidden = cursor.shape { + let cursor = if let terminal::alacritty_terminal::ansi::CursorShape::Hidden = cursor.shape { None } else { let cursor_point = DisplayCursor::from(cursor.point, *display_offset); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 21e055319af62bfa42b168635fdbb5129c061492..ad0538e2ea9cfb4a0ef29fe114019ebd53bacccd 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1,6 +1,5 @@ -use std::{ops::RangeInclusive, time::Duration}; +use std::{ops::RangeInclusive, path::PathBuf, time::Duration}; -use alacritty_terminal::{index::Point, term::TermMode}; use context_menu::{ContextMenu, ContextMenuItem}; use gpui::{ actions, @@ -14,10 +13,17 @@ use gpui::{ use serde::Deserialize; use settings::{Settings, TerminalBlink}; use smol::Timer; +use terminal::{ + alacritty_terminal::{ + index::Point, + term::{search::RegexSearch, TermMode}, + }, + Terminal, +}; use util::ResultExt; use workspace::pane; -use crate::{terminal_element::TerminalElement, Event, Terminal}; +use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement, Event}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); @@ -95,6 +101,22 @@ impl TerminalView { cx.emit(Event::Wakeup); } Event::BlinkChanged => this.blinking_on = !this.blinking_on, + Event::TitleChanged => { + // if let Some(foreground_info) = &terminal.read(cx).foreground_process_info { + // let cwd = foreground_info.cwd.clone(); + //TODO + // let item_id = self.item_id; + // let workspace_id = self.workspace_id; + cx.background() + .spawn(async move { + TERMINAL_DB + .save_working_directory(0, 0, PathBuf::new()) + .await + .log_err(); + }) + .detach(); + // } + } _ => cx.emit(*event), }) .detach(); @@ -246,8 +268,14 @@ impl TerminalView { query: project::search::SearchQuery, cx: &mut ViewContext, ) -> Task>> { - self.terminal - .update(cx, |term, cx| term.find_matches(query, cx)) + 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 { @@ -302,6 +330,14 @@ impl TerminalView { } } +pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option { + let searcher = match query { + project::search::SearchQuery::Text { query, .. } => RegexSearch::new(&query), + project::search::SearchQuery::Regex { query, .. } => RegexSearch::new(&query), + }; + searcher.ok() +} + impl View for TerminalView { fn ui_name() -> &'static str { "Terminal" diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 4163841d455d3e4e30620a28b80a0e5fdba16ca9..1b41613937f203588cad4dedc5faf55c6854cfc0 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -32,7 +32,7 @@ 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, TerminalContainer}; use fs::RealFs; use settings::watched_json::{watch_keymap_file, watch_settings_file, WatchedJsonFile}; @@ -119,7 +119,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)) From 2733f91d8caa2d18ed5e63aa9487d0add3d6afd0 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 8 Dec 2022 15:18:24 -0800 Subject: [PATCH 3/6] Fix bugs resulting from refactoring the terminal into project and workspace halves --- crates/db/src/query.rs | 4 +- crates/settings/src/settings.rs | 28 ++++++++- crates/terminal/src/terminal.rs | 7 +-- crates/terminal_view/src/persistence.rs | 22 +++---- .../src/terminal_container_view.rs | 62 ++++++++----------- crates/terminal_view/src/terminal_element.rs | 9 +-- crates/terminal_view/src/terminal_view.rs | 45 +++++++++----- 7 files changed, 101 insertions(+), 76 deletions(-) diff --git a/crates/db/src/query.rs b/crates/db/src/query.rs index 731fca15cb5c47b58e89aac1eb2a7b42189829c2..01132d383c2928937d26d7733870f2c430798a39 100644 --- a/crates/db/src/query.rs +++ b/crates/db/src/query.rs @@ -199,10 +199,10 @@ macro_rules! query { use $crate::anyhow::Context; - self.write(|connection| { + self.write(move |connection| { let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); - connection.select_row_bound::<($($arg_type),+), $return_type>(indoc! { $sql })?(($($arg),+)) + connection.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+)) .context(::std::format!( "Error in {}, select_row_bound failed to execute or parse for: {}", ::std::stringify!($id), diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 5137751579e90fc52aa8efbd0f049975fd79cc54..dd23f80abd83d16e3f0a1dc77400ad4b1b87ede7 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -199,7 +199,7 @@ impl Default for Shell { } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum AlternateScroll { On, @@ -473,6 +473,32 @@ impl Settings { }) } + pub fn terminal_scroll(&self) -> AlternateScroll { + *self.terminal_overrides.alternate_scroll.as_ref().unwrap_or( + self.terminal_defaults + .alternate_scroll + .as_ref() + .unwrap_or_else(|| &AlternateScroll::On), + ) + } + + pub fn terminal_shell(&self) -> Option { + self.terminal_overrides + .shell + .as_ref() + .or(self.terminal_defaults.shell.as_ref()) + .cloned() + } + + pub fn terminal_env(&self) -> HashMap { + self.terminal_overrides.env.clone().unwrap_or_else(|| { + self.terminal_defaults + .env + .clone() + .unwrap_or_else(|| HashMap::default()) + }) + } + #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &gpui::AppContext) -> Settings { Settings { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 937678df0ba47f4e068d9ca4206cbd8faa431adc..4b69de0bf2a37ea600db5fec17eebd948a87f877 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -269,9 +269,9 @@ impl TerminalBuilder { pub fn new( working_directory: Option, shell: Option, - env: Option>, + mut env: HashMap, blink_settings: Option, - alternate_scroll: &AlternateScroll, + alternate_scroll: AlternateScroll, window_id: usize, ) -> Result { let pty_config = { @@ -288,10 +288,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); diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index db715aeef70636abe4cbe02042dc22560441c04d..f090b384a44ae1c0a0e5743ed9060e14ccf6d052 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -1,20 +1,18 @@ use std::path::PathBuf; use db::{define_connection, query, sqlez_macros::sql}; -use workspace::{WorkspaceDb, WorkspaceId}; - -type ModelId = usize; +use workspace::{ItemId, WorkspaceDb, WorkspaceId}; define_connection! { pub static ref TERMINAL_DB: TerminalDb = &[sql!( CREATE TABLE terminals ( workspace_id INTEGER, - model_id INTEGER UNIQUE, + item_id INTEGER UNIQUE, working_directory BLOB, - PRIMARY KEY(workspace_id, model_id), + PRIMARY KEY(workspace_id, item_id), FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE + ON DELETE CASCADE ) STRICT; )]; } @@ -24,7 +22,7 @@ impl TerminalDb { pub async fn update_workspace_id( new_id: WorkspaceId, old_id: WorkspaceId, - item_id: ModelId + item_id: ItemId ) -> Result<()> { UPDATE terminals SET workspace_id = ? @@ -34,8 +32,8 @@ impl TerminalDb { query! { pub async fn save_working_directory( - item_id: ModelId, - workspace_id: i64, + item_id: ItemId, + workspace_id: WorkspaceId, working_directory: PathBuf ) -> Result<()> { INSERT OR REPLACE INTO terminals(item_id, workspace_id, working_directory) @@ -44,10 +42,10 @@ impl TerminalDb { } query! { - pub fn get_working_directory(item_id: ModelId, workspace_id: WorkspaceId) -> Result> { - SELECT working_directory - FROM terminals + pub async fn take_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { + DELETE FROM terminals WHERE item_id = ? AND workspace_id = ? + RETURNING working_directory } } } diff --git a/crates/terminal_view/src/terminal_container_view.rs b/crates/terminal_view/src/terminal_container_view.rs index bf1e7bbddb8f26ed445e002b17f3293bc8769b2b..9d8b79cd39dc0b5cdab61438d1588562a271c6cd 100644 --- a/crates/terminal_view/src/terminal_container_view.rs +++ b/crates/terminal_view/src/terminal_container_view.rs @@ -22,7 +22,7 @@ use workspace::{ use workspace::{register_deserializable_item, Pane, WorkspaceId}; use project::{LocalWorktree, Project, ProjectPath}; -use settings::{AlternateScroll, Settings, WorkingDirectory}; +use settings::{Settings, WorkingDirectory}; use smallvec::SmallVec; use std::ops::RangeInclusive; use std::path::{Path, PathBuf}; @@ -99,25 +99,13 @@ impl TerminalContainer { pub fn new( working_directory: Option, modal: bool, - _workspace_id: WorkspaceId, + workspace_id: WorkspaceId, cx: &mut ViewContext, ) -> Self { let settings = cx.global::(); - let shell = settings.terminal_overrides.shell.clone(); - let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap. - - //TODO: move this pattern to settings - let scroll = settings - .terminal_overrides - .alternate_scroll - .as_ref() - .unwrap_or( - settings - .terminal_defaults - .alternate_scroll - .as_ref() - .unwrap_or_else(|| &AlternateScroll::On), - ); + let shell = settings.terminal_shell(); + let envs = settings.terminal_env(); + let scroll = settings.terminal_scroll(); let content = match TerminalBuilder::new( working_directory.clone(), @@ -129,7 +117,10 @@ impl TerminalContainer { ) { Ok(terminal) => { let terminal = cx.add_model(|cx| terminal.subscribe(cx)); - let view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx)); + let item_id = cx.view_id(); + let view = cx.add_view(|cx| { + TerminalView::from_terminal(terminal, modal, workspace_id, item_id, cx) + }); cx.subscribe(&view, |_this, _content, event, cx| cx.emit(*event)) .detach(); @@ -394,25 +385,26 @@ impl Item for TerminalContainer { item_id: workspace::ItemId, cx: &mut ViewContext, ) -> Task>> { - let working_directory = TERMINAL_DB.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, - ) - }))) + 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| { + Ok(cx.add_view(pane, |cx| { + TerminalContainer::new(cwd, false, workspace_id, cx) + })) + }) + }) } - fn added_to_workspace(&mut self, _workspace: &mut Workspace, cx: &mut ViewContext) { - if let Some(_connected) = self.connected() { - // let id = workspace.database_id(); - // let terminal_handle = connected.read(cx).terminal().clone(); - //TODO - cx.background() - .spawn(TERMINAL_DB.update_workspace_id(0, 0, 0)) - .detach(); + fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { + if let Some(connected) = self.connected() { + connected.update(cx, |connected_view, cx| { + connected_view.added_to_workspace(workspace.database_id(), cx); + }) } } } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 53a38ec20a95e1f1bc3def84be65123305e1f6f8..506dd1423d58928ba3efbed87b5faf178cae2748 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -18,7 +18,7 @@ use ordered_float::OrderedFloat; use settings::Settings; use terminal::{ alacritty_terminal::{ - ansi::{Color as AnsiColor, CursorShape as AlacCursorShape, NamedColor}, + ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor}, grid::Dimensions, index::Point, term::{cell::Flags, TermMode}, @@ -198,10 +198,7 @@ impl TerminalElement { //Expand background rect range { - if matches!( - bg, - terminal::alacritty_terminal::ansi::Color::Named(NamedColor::Background) - ) { + if matches!(bg, Named(NamedColor::Background)) { //Continue to next cell, resetting variables if nescessary cur_alac_color = None; if let Some(rect) = cur_rect { @@ -639,7 +636,7 @@ impl Element for TerminalElement { //Layout cursor. Rectangle is used for IME, so we should lay it out even //if we don't end up showing it. - let cursor = if let terminal::alacritty_terminal::ansi::CursorShape::Hidden = cursor.shape { + let cursor = if let AlacCursorShape::Hidden = cursor.shape { None } else { let cursor_point = DisplayCursor::from(cursor.point, *display_offset); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index ad0538e2ea9cfb4a0ef29fe114019ebd53bacccd..c2f5c5c11432af3dbdf5e0296bb18c8d22030a92 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1,4 +1,4 @@ -use std::{ops::RangeInclusive, path::PathBuf, time::Duration}; +use std::{ops::RangeInclusive, time::Duration}; use context_menu::{ContextMenu, ContextMenuItem}; use gpui::{ @@ -21,7 +21,7 @@ use terminal::{ Terminal, }; use util::ResultExt; -use workspace::pane; +use workspace::{pane, ItemId, WorkspaceId}; use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement, Event}; @@ -75,6 +75,8 @@ pub struct TerminalView { blinking_on: bool, blinking_paused: bool, blink_epoch: usize, + workspace_id: WorkspaceId, + item_id: ItemId, } impl Entity for TerminalView { @@ -85,6 +87,8 @@ impl TerminalView { pub fn from_terminal( terminal: ModelHandle, modal: bool, + workspace_id: WorkspaceId, + item_id: ItemId, cx: &mut ViewContext, ) -> Self { cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); @@ -102,20 +106,20 @@ impl TerminalView { } Event::BlinkChanged => this.blinking_on = !this.blinking_on, Event::TitleChanged => { - // if let Some(foreground_info) = &terminal.read(cx).foreground_process_info { - // let cwd = foreground_info.cwd.clone(); - //TODO - // let item_id = self.item_id; - // let workspace_id = self.workspace_id; - cx.background() - .spawn(async move { - TERMINAL_DB - .save_working_directory(0, 0, PathBuf::new()) - .await - .log_err(); - }) - .detach(); - // } + if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info { + let cwd = foreground_info.cwd.clone(); + + let item_id = this.item_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), }) @@ -131,6 +135,8 @@ impl TerminalView { blinking_on: false, blinking_paused: false, blink_epoch: 0, + workspace_id, + item_id, } } @@ -282,6 +288,13 @@ impl TerminalView { &self.terminal } + pub fn added_to_workspace(&mut self, new_id: WorkspaceId, cx: &mut ViewContext) { + cx.background() + .spawn(TERMINAL_DB.update_workspace_id(new_id, self.workspace_id, self.item_id)) + .detach(); + self.workspace_id = new_id; + } + fn next_blink_epoch(&mut self) -> usize { self.blink_epoch += 1; self.blink_epoch From c42da5c9b9185dbefb70de5e144f3c70d9d7528b Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 8 Dec 2022 16:10:22 -0800 Subject: [PATCH 4/6] WIP --- crates/project/src/project.rs | 21 ++++++- crates/settings/src/settings.rs | 42 ++++++++------ crates/terminal/src/terminal.rs | 28 ++++----- crates/terminal_view/Cargo.toml | 2 +- .../src/terminal_container_view.rs | 57 ++++++------------- crates/terminal_view/src/terminal_view.rs | 5 ++ 6 files changed, 76 insertions(+), 79 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 40f1c93e5113d53be6967315e5ec9b25d890a497..545570da8993c55433ca7cba1c1b0b687881aa02 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -62,7 +62,7 @@ use std::{ }, time::Instant, }; -use terminal::Terminal; +use terminal::{Terminal, TerminalBuilder}; use thiserror::Error; use util::{defer, post_inc, ResultExt, TryFutureExt as _}; @@ -1196,14 +1196,29 @@ impl Project { pub fn create_terminal_connection( &mut self, - _cx: &mut ModelContext, + working_directory: Option, + window_id: usize, + cx: &mut ModelContext, ) -> Result> { if self.is_remote() { return Err(anyhow!( "creating terminals as a guest is not supported yet" )); } else { - unimplemented!() + let settings = cx.global::(); + let shell = settings.terminal_shell(); + let envs = settings.terminal_env(); + let scroll = settings.terminal_scroll(); + + TerminalBuilder::new( + working_directory.clone(), + shell, + envs, + settings.terminal_overrides.blinking.clone(), + scroll, + window_id, + ) + .map(|builder| cx.add_model(|cx| builder.subscribe(cx))) } } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index dd23f80abd83d16e3f0a1dc77400ad4b1b87ede7..f0c64a1bb995f3a710301de16fd8c00e02dc0088 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -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,30 +479,30 @@ impl Settings { }) } + fn terminal_setting(&self, f: F) -> R + where + F: Fn(&TerminalSettings) -> Option<&R>, + { + f(&self.terminal_overrides) + .or_else(|| f(&self.terminal_defaults)) + .cloned() + .unwrap_or_else(|| R::default()) + } + pub fn terminal_scroll(&self) -> AlternateScroll { - *self.terminal_overrides.alternate_scroll.as_ref().unwrap_or( - self.terminal_defaults - .alternate_scroll - .as_ref() - .unwrap_or_else(|| &AlternateScroll::On), - ) + self.terminal_setting(|terminal_setting| terminal_setting.alternate_scroll.as_ref()) } - pub fn terminal_shell(&self) -> Option { - self.terminal_overrides - .shell - .as_ref() - .or(self.terminal_defaults.shell.as_ref()) - .cloned() + pub fn terminal_shell(&self) -> Shell { + self.terminal_setting(|terminal_setting| terminal_setting.shell.as_ref()) } pub fn terminal_env(&self) -> HashMap { - self.terminal_overrides.env.clone().unwrap_or_else(|| { - self.terminal_defaults - .env - .clone() - .unwrap_or_else(|| HashMap::default()) - }) + 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"))] diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 4b69de0bf2a37ea600db5fec17eebd948a87f877..7cdac33cda45da3f9c85f3144f1be04e7abadbf0 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -198,7 +198,7 @@ impl Dimensions for TerminalSize { #[derive(Error, Debug)] pub struct TerminalError { pub directory: Option, - pub shell: Option, + pub shell: Shell, pub source: std::io::Error, } @@ -226,24 +226,20 @@ impl TerminalError { }) } - pub fn shell_to_string(&self) -> Option { - self.shell.as_ref().map(|shell| match shell { + pub fn shell_to_string(&self) -> String { + match &self.shell { Shell::System => "".to_string(), Shell::Program(p) => p.to_string(), Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), - }) + } } pub fn fmt_shell(&self) -> String { - self.shell - .clone() - .map(|shell| match shell { - Shell::System => "".to_string(), - - Shell::Program(s) => s, - Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), - }) - .unwrap_or_else(|| "".to_string()) + match &self.shell { + Shell::System => "".to_string(), + Shell::Program(s) => s.to_string(), + Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), + } } } @@ -268,18 +264,18 @@ pub struct TerminalBuilder { impl TerminalBuilder { pub fn new( working_directory: Option, - shell: Option, + shell: Shell, mut env: HashMap, blink_settings: Option, alternate_scroll: AlternateScroll, window_id: usize, ) -> Result { let pty_config = { - let alac_shell = shell.clone().and_then(|shell| match shell { + let alac_shell = match shell.clone() { Shell::System => None, Shell::Program(program) => Some(Program::Just(program)), Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }), - }); + }; PtyConfig { shell: alac_shell, diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index fae60a943d4d32720e855d6f5fb1a2830e76e166..05fda2c75f6893c5d502b6b223044f8bceaa5ad6 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [lib] -path = "src/terminal_container_view.rs" +path = "src/terminal_view.rs" doctest = false [dependencies] diff --git a/crates/terminal_view/src/terminal_container_view.rs b/crates/terminal_view/src/terminal_container_view.rs index 9d8b79cd39dc0b5cdab61438d1588562a271c6cd..322bf5ab527cd68b4a8525b7db0a1eca6b6a6c30 100644 --- a/crates/terminal_view/src/terminal_container_view.rs +++ b/crates/terminal_view/src/terminal_container_view.rs @@ -1,18 +1,14 @@ -mod persistence; -pub mod terminal_element; -pub mod terminal_view; - use crate::persistence::TERMINAL_DB; -use crate::terminal_view::TerminalView; +use crate::TerminalView; use terminal::alacritty_terminal::index::Point; -use terminal::{Event, TerminalBuilder, TerminalError}; +use terminal::{Event, Terminal, TerminalError}; +use crate::regex_search_for_query; use dirs::home_dir; use gpui::{ actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use terminal_view::regex_search_for_query; use util::{truncate_and_trailoff, ResultExt}; use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}; use workspace::{ @@ -36,7 +32,7 @@ pub fn init(cx: &mut MutableAppContext) { register_deserializable_item::(cx); - terminal_view::init(cx); + // terminal_view::init(cx); } //Make terminal view an enum, that can give you views for the error and non-error states @@ -81,47 +77,31 @@ impl TerminalContainer { _: &workspace::NewTerminal, cx: &mut ViewContext, ) { - let strategy = cx - .global::() - .terminal_overrides - .working_directory - .clone() - .unwrap_or(WorkingDirectory::CurrentProjectDirectory); + let strategy = cx.global::().terminal_strategy(); let working_directory = get_working_directory(workspace, cx, strategy); - let view = cx.add_view(|cx| { - TerminalContainer::new(working_directory, false, workspace.database_id(), cx) + + let window_id = cx.window_id(); + let terminal = workspace.project().update(cx, |project, cx| { + project.create_terminal_connection(working_directory, window_id, cx) }); + + let view = cx.add_view(|cx| TerminalContainer::new(terminal, workspace.database_id(), cx)); workspace.add_item(Box::new(view), cx); } ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices pub fn new( - working_directory: Option, - modal: bool, + model: anyhow::Result>, workspace_id: WorkspaceId, cx: &mut ViewContext, ) -> Self { - let settings = cx.global::(); - let shell = settings.terminal_shell(); - let envs = settings.terminal_env(); - let scroll = settings.terminal_scroll(); - - let content = match TerminalBuilder::new( - working_directory.clone(), - shell, - envs, - settings.terminal_overrides.blinking.clone(), - scroll, - cx.window_id(), - ) { + let content = match model { Ok(terminal) => { - let terminal = cx.add_model(|cx| terminal.subscribe(cx)); let item_id = cx.view_id(); let view = cx.add_view(|cx| { - TerminalView::from_terminal(terminal, modal, workspace_id, item_id, cx) + TerminalView::from_terminal(terminal, false, workspace_id, item_id, cx) }); - cx.subscribe(&view, |_this, _content, event, cx| cx.emit(*event)) .detach(); TerminalContainerContent::Connected(view) @@ -136,7 +116,7 @@ impl TerminalContainer { TerminalContainer { content, - associated_directory: working_directory, + associated_directory: None, //working_directory, } } @@ -183,12 +163,7 @@ impl View for ErrorView { //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 program_text = format!("Shell Program: `{}`", self.error.shell_to_string()); let directory_text = { match self.error.directory.as_ref() { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index c2f5c5c11432af3dbdf5e0296bb18c8d22030a92..dbe861b7813e6ede5f702d7446bcffeac78cb81a 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1,3 +1,7 @@ +mod persistence; +pub mod terminal_container_view; +pub mod terminal_element; + use std::{ops::RangeInclusive, time::Duration}; use context_menu::{ContextMenu, ContextMenuItem}; @@ -52,6 +56,7 @@ impl_actions!(terminal, [SendText, SendKeystroke]); impl_internal_actions!(project_panel, [DeployContextMenu]); pub fn init(cx: &mut MutableAppContext) { + terminal_container_view::init(cx); //Useful terminal views cx.add_action(TerminalView::send_text); cx.add_action(TerminalView::send_keystroke); From da100a09fb1a6e0f9778dc49da7bfd4f3d0aa614 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 8 Dec 2022 19:05:26 -0800 Subject: [PATCH 5/6] WIP --- crates/project/src/project.rs | 2 +- .../src/terminal_container_view.rs | 21 ++++++++++++------- crates/terminal_view/src/terminal_element.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 4 ++-- crates/zed/src/main.rs | 8 +++++-- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 545570da8993c55433ca7cba1c1b0b687881aa02..9b4a163af4e6f78e397c730f444a4f697ece1e22 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1194,7 +1194,7 @@ impl Project { !self.is_local() } - pub fn create_terminal_connection( + pub fn create_terminal( &mut self, working_directory: Option, window_id: usize, diff --git a/crates/terminal_view/src/terminal_container_view.rs b/crates/terminal_view/src/terminal_container_view.rs index 322bf5ab527cd68b4a8525b7db0a1eca6b6a6c30..4a0d47794a3f5bb7cd45211424617fe6d408fd97 100644 --- a/crates/terminal_view/src/terminal_container_view.rs +++ b/crates/terminal_view/src/terminal_container_view.rs @@ -82,21 +82,22 @@ impl TerminalContainer { let working_directory = get_working_directory(workspace, cx, strategy); let window_id = cx.window_id(); + let project = workspace.project().clone(); let terminal = workspace.project().update(cx, |project, cx| { - project.create_terminal_connection(working_directory, window_id, cx) + project.create_terminal(working_directory, window_id, cx) }); let view = cx.add_view(|cx| TerminalContainer::new(terminal, 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 + ///Create a new Terminal view. pub fn new( - model: anyhow::Result>, + maybe_terminal: anyhow::Result>, workspace_id: WorkspaceId, cx: &mut ViewContext, ) -> Self { - let content = match model { + let content = match maybe_terminal { Ok(terminal) => { let item_id = cx.view_id(); let view = cx.add_view(|cx| { @@ -251,8 +252,7 @@ impl Item for TerminalContainer { //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, + Err(anyhow::anyhow!("failed to instantiate terminal")), workspace_id, cx, )) @@ -354,12 +354,13 @@ impl Item for TerminalContainer { } fn deserialize( - _project: ModelHandle, + project: ModelHandle, _workspace: WeakViewHandle, workspace_id: workspace::WorkspaceId, item_id: workspace::ItemId, cx: &mut ViewContext, ) -> Task>> { + let window_id = cx.window_id(); cx.spawn(|pane, mut cx| async move { let cwd = TERMINAL_DB .take_working_directory(item_id, workspace_id) @@ -368,8 +369,12 @@ impl Item for TerminalContainer { .flatten(); cx.update(|cx| { + let terminal = project.update(cx, |project, cx| { + project.create_terminal(cwd, window_id, cx) + }); + Ok(cx.add_view(pane, |cx| { - TerminalContainer::new(cwd, false, workspace_id, cx) + TerminalContainer::new(terminal, workspace_id, cx) })) }) }) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 506dd1423d58928ba3efbed87b5faf178cae2748..08ed3ecc2d1da04352dac73a9438a5f3087c8b74 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -32,7 +32,7 @@ use util::ResultExt; use std::{fmt::Debug, ops::RangeInclusive}; use std::{mem, ops::Range}; -use crate::terminal_view::{DeployContextMenu, TerminalView}; +use crate::{DeployContextMenu, TerminalView}; ///The information generated during layout that is nescessary for painting pub struct LayoutState { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index dbe861b7813e6ede5f702d7446bcffeac78cb81a..c2747e3ef25578c4d5f9ac9d3d425a34d9dd4f87 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -22,12 +22,12 @@ use terminal::{ index::Point, term::{search::RegexSearch, TermMode}, }, - Terminal, + Event, Terminal, }; use util::ResultExt; use workspace::{pane, ItemId, WorkspaceId}; -use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement, Event}; +use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 1b41613937f203588cad4dedc5faf55c6854cfc0..2396af6465d291587355b7c11a7657570fe2bb3e 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -32,7 +32,7 @@ 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_view::{get_working_directory, TerminalContainer}; +use terminal_view::terminal_container_view::{get_working_directory, TerminalContainer}; use fs::RealFs; use settings::watched_json::{watch_keymap_file, watch_settings_file, WatchedJsonFile}; @@ -595,7 +595,11 @@ 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) + TerminalContainer::new( + Err(anyhow!("Don't have a project to open a terminal")), + workspace.database_id(), + cx, + ) }); Box::new(terminal_handle) } From 925c9e13bbf24435e5fe51b6e8abc5cff581c218 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 8 Dec 2022 20:14:43 -0800 Subject: [PATCH 6/6] Remove terminal container view, switch to notify errors --- crates/collab/src/integration_tests.rs | 2 +- crates/collab_ui/src/collab_ui.rs | 2 +- .../src/terminal_container_view.rs | 771 ------------------ crates/terminal_view/src/terminal_view.rs | 620 +++++++++++++- crates/workspace/src/dock.rs | 23 +- crates/workspace/src/notifications.rs | 80 +- crates/workspace/src/workspace.rs | 12 +- crates/zed/src/main.rs | 31 +- 8 files changed, 700 insertions(+), 841 deletions(-) delete mode 100644 crates/terminal_view/src/terminal_container_view.rs diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 3639afd47c78899d9a98a24fd5ddcceb6e028a5c..a77ae4925d0d0f4fc5af80225385eb59ef2c9af5 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -6022,7 +6022,7 @@ impl TestServer { fs: fs.clone(), build_window_options: Default::default, initialize_workspace: |_, _, _| unimplemented!(), - default_item_factory: |_, _| unimplemented!(), + dock_default_item_factory: |_, _| unimplemented!(), }); Project::init(&client); diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index abc62605f93cc34046e3663e41c0e561f14b3cb6..1b851c3f7595e000e3242bec529b9ff4c8d7bc45 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -54,7 +54,7 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { Default::default(), 0, project, - app_state.default_item_factory, + app_state.dock_default_item_factory, cx, ); (app_state.initialize_workspace)(&mut workspace, &app_state, cx); diff --git a/crates/terminal_view/src/terminal_container_view.rs b/crates/terminal_view/src/terminal_container_view.rs deleted file mode 100644 index 4a0d47794a3f5bb7cd45211424617fe6d408fd97..0000000000000000000000000000000000000000 --- a/crates/terminal_view/src/terminal_container_view.rs +++ /dev/null @@ -1,771 +0,0 @@ -use crate::persistence::TERMINAL_DB; -use crate::TerminalView; -use terminal::alacritty_terminal::index::Point; -use terminal::{Event, Terminal, TerminalError}; - -use crate::regex_search_for_query; -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::{Settings, WorkingDirectory}; -use smallvec::SmallVec; -use std::ops::RangeInclusive; -use std::path::{Path, PathBuf}; - -use crate::terminal_element::TerminalElement; - -actions!(terminal, [DeployModal]); - -pub fn init(cx: &mut MutableAppContext) { - cx.add_action(TerminalContainer::deploy); - - register_deserializable_item::(cx); - - // terminal_view::init(cx); -} - -//Make terminal view an enum, that can give you views for the error and non-error states -//Take away all the result unwrapping in the current TerminalView by making it 'infallible' -//Bubble up to deploy(_modal)() calls - -pub enum TerminalContainerContent { - Connected(ViewHandle), - Error(ViewHandle), -} - -impl TerminalContainerContent { - fn handle(&self) -> AnyViewHandle { - match self { - Self::Connected(handle) => handle.into(), - Self::Error(handle) => handle.into(), - } - } -} - -pub struct TerminalContainer { - pub content: TerminalContainerContent, - associated_directory: Option, -} - -pub struct ErrorView { - error: TerminalError, -} - -impl Entity for TerminalContainer { - type Event = Event; -} - -impl Entity for ErrorView { - type Event = Event; -} - -impl TerminalContainer { - ///Create a new Terminal in the current working directory or the user's home directory - pub fn deploy( - workspace: &mut Workspace, - _: &workspace::NewTerminal, - cx: &mut ViewContext, - ) { - let strategy = cx.global::().terminal_strategy(); - - let working_directory = get_working_directory(workspace, cx, strategy); - - let window_id = cx.window_id(); - let project = workspace.project().clone(); - let terminal = workspace.project().update(cx, |project, cx| { - project.create_terminal(working_directory, window_id, cx) - }); - - let view = cx.add_view(|cx| TerminalContainer::new(terminal, workspace.database_id(), cx)); - workspace.add_item(Box::new(view), cx); - } - - ///Create a new Terminal view. - pub fn new( - maybe_terminal: anyhow::Result>, - workspace_id: WorkspaceId, - cx: &mut ViewContext, - ) -> Self { - let content = match maybe_terminal { - Ok(terminal) => { - let item_id = cx.view_id(); - let view = cx.add_view(|cx| { - TerminalView::from_terminal(terminal, false, workspace_id, item_id, cx) - }); - cx.subscribe(&view, |_this, _content, event, cx| cx.emit(*event)) - .detach(); - TerminalContainerContent::Connected(view) - } - Err(error) => { - let view = cx.add_view(|_| ErrorView { - error: error.downcast::().unwrap(), - }); - TerminalContainerContent::Error(view) - } - }; - - TerminalContainer { - content, - associated_directory: None, //working_directory, - } - } - - fn connected(&self) -> Option> { - match &self.content { - TerminalContainerContent::Connected(vh) => Some(vh.clone()), - TerminalContainerContent::Error(_) => None, - } - } -} - -impl View for TerminalContainer { - fn ui_name() -> &'static str { - "Terminal" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - match &self.content { - TerminalContainerContent::Connected(connected) => ChildView::new(connected, cx), - TerminalContainerContent::Error(error) => ChildView::new(error, cx), - } - .boxed() - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - cx.focus(self.content.handle()); - } - } -} - -impl View for ErrorView { - fn ui_name() -> &'static str { - "Terminal Error" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let settings = cx.global::(); - let style = TerminalElement::make_text_style(cx.font_cache(), settings); - - //TODO: - //We want markdown style highlighting so we can format the program and working directory with `` - //We want a max-width of 75% with word-wrap - //We want to be able to select the text - //Want to be able to scroll if the error message is massive somehow (resiliency) - - let program_text = format!("Shell Program: `{}`", self.error.shell_to_string()); - - let directory_text = { - match self.error.directory.as_ref() { - Some(path) => format!("Working directory: `{}`", path.to_string_lossy()), - None => "No working directory specified".to_string(), - } - }; - - let error_text = self.error.source.to_string(); - - Flex::column() - .with_child( - Text::new("Failed to open the terminal.".to_string(), style.clone()) - .contained() - .boxed(), - ) - .with_child(Text::new(program_text, style.clone()).contained().boxed()) - .with_child(Text::new(directory_text, style.clone()).contained().boxed()) - .with_child(Text::new(error_text, style).contained().boxed()) - .aligned() - .boxed() - } -} - -impl Item for TerminalContainer { - fn tab_content( - &self, - _detail: Option, - tab_theme: &theme::Tab, - cx: &gpui::AppContext, - ) -> ElementBox { - let title = match &self.content { - TerminalContainerContent::Connected(connected) => connected - .read(cx) - .handle() - .read(cx) - .foreground_process_info - .as_ref() - .map(|fpi| { - format!( - "{} — {}", - truncate_and_trailoff( - &fpi.cwd - .file_name() - .map(|name| name.to_string_lossy().to_string()) - .unwrap_or_default(), - 25 - ), - truncate_and_trailoff( - &{ - format!( - "{}{}", - fpi.name, - if fpi.argv.len() >= 1 { - format!(" {}", (&fpi.argv[1..]).join(" ")) - } else { - "".to_string() - } - ) - }, - 25 - ) - ) - }) - .unwrap_or_else(|| "Terminal".to_string()), - TerminalContainerContent::Error(_) => "Terminal".to_string(), - }; - - Flex::row() - .with_child( - Label::new(title, tab_theme.label.clone()) - .aligned() - .contained() - .boxed(), - ) - .boxed() - } - - fn clone_on_split( - &self, - workspace_id: WorkspaceId, - cx: &mut ViewContext, - ) -> Option { - //From what I can tell, there's no way to tell the current working - //Directory of the terminal from outside the shell. There might be - //solutions to this, but they are non-trivial and require more IPC - Some(TerminalContainer::new( - Err(anyhow::anyhow!("failed to instantiate terminal")), - workspace_id, - cx, - )) - } - - fn project_path(&self, _cx: &gpui::AppContext) -> Option { - None - } - - fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { - SmallVec::new() - } - - fn is_singleton(&self, _cx: &gpui::AppContext) -> bool { - false - } - - fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} - - fn can_save(&self, _cx: &gpui::AppContext) -> bool { - false - } - - fn save( - &mut self, - _project: gpui::ModelHandle, - _cx: &mut ViewContext, - ) -> gpui::Task> { - unreachable!("save should not have been called"); - } - - fn save_as( - &mut self, - _project: gpui::ModelHandle, - _abs_path: std::path::PathBuf, - _cx: &mut ViewContext, - ) -> gpui::Task> { - unreachable!("save_as should not have been called"); - } - - fn reload( - &mut self, - _project: gpui::ModelHandle, - _cx: &mut ViewContext, - ) -> gpui::Task> { - gpui::Task::ready(Ok(())) - } - - fn is_dirty(&self, cx: &gpui::AppContext) -> bool { - if let TerminalContainerContent::Connected(connected) = &self.content { - connected.read(cx).has_bell() - } else { - false - } - } - - fn has_conflict(&self, _cx: &AppContext) -> bool { - false - } - - fn as_searchable(&self, handle: &ViewHandle) -> Option> { - Some(Box::new(handle.clone())) - } - - fn to_item_events(event: &Self::Event) -> Vec { - match event { - Event::BreadcrumbsChanged => vec![ItemEvent::UpdateBreadcrumbs], - Event::TitleChanged | Event::Wakeup => vec![ItemEvent::UpdateTab], - Event::CloseTerminal => vec![ItemEvent::CloseItem], - _ => vec![], - } - } - - fn breadcrumb_location(&self) -> ToolbarItemLocation { - if self.connected().is_some() { - ToolbarItemLocation::PrimaryLeft { flex: None } - } else { - ToolbarItemLocation::Hidden - } - } - - fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { - let connected = self.connected()?; - - Some(vec![Text::new( - connected - .read(cx) - .terminal() - .read(cx) - .breadcrumb_text - .to_string(), - theme.breadcrumbs.text.clone(), - ) - .boxed()]) - } - - fn serialized_item_kind() -> Option<&'static str> { - Some("Terminal") - } - - fn deserialize( - project: ModelHandle, - _workspace: WeakViewHandle, - workspace_id: workspace::WorkspaceId, - item_id: workspace::ItemId, - cx: &mut ViewContext, - ) -> Task>> { - let 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| { - TerminalContainer::new(terminal, workspace_id, cx) - })) - }) - }) - } - - fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { - if let Some(connected) = self.connected() { - connected.update(cx, |connected_view, cx| { - connected_view.added_to_workspace(workspace.database_id(), cx); - }) - } - } -} - -impl SearchableItem for TerminalContainer { - type Match = RangeInclusive; - - fn supported_options() -> SearchOptions { - SearchOptions { - case: false, - word: false, - regex: false, - } - } - - /// Convert events raised by this item into search-relevant events (if applicable) - fn to_search_event(event: &Self::Event) -> Option { - match event { - Event::Wakeup => Some(SearchEvent::MatchesInvalidated), - Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged), - _ => None, - } - } - - /// Clear stored matches - fn clear_matches(&mut self, cx: &mut ViewContext) { - if let TerminalContainerContent::Connected(connected) = &self.content { - let terminal = connected.read(cx).terminal().clone(); - terminal.update(cx, |term, _| term.matches.clear()) - } - } - - /// Store matches returned from find_matches somewhere for rendering - fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext) { - if let TerminalContainerContent::Connected(connected) = &self.content { - let terminal = connected.read(cx).terminal().clone(); - terminal.update(cx, |term, _| term.matches = matches) - } - } - - /// Return the selection content to pre-load into this search - fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { - if let TerminalContainerContent::Connected(connected) = &self.content { - let terminal = connected.read(cx).terminal().clone(); - terminal - .read(cx) - .last_content - .selection_text - .clone() - .unwrap_or_default() - } else { - Default::default() - } - } - - /// Focus match at given index into the Vec of matches - fn activate_match(&mut self, index: usize, _: Vec, cx: &mut ViewContext) { - if let TerminalContainerContent::Connected(connected) = &self.content { - let terminal = connected.read(cx).terminal().clone(); - terminal.update(cx, |term, _| term.activate_match(index)); - cx.notify(); - } - } - - /// Get all of the matches for this query, should be done on the background - fn find_matches( - &mut self, - query: project::search::SearchQuery, - cx: &mut ViewContext, - ) -> Task> { - if let TerminalContainerContent::Connected(connected) = &self.content { - let terminal = connected.read(cx).terminal().clone(); - if let Some(searcher) = regex_search_for_query(query) { - terminal.update(cx, |term, cx| term.find_matches(searcher, cx)) - } else { - cx.background().spawn(async { Vec::new() }) - } - } else { - Task::ready(Vec::new()) - } - } - - /// Reports back to the search toolbar what the active match should be (the selection) - fn active_match_index( - &mut self, - matches: Vec, - cx: &mut ViewContext, - ) -> Option { - let connected = self.connected(); - // Selection head might have a value if there's a selection that isn't - // associated with a match. Therefore, if there are no matches, we should - // report None, no matter the state of the terminal - let res = if matches.len() > 0 && connected.is_some() { - if let Some(selection_head) = connected - .unwrap() - .read(cx) - .terminal() - .read(cx) - .selection_head - { - // If selection head is contained in a match. Return that match - if let Some(ix) = matches - .iter() - .enumerate() - .find(|(_, search_match)| { - search_match.contains(&selection_head) - || search_match.start() > &selection_head - }) - .map(|(ix, _)| ix) - { - Some(ix) - } else { - // If no selection after selection head, return the last match - Some(matches.len().saturating_sub(1)) - } - } else { - // Matches found but no active selection, return the first last one (closest to cursor) - Some(matches.len().saturating_sub(1)) - } - } else { - None - }; - - res - } -} - -///Get's the working directory for the given workspace, respecting the user's settings. -pub fn get_working_directory( - workspace: &Workspace, - cx: &AppContext, - strategy: WorkingDirectory, -) -> Option { - let res = match strategy { - WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx) - .or_else(|| first_project_directory(workspace, cx)), - WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), - WorkingDirectory::AlwaysHome => None, - WorkingDirectory::Always { directory } => { - shellexpand::full(&directory) //TODO handle this better - .ok() - .map(|dir| Path::new(&dir.to_string()).to_path_buf()) - .filter(|dir| dir.is_dir()) - } - }; - res.or_else(home_dir) -} - -///Get's the first project's home directory, or the home directory -fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { - workspace - .worktrees(cx) - .next() - .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) - .and_then(get_path_from_wt) -} - -///Gets the intuitively correct working directory from the given workspace -///If there is an active entry for this project, returns that entry's worktree root. -///If there's no active entry but there is a worktree, returns that worktrees root. -///If either of these roots are files, or if there are any other query failures, -/// returns the user's home directory -fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { - let project = workspace.project().read(cx); - - project - .active_entry() - .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) - .or_else(|| workspace.worktrees(cx).next()) - .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) - .and_then(get_path_from_wt) -} - -fn get_path_from_wt(wt: &LocalWorktree) -> Option { - wt.root_entry() - .filter(|re| re.is_dir()) - .map(|_| wt.abs_path().to_path_buf()) -} - -#[cfg(test)] -mod tests { - - use super::*; - use gpui::TestAppContext; - use project::{Entry, Worktree}; - use workspace::AppState; - - use std::path::Path; - - ///Working directory calculation tests - - ///No Worktrees in project -> home_dir() - #[gpui::test] - async fn no_worktree(cx: &mut TestAppContext) { - //Setup variables - let (project, workspace) = blank_workspace(cx).await; - //Test - cx.read(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - //Make sure enviroment is as expeted - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_none()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, None); - }); - } - - ///No active entry, but a worktree, worktree is a file -> home_dir() - #[gpui::test] - async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) { - //Setup variables - - let (project, workspace) = blank_workspace(cx).await; - create_file_wt(project.clone(), "/root.txt", cx).await; - - cx.read(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - //Make sure enviroment is as expeted - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, None); - }); - } - - //No active entry, but a worktree, worktree is a folder -> worktree_folder - #[gpui::test] - async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) { - //Setup variables - let (project, workspace) = blank_workspace(cx).await; - let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await; - - //Test - cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); - }); - } - - //Active entry with a work tree, worktree is a file -> home_dir() - #[gpui::test] - async fn active_entry_worktree_is_file(cx: &mut TestAppContext) { - //Setup variables - - let (project, workspace) = blank_workspace(cx).await; - let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await; - let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await; - insert_active_entry_for(wt2, entry2, project.clone(), cx); - - //Test - cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - }); - } - - //Active entry, with a worktree, worktree is a folder -> worktree_folder - #[gpui::test] - async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) { - //Setup variables - let (project, workspace) = blank_workspace(cx).await; - let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await; - let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await; - insert_active_entry_for(wt2, entry2, project.clone(), cx); - - //Test - cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root2/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - }); - } - - ///Creates a worktree with 1 file: /root.txt - pub async fn blank_workspace( - cx: &mut TestAppContext, - ) -> (ModelHandle, ViewHandle) { - let params = cx.update(AppState::test); - - let project = Project::test(params.fs.clone(), [], cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new( - Default::default(), - 0, - project.clone(), - |_, _| unimplemented!(), - cx, - ) - }); - - (project, workspace) - } - - ///Creates a worktree with 1 folder: /root{suffix}/ - async fn create_folder_wt( - project: ModelHandle, - path: impl AsRef, - cx: &mut TestAppContext, - ) -> (ModelHandle, Entry) { - create_wt(project, true, path, cx).await - } - - ///Creates a worktree with 1 file: /root{suffix}.txt - async fn create_file_wt( - project: ModelHandle, - path: impl AsRef, - cx: &mut TestAppContext, - ) -> (ModelHandle, Entry) { - create_wt(project, false, path, cx).await - } - - async fn create_wt( - project: ModelHandle, - is_dir: bool, - path: impl AsRef, - cx: &mut TestAppContext, - ) -> (ModelHandle, Entry) { - let (wt, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree(path, true, cx) - }) - .await - .unwrap(); - - let entry = cx - .update(|cx| { - wt.update(cx, |wt, cx| { - wt.as_local() - .unwrap() - .create_entry(Path::new(""), is_dir, cx) - }) - }) - .await - .unwrap(); - - (wt, entry) - } - - pub fn insert_active_entry_for( - wt: ModelHandle, - entry: Entry, - project: ModelHandle, - cx: &mut TestAppContext, - ) { - cx.update(|cx| { - let p = ProjectPath { - worktree_id: wt.read(cx).id(), - path: entry.path, - }; - project.update(cx, |project, cx| project.set_active_path(Some(p), cx)); - }); - } -} diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index c2747e3ef25578c4d5f9ac9d3d425a34d9dd4f87..7602a3db2223651d95b03ded49936da1d273888c 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1,21 +1,27 @@ mod persistence; -pub mod terminal_container_view; pub mod terminal_element; -use std::{ops::RangeInclusive, time::Duration}; +use std::{ + ops::RangeInclusive, + path::{Path, PathBuf}, + time::Duration, +}; use context_menu::{ContextMenu, ContextMenuItem}; +use dirs::home_dir; use gpui::{ actions, - elements::{AnchorCorner, ChildView, ParentElement, Stack}, + 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, + View, ViewContext, ViewHandle, WeakViewHandle, }; +use project::{LocalWorktree, Project, ProjectPath}; use serde::Deserialize; -use settings::{Settings, TerminalBlink}; +use settings::{Settings, TerminalBlink, WorkingDirectory}; +use smallvec::SmallVec; use smol::Timer; use terminal::{ alacritty_terminal::{ @@ -24,8 +30,14 @@ use terminal::{ }, Event, Terminal, }; -use util::ResultExt; -use workspace::{pane, ItemId, WorkspaceId}; +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}; @@ -56,7 +68,10 @@ impl_actions!(terminal, [SendText, SendKeystroke]); impl_internal_actions!(project_panel, [DeployContextMenu]); pub fn init(cx: &mut MutableAppContext) { - terminal_container_view::init(cx); + cx.add_action(TerminalView::deploy); + + register_deserializable_item::(cx); + //Useful terminal views cx.add_action(TerminalView::send_text); cx.add_action(TerminalView::send_keystroke); @@ -73,15 +88,12 @@ pub struct TerminalView { has_new_content: bool, //Currently using iTerm bell, show bell emoji in tab until input is received has_bell: bool, - // Only for styling purposes. Doesn't effect behavior - modal: bool, context_menu: ViewHandle, blink_state: bool, blinking_on: bool, blinking_paused: bool, blink_epoch: usize, workspace_id: WorkspaceId, - item_id: ItemId, } impl Entity for TerminalView { @@ -89,11 +101,33 @@ impl Entity for TerminalView { } impl TerminalView { - pub fn from_terminal( + ///Create a new Terminal in the current working directory or the user's home directory + pub fn deploy( + workspace: &mut Workspace, + _: &workspace::NewTerminal, + cx: &mut ViewContext, + ) { + let strategy = cx.global::().terminal_strategy(); + + let working_directory = get_working_directory(workspace, cx, strategy); + + let window_id = cx.window_id(); + let terminal = workspace + .project() + .update(cx, |project, cx| { + project.create_terminal(working_directory, window_id, cx) + }) + .notify_err(workspace, cx); + + if let Some(terminal) = terminal { + let view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx)); + workspace.add_item(Box::new(view), cx) + } + } + + pub fn new( terminal: ModelHandle, - modal: bool, workspace_id: WorkspaceId, - item_id: ItemId, cx: &mut ViewContext, ) -> Self { cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); @@ -114,7 +148,7 @@ impl TerminalView { if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info { let cwd = foreground_info.cwd.clone(); - let item_id = this.item_id; + let item_id = cx.view_id(); let workspace_id = this.workspace_id; cx.background() .spawn(async move { @@ -134,14 +168,12 @@ impl TerminalView { 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, workspace_id, - item_id, } } @@ -293,13 +325,6 @@ impl TerminalView { &self.terminal } - pub fn added_to_workspace(&mut self, new_id: WorkspaceId, cx: &mut ViewContext) { - cx.background() - .spawn(TERMINAL_DB.update_workspace_id(new_id, self.workspace_id, self.item_id)) - .detach(); - self.workspace_id = new_id; - } - fn next_blink_epoch(&mut self) -> usize { self.blink_epoch += 1; self.blink_epoch @@ -442,9 +467,7 @@ impl View for TerminalView { 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(), @@ -523,3 +546,546 @@ impl View for TerminalView { context } } + +impl Item for TerminalView { + fn tab_content( + &self, + _detail: Option, + tab_theme: &theme::Tab, + cx: &gpui::AppContext, + ) -> ElementBox { + let title = self + .terminal() + .read(cx) + .foreground_process_info + .as_ref() + .map(|fpi| { + format!( + "{} — {}", + truncate_and_trailoff( + &fpi.cwd + .file_name() + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or_default(), + 25 + ), + truncate_and_trailoff( + &{ + format!( + "{}{}", + fpi.name, + if fpi.argv.len() >= 1 { + format!(" {}", (&fpi.argv[1..]).join(" ")) + } else { + "".to_string() + } + ) + }, + 25 + ) + ) + }) + .unwrap_or_else(|| "Terminal".to_string()); + + Flex::row() + .with_child( + Label::new(title, tab_theme.label.clone()) + .aligned() + .contained() + .boxed(), + ) + .boxed() + } + + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + _cx: &mut ViewContext, + ) -> Option { + //From what I can tell, there's no way to tell the current working + //Directory of the terminal from outside the shell. There might be + //solutions to this, but they are non-trivial and require more IPC + + // Some(TerminalContainer::new( + // Err(anyhow::anyhow!("failed to instantiate terminal")), + // workspace_id, + // cx, + // )) + + // TODO + None + } + + fn project_path(&self, _cx: &gpui::AppContext) -> Option { + None + } + + fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { + SmallVec::new() + } + + fn is_singleton(&self, _cx: &gpui::AppContext) -> bool { + false + } + + fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} + + fn can_save(&self, _cx: &gpui::AppContext) -> bool { + false + } + + fn save( + &mut self, + _project: gpui::ModelHandle, + _cx: &mut ViewContext, + ) -> gpui::Task> { + unreachable!("save should not have been called"); + } + + fn save_as( + &mut self, + _project: gpui::ModelHandle, + _abs_path: std::path::PathBuf, + _cx: &mut ViewContext, + ) -> gpui::Task> { + unreachable!("save_as should not have been called"); + } + + fn reload( + &mut self, + _project: gpui::ModelHandle, + _cx: &mut ViewContext, + ) -> gpui::Task> { + gpui::Task::ready(Ok(())) + } + + fn is_dirty(&self, _cx: &gpui::AppContext) -> bool { + self.has_bell() + } + + fn has_conflict(&self, _cx: &AppContext) -> bool { + false + } + + fn as_searchable(&self, handle: &ViewHandle) -> Option> { + Some(Box::new(handle.clone())) + } + + fn to_item_events(event: &Self::Event) -> Vec { + match event { + Event::BreadcrumbsChanged => vec![ItemEvent::UpdateBreadcrumbs], + Event::TitleChanged | Event::Wakeup => vec![ItemEvent::UpdateTab], + Event::CloseTerminal => vec![ItemEvent::CloseItem], + _ => vec![], + } + } + + fn breadcrumb_location(&self) -> ToolbarItemLocation { + ToolbarItemLocation::PrimaryLeft { flex: None } + } + + fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { + Some(vec![Text::new( + self.terminal().read(cx).breadcrumb_text.to_string(), + theme.breadcrumbs.text.clone(), + ) + .boxed()]) + } + + fn serialized_item_kind() -> Option<&'static str> { + Some("Terminal") + } + + fn deserialize( + project: ModelHandle, + _workspace: WeakViewHandle, + workspace_id: workspace::WorkspaceId, + item_id: workspace::ItemId, + cx: &mut ViewContext, + ) -> Task>> { + let window_id = cx.window_id(); + cx.spawn(|pane, mut cx| async move { + let cwd = TERMINAL_DB + .take_working_directory(item_id, workspace_id) + .await + .log_err() + .flatten(); + + cx.update(|cx| { + let terminal = project.update(cx, |project, cx| { + project.create_terminal(cwd, window_id, cx) + })?; + + Ok(cx.add_view(pane, |cx| TerminalView::new(terminal, workspace_id, cx))) + }) + }) + } + + fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { + cx.background() + .spawn(TERMINAL_DB.update_workspace_id( + workspace.database_id(), + self.workspace_id, + cx.view_id(), + )) + .detach(); + self.workspace_id = workspace.database_id(); + } +} + +impl SearchableItem for TerminalView { + type Match = RangeInclusive; + + fn supported_options() -> SearchOptions { + SearchOptions { + case: false, + word: false, + regex: false, + } + } + + /// Convert events raised by this item into search-relevant events (if applicable) + fn to_search_event(event: &Self::Event) -> Option { + match event { + Event::Wakeup => Some(SearchEvent::MatchesInvalidated), + Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged), + _ => None, + } + } + + /// Clear stored matches + fn clear_matches(&mut self, cx: &mut ViewContext) { + self.terminal().update(cx, |term, _| term.matches.clear()) + } + + /// Store matches returned from find_matches somewhere for rendering + fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext) { + self.terminal().update(cx, |term, _| term.matches = matches) + } + + /// Return the selection content to pre-load into this search + fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { + self.terminal() + .read(cx) + .last_content + .selection_text + .clone() + .unwrap_or_default() + } + + /// Focus match at given index into the Vec of matches + fn activate_match(&mut self, index: usize, _: Vec, cx: &mut ViewContext) { + self.terminal() + .update(cx, |term, _| term.activate_match(index)); + cx.notify(); + } + + /// Get all of the matches for this query, should be done on the background + fn find_matches( + &mut self, + query: project::search::SearchQuery, + cx: &mut ViewContext, + ) -> Task> { + if let Some(searcher) = regex_search_for_query(query) { + self.terminal() + .update(cx, |term, cx| term.find_matches(searcher, cx)) + } else { + Task::ready(vec![]) + } + } + + /// Reports back to the search toolbar what the active match should be (the selection) + fn active_match_index( + &mut self, + matches: Vec, + cx: &mut ViewContext, + ) -> Option { + // Selection head might have a value if there's a selection that isn't + // associated with a match. Therefore, if there are no matches, we should + // report None, no matter the state of the terminal + let res = if matches.len() > 0 { + if let Some(selection_head) = self.terminal().read(cx).selection_head { + // If selection head is contained in a match. Return that match + if let Some(ix) = matches + .iter() + .enumerate() + .find(|(_, search_match)| { + search_match.contains(&selection_head) + || search_match.start() > &selection_head + }) + .map(|(ix, _)| ix) + { + Some(ix) + } else { + // If no selection after selection head, return the last match + Some(matches.len().saturating_sub(1)) + } + } else { + // Matches found but no active selection, return the first last one (closest to cursor) + Some(matches.len().saturating_sub(1)) + } + } else { + None + }; + + res + } +} + +///Get's the working directory for the given workspace, respecting the user's settings. +pub fn get_working_directory( + workspace: &Workspace, + cx: &AppContext, + strategy: WorkingDirectory, +) -> Option { + let res = match strategy { + WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx) + .or_else(|| first_project_directory(workspace, cx)), + WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), + WorkingDirectory::AlwaysHome => None, + WorkingDirectory::Always { directory } => { + shellexpand::full(&directory) //TODO handle this better + .ok() + .map(|dir| Path::new(&dir.to_string()).to_path_buf()) + .filter(|dir| dir.is_dir()) + } + }; + res.or_else(home_dir) +} + +///Get's the first project's home directory, or the home directory +fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { + workspace + .worktrees(cx) + .next() + .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) + .and_then(get_path_from_wt) +} + +///Gets the intuitively correct working directory from the given workspace +///If there is an active entry for this project, returns that entry's worktree root. +///If there's no active entry but there is a worktree, returns that worktrees root. +///If either of these roots are files, or if there are any other query failures, +/// returns the user's home directory +fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { + let project = workspace.project().read(cx); + + project + .active_entry() + .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) + .or_else(|| workspace.worktrees(cx).next()) + .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) + .and_then(get_path_from_wt) +} + +fn get_path_from_wt(wt: &LocalWorktree) -> Option { + wt.root_entry() + .filter(|re| re.is_dir()) + .map(|_| wt.abs_path().to_path_buf()) +} + +#[cfg(test)] +mod tests { + + use super::*; + use gpui::TestAppContext; + use project::{Entry, Project, ProjectPath, Worktree}; + use workspace::AppState; + + use std::path::Path; + + ///Working directory calculation tests + + ///No Worktrees in project -> home_dir() + #[gpui::test] + async fn no_worktree(cx: &mut TestAppContext) { + //Setup variables + let (project, workspace) = blank_workspace(cx).await; + //Test + cx.read(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + //Make sure enviroment is as expeted + assert!(active_entry.is_none()); + assert!(workspace.worktrees(cx).next().is_none()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, None); + let res = first_project_directory(workspace, cx); + assert_eq!(res, None); + }); + } + + ///No active entry, but a worktree, worktree is a file -> home_dir() + #[gpui::test] + async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) { + //Setup variables + + let (project, workspace) = blank_workspace(cx).await; + create_file_wt(project.clone(), "/root.txt", cx).await; + + cx.read(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + //Make sure enviroment is as expeted + assert!(active_entry.is_none()); + assert!(workspace.worktrees(cx).next().is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, None); + let res = first_project_directory(workspace, cx); + assert_eq!(res, None); + }); + } + + //No active entry, but a worktree, worktree is a folder -> worktree_folder + #[gpui::test] + async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) { + //Setup variables + let (project, workspace) = blank_workspace(cx).await; + let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await; + + //Test + cx.update(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + assert!(active_entry.is_none()); + assert!(workspace.worktrees(cx).next().is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); + let res = first_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); + }); + } + + //Active entry with a work tree, worktree is a file -> home_dir() + #[gpui::test] + async fn active_entry_worktree_is_file(cx: &mut TestAppContext) { + //Setup variables + + let (project, workspace) = blank_workspace(cx).await; + let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await; + let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await; + insert_active_entry_for(wt2, entry2, project.clone(), cx); + + //Test + cx.update(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + assert!(active_entry.is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, None); + let res = first_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); + }); + } + + //Active entry, with a worktree, worktree is a folder -> worktree_folder + #[gpui::test] + async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) { + //Setup variables + let (project, workspace) = blank_workspace(cx).await; + let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await; + let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await; + insert_active_entry_for(wt2, entry2, project.clone(), cx); + + //Test + cx.update(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + assert!(active_entry.is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root2/")).to_path_buf())); + let res = first_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); + }); + } + + ///Creates a worktree with 1 file: /root.txt + pub async fn blank_workspace( + cx: &mut TestAppContext, + ) -> (ModelHandle, ViewHandle) { + let params = cx.update(AppState::test); + + let project = Project::test(params.fs.clone(), [], cx).await; + let (_, workspace) = cx.add_window(|cx| { + Workspace::new( + Default::default(), + 0, + project.clone(), + |_, _| unimplemented!(), + cx, + ) + }); + + (project, workspace) + } + + ///Creates a worktree with 1 folder: /root{suffix}/ + async fn create_folder_wt( + project: ModelHandle, + path: impl AsRef, + cx: &mut TestAppContext, + ) -> (ModelHandle, Entry) { + create_wt(project, true, path, cx).await + } + + ///Creates a worktree with 1 file: /root{suffix}.txt + async fn create_file_wt( + project: ModelHandle, + path: impl AsRef, + cx: &mut TestAppContext, + ) -> (ModelHandle, Entry) { + create_wt(project, false, path, cx).await + } + + async fn create_wt( + project: ModelHandle, + is_dir: bool, + path: impl AsRef, + cx: &mut TestAppContext, + ) -> (ModelHandle, Entry) { + let (wt, _) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree(path, true, cx) + }) + .await + .unwrap(); + + let entry = cx + .update(|cx| { + wt.update(cx, |wt, cx| { + wt.as_local() + .unwrap() + .create_entry(Path::new(""), is_dir, cx) + }) + }) + .await + .unwrap(); + + (wt, entry) + } + + pub fn insert_active_entry_for( + wt: ModelHandle, + entry: Entry, + project: ModelHandle, + cx: &mut TestAppContext, + ) { + cx.update(|cx| { + let p = ProjectPath { + worktree_id: wt.read(cx).id(), + path: entry.path, + }; + project.update(cx, |project, cx| project.set_active_path(Some(p), cx)); + }); + } +} diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 0879166bbe733faf5b9ee0e86695cf3bfe391e39..78ee56f18882e336e5584af7baaf683dacbe7448 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -126,18 +126,21 @@ impl DockPosition { } } -pub type DefaultItemFactory = - fn(&mut Workspace, &mut ViewContext) -> Box; +pub type DockDefaultItemFactory = + fn(workspace: &mut Workspace, cx: &mut ViewContext) -> Option>; pub struct Dock { position: DockPosition, panel_sizes: HashMap, pane: ViewHandle, - default_item_factory: DefaultItemFactory, + default_item_factory: DockDefaultItemFactory, } impl Dock { - pub fn new(default_item_factory: DefaultItemFactory, cx: &mut ViewContext) -> Self { + pub fn new( + default_item_factory: DockDefaultItemFactory, + cx: &mut ViewContext, + ) -> Self { let position = DockPosition::Hidden(cx.global::().default_dock_anchor); let pane = cx.add_view(|cx| Pane::new(Some(position.anchor()), cx)); @@ -192,9 +195,11 @@ impl Dock { // Ensure that the pane has at least one item or construct a default item to put in it let pane = workspace.dock.pane.clone(); if pane.read(cx).items().next().is_none() { - let item_to_add = (workspace.dock.default_item_factory)(workspace, cx); - // Adding the item focuses the pane by default - Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx); + if let Some(item_to_add) = (workspace.dock.default_item_factory)(workspace, cx) { + Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx); + } else { + workspace.dock.position = workspace.dock.position.hide(); + } } else { cx.focus(pane); } @@ -465,8 +470,8 @@ mod tests { pub fn default_item_factory( _workspace: &mut Workspace, cx: &mut ViewContext, - ) -> Box { - Box::new(cx.add_view(|_| TestItem::new())) + ) -> Option> { + Some(Box::new(cx.add_view(|_| TestItem::new()))) } #[gpui::test] diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 91656727d0efc6f161b24490f29eb99c326a8d94..0e76d45518d71d13039f1f378418bfedec7a9f50 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -161,8 +161,8 @@ pub mod simple_message_notification { pub struct MessageNotification { message: String, - click_action: Box, - click_message: String, + click_action: Option>, + click_message: Option, } pub enum MessageNotificationEvent { @@ -174,6 +174,14 @@ pub mod simple_message_notification { } impl MessageNotification { + pub fn new_messsage>(message: S) -> MessageNotification { + Self { + message: message.as_ref().to_string(), + click_action: None, + click_message: None, + } + } + pub fn new, A: Action, S2: AsRef>( message: S1, click_action: A, @@ -181,8 +189,8 @@ pub mod simple_message_notification { ) -> Self { Self { message: message.as_ref().to_string(), - click_action: Box::new(click_action) as Box, - click_message: click_message.as_ref().to_string(), + click_action: Some(Box::new(click_action) as Box), + click_message: Some(click_message.as_ref().to_string()), } } @@ -202,8 +210,11 @@ pub mod simple_message_notification { enum MessageNotificationTag {} - let click_action = self.click_action.boxed_clone(); - let click_message = self.click_message.clone(); + let click_action = self + .click_action + .as_ref() + .map(|action| action.boxed_clone()); + let click_message = self.click_message.as_ref().map(|message| message.clone()); let message = self.message.clone(); MouseEventHandler::::new(0, cx, |state, cx| { @@ -251,20 +262,28 @@ pub mod simple_message_notification { ) .boxed(), ) - .with_child({ + .with_children({ let style = theme.action_message.style_for(state, false); - - Text::new(click_message, style.text.clone()) - .contained() - .with_style(style.container) - .boxed() + if let Some(click_message) = click_message { + Some( + Text::new(click_message, style.text.clone()) + .contained() + .with_style(style.container) + .boxed(), + ) + } else { + None + } + .into_iter() }) .contained() .boxed() }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_any_action(click_action.boxed_clone()) + if let Some(click_action) = click_action.as_ref() { + cx.dispatch_any_action(click_action.boxed_clone()) + } }) .boxed() } @@ -278,3 +297,38 @@ pub mod simple_message_notification { } } } + +pub trait NotifyResultExt { + type Ok; + + fn notify_err( + self, + workspace: &mut Workspace, + cx: &mut ViewContext, + ) -> Option; +} + +impl NotifyResultExt for Result +where + E: std::fmt::Debug, +{ + type Ok = T; + + fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext) -> Option { + match self { + Ok(value) => Some(value), + Err(err) => { + workspace.show_notification(0, cx, |cx| { + cx.add_view(|_cx| { + simple_message_notification::MessageNotification::new_messsage(format!( + "Error: {:?}", + err, + )) + }) + }); + + None + } + } + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a0c353b3f808bf1f1a5c9a9909f2047139916449..d38cf96ed29ae312475a303bbb151bb155733466 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -27,7 +27,7 @@ use anyhow::{anyhow, Context, Result}; use call::ActiveCall; use client::{proto, Client, PeerId, TypedEnvelope, UserStore}; use collections::{hash_map, HashMap, HashSet}; -use dock::{DefaultItemFactory, Dock, ToggleDockButton}; +use dock::{Dock, DockDefaultItemFactory, ToggleDockButton}; use drag_and_drop::DragAndDrop; use fs::{self, Fs}; use futures::{channel::oneshot, FutureExt, StreamExt}; @@ -375,7 +375,7 @@ pub struct AppState { pub fs: Arc, pub build_window_options: fn() -> WindowOptions<'static>, pub initialize_workspace: fn(&mut Workspace, &Arc, &mut ViewContext), - pub default_item_factory: DefaultItemFactory, + pub dock_default_item_factory: DockDefaultItemFactory, } impl AppState { @@ -401,7 +401,7 @@ impl AppState { user_store, initialize_workspace: |_, _, _| {}, build_window_options: Default::default, - default_item_factory: |_, _| unimplemented!(), + dock_default_item_factory: |_, _| unimplemented!(), }) } } @@ -515,7 +515,7 @@ impl Workspace { serialized_workspace: Option, workspace_id: WorkspaceId, project: ModelHandle, - dock_default_factory: DefaultItemFactory, + dock_default_factory: DockDefaultItemFactory, cx: &mut ViewContext, ) -> Self { cx.observe_fullscreen(|_, _, cx| cx.notify()).detach(); @@ -703,7 +703,7 @@ impl Workspace { serialized_workspace, workspace_id, project_handle, - app_state.default_item_factory, + app_state.dock_default_item_factory, cx, ); (app_state.initialize_workspace)(&mut workspace, &app_state, cx); @@ -2694,7 +2694,7 @@ mod tests { pub fn default_item_factory( _workspace: &mut Workspace, _cx: &mut ViewContext, - ) -> Box { + ) -> Option> { unimplemented!(); } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 2396af6465d291587355b7c11a7657570fe2bb3e..09a20b566002158d6f9f4e3471fb9e39c8852a73 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -32,13 +32,15 @@ use settings::{ use smol::process::Command; use std::fs::OpenOptions; use std::{env, ffi::OsStr, panic, path::PathBuf, sync::Arc, thread, time::Duration}; -use terminal_view::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() { @@ -150,7 +152,7 @@ fn main() { fs, build_window_options, initialize_workspace, - default_item_factory, + dock_default_item_factory, }); auto_update::init(http, client::ZED_SERVER_URL.clone(), cx); @@ -581,10 +583,10 @@ async fn handle_cli_connection( } } -pub fn default_item_factory( +pub fn dock_default_item_factory( workspace: &mut Workspace, cx: &mut ViewContext, -) -> Box { +) -> Option> { let strategy = cx .global::() .terminal_overrides @@ -594,12 +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( - Err(anyhow!("Don't have a project to open a terminal")), - 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)) }