diff --git a/Cargo.lock b/Cargo.lock index d1b0e62ca7..ccf5b2428d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,45 @@ dependencies = [ "memchr", ] +[[package]] +name = "alacritty_config_derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77044c45bdb871e501b5789ad16293ecb619e5733b60f4bb01d1cb31c463c336" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "alacritty_terminal" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fb5d4af84e39f9754d039ff6de2233c8996dbae0af74910156e559e5766e2f" +dependencies = [ + "alacritty_config_derive", + "base64 0.13.0", + "bitflags", + "dirs 3.0.2", + "libc", + "log", + "mio 0.6.23", + "mio-anonymous-pipes", + "mio-extras", + "miow 0.3.7", + "nix", + "parking_lot 0.11.2", + "regex-automata", + "serde", + "serde_yaml", + "signal-hook", + "signal-hook-mio", + "unicode-width", + "vte", + "winapi 0.3.9", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -2500,6 +2539,12 @@ dependencies = [ "safemem", ] +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "lipsum" version = "0.8.2" @@ -2724,7 +2769,7 @@ dependencies = [ "kernel32-sys", "libc", "log", - "miow", + "miow 0.2.2", "net2", "slab", "winapi 0.2.8", @@ -2742,6 +2787,42 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "mio-anonymous-pipes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bc513025fe5005a3aa561b50fdb2cda5a150b84800ae02acd8aa9ed62ca1a6b" +dependencies = [ + "mio 0.6.23", + "miow 0.3.7", + "parking_lot 0.11.2", + "spsc-buffer", + "winapi 0.3.9", +] + +[[package]] +name = "mio-extras" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" +dependencies = [ + "lazycell", + "log", + "mio 0.6.23", + "slab", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio 0.6.23", +] + [[package]] name = "miow" version = "0.2.2" @@ -2754,6 +2835,15 @@ dependencies = [ "ws2_32-sys", ] +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "multimap" version = "0.8.3" @@ -2798,6 +2888,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "nix" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", + "memoffset", +] + [[package]] name = "nom" version = "7.1.1" @@ -4252,6 +4355,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707d15895415db6628332b737c838b88c598522e4dc70647e59b72312924aebc" +dependencies = [ + "indexmap", + "ryu", + "serde", + "yaml-rust", +] + [[package]] name = "servo-fontconfig" version = "0.5.1" @@ -4364,6 +4479,18 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio 0.6.23", + "mio-uds", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -4492,6 +4619,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spsc-buffer" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be6c3f39c37a4283ee4b43d1311c828f2e1fb0541e76ea0cb1a2abd9ef2f5b3b" + [[package]] name = "sqlformat" version = "0.1.8" @@ -4739,6 +4872,23 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal" +version = "0.1.0" +dependencies = [ + "alacritty_terminal", + "editor", + "futures", + "gpui", + "mio-extras", + "project", + "settings", + "smallvec", + "theme", + "util", + "workspace", +] + [[package]] name = "text" version = "0.1.0" @@ -5531,6 +5681,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + [[package]] name = "util" version = "0.1.0" @@ -5616,6 +5772,26 @@ dependencies = [ "workspace", ] +[[package]] +name = "vte" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "waker-fn" version = "1.1.0" @@ -5967,6 +6143,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zed" version = "0.42.0" @@ -6034,6 +6219,7 @@ dependencies = [ "smol", "sum_tree", "tempdir", + "terminal", "text", "theme", "theme_selector", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 0f1e005891..e24996899c 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -226,7 +226,8 @@ "cmd-p": "file_finder::Toggle", "cmd-shift-P": "command_palette::Toggle", "cmd-shift-M": "diagnostics::Deploy", - "cmd-alt-s": "workspace::SaveAll" + "cmd-alt-s": "workspace::SaveAll", + "shift-cmd-T": "terminal::Deploy" } }, // Bindings from Sublime Text diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml new file mode 100644 index 0000000000..4d0cbb3cfc --- /dev/null +++ b/crates/terminal/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "terminal" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/terminal.rs" +doctest = false + +[dependencies] +alacritty_terminal = "0.16.1" +editor = { path = "../editor" } +util = { path = "../util" } +gpui = { path = "../gpui" } +theme = { path = "../theme" } +settings = { path = "../settings" } +workspace = { path = "../workspace" } +project = { path = "../project" } +smallvec = { version = "1.6", features = ["union"] } +mio-extras = "2.0.6" +futures = "0.3" diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs new file mode 100644 index 0000000000..8c4e264cc8 --- /dev/null +++ b/crates/terminal/src/terminal.rs @@ -0,0 +1,418 @@ +use std::sync::Arc; + +use alacritty_terminal::{ + // ansi::Handler, + config::{Config, Program, PtyConfig}, + event::{Event, EventListener, Notify}, + event_loop::{EventLoop, Notifier}, + grid::{Indexed, Scroll}, + index::Point, + sync::FairMutex, + term::{cell::Cell, SizeInfo}, + tty, + Term, +}; +use futures::{ + channel::mpsc::{unbounded, UnboundedSender}, + StreamExt, +}; +use gpui::{ + actions, + color::Color, + elements::*, + fonts::{with_font_cache, TextStyle}, + geometry::{rect::RectF, vector::vec2f}, + impl_internal_actions, + keymap::Keystroke, + text_layout::Line, + Entity, + Event::KeyDown, + MutableAppContext, Quad, View, ViewContext, +}; +use project::{Project, ProjectPath}; +use settings::Settings; +use smallvec::SmallVec; +use workspace::{Item, Workspace}; + +//ASCII Control characters on a keyboard +const BACKSPACE: char = 8_u8 as char; +const TAB: char = 9_u8 as char; +const CARRIAGE_RETURN: char = 13_u8 as char; +const ESC: char = 27_u8 as char; +const DEL: char = 127_u8 as char; + +#[derive(Clone, Debug, PartialEq, Eq)] +enum Direction { + LEFT, + RIGHT, +} + +impl Default for Direction { + fn default() -> Self { + Direction::LEFT + } +} + +#[derive(Clone, Default, Debug, PartialEq, Eq)] +struct KeyInput(char); +#[derive(Clone, Default, Debug, PartialEq, Eq)] +struct DirectionInput(Direction); + +actions!(terminal, [Deploy]); +impl_internal_actions!(terminal, [KeyInput, DirectionInput]); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(TerminalView::deploy); + cx.add_action(TerminalView::write_key_to_pty); + cx.add_action(TerminalView::move_cursor); +} + +#[derive(Clone)] +pub struct ZedListener(UnboundedSender); + +impl EventListener for ZedListener { + fn send_event(&self, event: Event) { + self.0.unbounded_send(event).ok(); + } +} + +struct TerminalView { + pty_tx: Notifier, + term: Arc>>, + title: String, +} + +impl Entity for TerminalView { + type Event = (); +} + +impl TerminalView { + fn new(cx: &mut ViewContext) -> Self { + let (events_tx, mut events_rx) = unbounded(); + cx.spawn(|this, mut cx| async move { + while let Some(event) = events_rx.next().await { + this.update(&mut cx, |this, cx| { + this.process_terminal_event(event, cx); + cx.notify(); + }); + } + }) + .detach(); + + let pty_config = PtyConfig { + shell: Some(Program::Just("zsh".to_string())), + working_directory: None, + hold: false, + }; + + let config = Config { + pty_config: pty_config.clone(), + ..Default::default() + }; + let size_info = SizeInfo::new(400., 100.0, 5., 5., 0., 0., false); + + let term = Term::new(&config, size_info, ZedListener(events_tx.clone())); + let term = Arc::new(FairMutex::new(term)); + + let pty = tty::new(&pty_config, &size_info, None).expect("Could not create tty"); + + let event_loop = EventLoop::new( + term.clone(), + ZedListener(events_tx.clone()), + pty, + pty_config.hold, + false, + ); + + let pty_tx = Notifier(event_loop.channel()); + let _io_thread = event_loop.spawn(); //todo cleanup + + TerminalView { + title: "Terminal".to_string(), + term, + pty_tx, + } + } + + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + workspace.add_item(Box::new(cx.add_view(|cx| TerminalView::new(cx))), cx); + } + + fn process_terminal_event( + &mut self, + event: alacritty_terminal::event::Event, + cx: &mut ViewContext, + ) { + match event { + alacritty_terminal::event::Event::Wakeup => cx.notify(), + alacritty_terminal::event::Event::PtyWrite(out) => self.pty_tx.notify(out.into_bytes()), + _ => {} + } + // + } + + fn write_key_to_pty(&mut self, action: &KeyInput, cx: &mut ViewContext) { + let mut bytes = vec![0; action.0.len_utf8()]; + action.0.encode_utf8(&mut bytes[..]); + self.pty_tx.notify(bytes); + } + + fn move_cursor(&mut self, action: &DirectionInput, cx: &mut ViewContext) { + let term = self.term.lock(); + match action.0 { + Direction::LEFT => { + self.pty_tx.notify("\x1b[C".to_string().into_bytes()); + } + Direction::RIGHT => { + self.pty_tx.notify("\x1b[D".to_string().into_bytes()); + } + } + } +} + +impl View for TerminalView { + fn ui_name() -> &'static str { + "TerminalView" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let _theme = cx.global::().theme.clone(); + + TerminalEl::new(self.term.clone()) + .contained() + // .with_style(theme.terminal.container) + .boxed() + } +} + +struct TerminalEl { + term: Arc>>, +} + +impl TerminalEl { + fn new(term: Arc>>) -> TerminalEl { + TerminalEl { term } + } +} + +struct LayoutState { + lines: Vec, + line_height: f32, +} + +impl Element for TerminalEl { + type LayoutState = LayoutState; + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + cx: &mut gpui::LayoutContext, + ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { + let term = self.term.lock(); + let content = term.renderable_content(); + + let mut lines = vec![]; + let mut cur_line = vec![]; + let mut last_line = 0; + for cell in content.display_iter { + let Indexed { + point: Point { line, .. }, + cell: Cell { c, .. }, + } = cell; + + if line != last_line { + lines.push(cur_line); + cur_line = vec![]; + last_line = line.0; + } + cur_line.push(c); + } + let line = lines + .into_iter() + .map(|char_vec| char_vec.into_iter().collect::()) + .fold("".to_string(), |grid, line| grid + &line + "\n"); + + let chunks = vec![(&line[..], None)].into_iter(); + + let text_style = with_font_cache(cx.font_cache.clone(), || TextStyle { + color: Color::white(), + ..Default::default() + }); + + let shaped_lines = layout_highlighted_chunks( + chunks, + &text_style, + cx.text_layout_cache, + &cx.font_cache, + usize::MAX, + line.matches('\n').count() + 1, + ); + let line_height = cx.font_cache.line_height(text_style.font_size); + + ( + constraint.max, + LayoutState { + lines: shaped_lines, + line_height, + }, + ) + } + + fn paint( + &mut self, + bounds: gpui::geometry::rect::RectF, + visible_bounds: gpui::geometry::rect::RectF, + layout: &mut Self::LayoutState, + cx: &mut gpui::PaintContext, + ) -> Self::PaintState { + let mut origin = bounds.origin(); + + for line in &layout.lines { + let boundaries = RectF::new(origin, vec2f(bounds.width(), layout.line_height)); + + if boundaries.intersects(visible_bounds) { + line.paint(origin, visible_bounds, layout.line_height, cx); + } + + origin.set_y(boundaries.max_y()); + } + + let term = self.term.lock(); + let cursor = term.renderable_content().cursor; + + let bounds = RectF::new( + vec2f( + cursor.point.column.0 as f32 * 10.0 + 150.0, + cursor.point.line.0 as f32 * 10.0 + 150.0, + ), + vec2f(10.0, 10.0), + ); + + cx.scene.push_quad(Quad { + bounds, + background: Some(Color::red()), + border: Default::default(), + corner_radius: 0., + }); + } + + fn dispatch_event( + &mut self, + event: &gpui::Event, + _bounds: gpui::geometry::rect::RectF, + _visible_bounds: gpui::geometry::rect::RectF, + _layout: &mut Self::LayoutState, + _paint: &mut Self::PaintState, + cx: &mut gpui::EventContext, + ) -> bool { + match event { + KeyDown { + input: Some(input), .. + } => { + dbg!(event); + cx.dispatch_action(KeyInput(input.chars().next().unwrap())); + true + } //TODO: Write control characters (ctrl-c) to pty + KeyDown { + keystroke: Keystroke { key, .. }, + input: None, + .. + } => { + dbg!(event); + if key == "backspace" { + cx.dispatch_action(KeyInput(DEL)); + true + } else if key == "enter" { + //There may be some subtlety here in how our terminal works + cx.dispatch_action(KeyInput(CARRIAGE_RETURN)); + true + } else if key == "tab" { + cx.dispatch_action(KeyInput(TAB)); + true + } else if key == "left" { + cx.dispatch_action(DirectionInput(Direction::LEFT)); + true + } else if key == "right" { + cx.dispatch_action(DirectionInput(Direction::RIGHT)); + true + // } else if key == "escape" { //TODO + // cx.dispatch_action(KeyInput(ESC)); + // true + } else { + false + } + } + _ => false, + } + } + + fn debug( + &self, + _bounds: gpui::geometry::rect::RectF, + _layout: &Self::LayoutState, + _paint: &Self::PaintState, + _cx: &gpui::DebugContext, + ) -> gpui::serde_json::Value { + unreachable!("Should never be called hopefully") + } +} + +impl Item for TerminalView { + fn tab_content(&self, style: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + let settings = cx.global::(); + let search_theme = &settings.theme.search; + Flex::row() + .with_child( + Label::new(self.title.clone(), style.label.clone()) + .aligned() + .contained() + .with_margin_left(search_theme.tab_icon_spacing) + .boxed(), + ) + .boxed() + } + + fn project_path(&self, _cx: &gpui::AppContext) -> Option { + None + } + + fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { + todo!() + } + + 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(())) + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 56472be040..b2d62d66db 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -46,6 +46,7 @@ rpc = { path = "../rpc" } settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } text = { path = "../text" } +terminal = { path = "../terminal" } theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } util = { path = "../util" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 10aa717c0d..e04b92dc0a 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -181,6 +181,7 @@ fn main() { diagnostics::init(cx); search::init(cx); vim::init(cx); + terminal::init(cx); let db = cx.background().block(db); let (settings_file, keymap_file) = cx.background().block(config_files).unwrap(); diff --git a/styles/package-lock.json b/styles/package-lock.json index 49304dc2fa..2eb6d3a1bf 100644 --- a/styles/package-lock.json +++ b/styles/package-lock.json @@ -5,7 +5,6 @@ "requires": true, "packages": { "": { - "name": "styles", "version": "1.0.0", "license": "ISC", "dependencies": {