Finally on solid conceptual ground, able to move ahead confidently with Alacritty code

Mikayla Maki created

Change summary

Cargo.lock                      | 188 +++++++++++++++
assets/keymaps/default.json     |   3 
crates/terminal/Cargo.toml      |  21 +
crates/terminal/src/terminal.rs | 418 +++++++++++++++++++++++++++++++++++
crates/zed/Cargo.toml           |   1 
crates/zed/src/main.rs          |   1 
styles/package-lock.json        |   1 
7 files changed, 630 insertions(+), 3 deletions(-)

Detailed changes

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

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

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"

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<Event>);
+
+impl EventListener for ZedListener {
+    fn send_event(&self, event: Event) {
+        self.0.unbounded_send(event).ok();
+    }
+}
+
+struct TerminalView {
+    pty_tx: Notifier,
+    term: Arc<FairMutex<Term<ZedListener>>>,
+    title: String,
+}
+
+impl Entity for TerminalView {
+    type Event = ();
+}
+
+impl TerminalView {
+    fn new(cx: &mut ViewContext<Self>) -> 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>) {
+        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<Self>,
+    ) {
+        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<Self>) {
+        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<Self>) {
+        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::<Settings>().theme.clone();
+
+        TerminalEl::new(self.term.clone())
+            .contained()
+            // .with_style(theme.terminal.container)
+            .boxed()
+    }
+}
+
+struct TerminalEl {
+    term: Arc<FairMutex<Term<ZedListener>>>,
+}
+
+impl TerminalEl {
+    fn new(term: Arc<FairMutex<Term<ZedListener>>>) -> TerminalEl {
+        TerminalEl { term }
+    }
+}
+
+struct LayoutState {
+    lines: Vec<Line>,
+    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::<String>())
+            .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::<Settings>();
+        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<ProjectPath> {
+        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<Self>) {}
+
+    fn can_save(&self, _cx: &gpui::AppContext) -> bool {
+        false
+    }
+
+    fn save(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        unreachable!("save should not have been called");
+    }
+
+    fn save_as(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _abs_path: std::path::PathBuf,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        unreachable!("save_as should not have been called");
+    }
+
+    fn reload(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        gpui::Task::ready(Ok(()))
+    }
+}

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

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

styles/package-lock.json 🔗

@@ -5,7 +5,6 @@
     "requires": true,
     "packages": {
         "": {
-            "name": "styles",
             "version": "1.0.0",
             "license": "ISC",
             "dependencies": {