From b36bf0c56d8711ce0b1798b8e55cccccbd49ac63 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 20 Jun 2022 17:36:13 -0700 Subject: [PATCH 01/60] Finally on solid conceptual ground, able to move ahead confidently with Alacritty code --- 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(-) create mode 100644 crates/terminal/Cargo.toml create mode 100644 crates/terminal/src/terminal.rs 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": { From 31bc758f35a9e30763c0072febc55fc554cdbfa1 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 22 Jun 2022 11:23:09 -0700 Subject: [PATCH 02/60] Forgot to commit last night --- assets/keymaps/default.json | 18 +- crates/terminal/src/terminal.rs | 322 ++++++++++++++++++++------------ crates/theme/src/theme.rs | 1 + 3 files changed, 224 insertions(+), 117 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index e24996899c..19029decdf 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -226,8 +226,7 @@ "cmd-p": "file_finder::Toggle", "cmd-shift-P": "command_palette::Toggle", "cmd-shift-M": "diagnostics::Deploy", - "cmd-alt-s": "workspace::SaveAll", - "shift-cmd-T": "terminal::Deploy" + "cmd-alt-s": "workspace::SaveAll" } }, // Bindings from Sublime Text @@ -353,5 +352,20 @@ "f2": "project_panel::Rename", "backspace": "project_panel::Delete" } + }, + { + "context": "Terminal", + "bindings": { + "ctrl-c": "terminal::SIGINT", + "escape": "terminal::ESCAPE", + "ctrl-d": "terminal::Quit", + "backspace": "terminal::DEL", + "enter": "terminal::RETURN", + "left": "terminal::LEFT", + "right": "terminal::RIGHT", + "up": "terminal::HistoryBack", + "down": "terminal::HistoryForward", + "tab": "terminal::AutoComplete" + } } ] \ No newline at end of file diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 8c4e264cc8..f6237eed2b 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,16 +1,14 @@ use std::sync::Arc; use alacritty_terminal::{ - // ansi::Handler, config::{Config, Program, PtyConfig}, event::{Event, EventListener, Notify}, - event_loop::{EventLoop, Notifier}, - grid::{Indexed, Scroll}, + event_loop::{EventLoop, Msg, Notifier}, + grid::Indexed, index::Point, sync::FairMutex, term::{cell::Cell, SizeInfo}, - tty, - Term, + tty, Term, }; use futures::{ channel::mpsc::{unbounded, UnboundedSender}, @@ -23,7 +21,7 @@ use gpui::{ fonts::{with_font_cache, TextStyle}, geometry::{rect::RectF, vector::vec2f}, impl_internal_actions, - keymap::Keystroke, + json::json, text_layout::Line, Entity, Event::KeyDown, @@ -35,36 +33,52 @@ 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 - } -} +//Consts -> Structs -> Impls -> Functions, Vaguely in order of importance +const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c' +const TAB_CHAR: char = 9_u8 as char; +const CARRIAGE_RETURN_CHAR: char = 13_u8 as char; +const ESC_CHAR: char = 27_u8 as char; +const DEL_CHAR: char = 127_u8 as char; +const LEFT_SEQ: &str = "\x1b[D"; +const RIGHT_SEQ: &str = "\x1b[C"; +const UP_SEQ: &str = "\x1b[A"; +const DOWN_SEQ: &str = "\x1b[B"; +const DEFAULT_TITLE: &str = "Terminal"; #[derive(Clone, Default, Debug, PartialEq, Eq)] -struct KeyInput(char); -#[derive(Clone, Default, Debug, PartialEq, Eq)] -struct DirectionInput(Direction); +struct Input(String); -actions!(terminal, [Deploy]); -impl_internal_actions!(terminal, [KeyInput, DirectionInput]); +actions!( + terminal, + [ + Deploy, + SIGINT, + ESCAPE, + Quit, + DEL, + RETURN, + LEFT, + RIGHT, + HistoryBack, + HistoryForward, + AutoComplete + ] +); +impl_internal_actions!(terminal, [Input]); 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); + cx.add_action(Terminal::deploy); + cx.add_action(Terminal::write_to_pty); + cx.add_action(Terminal::send_sigint); //TODO figure out how to do this properly + cx.add_action(Terminal::escape); + cx.add_action(Terminal::quit); + cx.add_action(Terminal::del); + cx.add_action(Terminal::carriage_return); + cx.add_action(Terminal::left); + cx.add_action(Terminal::right); + cx.add_action(Terminal::history_back); + cx.add_action(Terminal::history_forward); + cx.add_action(Terminal::autocomplete); } #[derive(Clone)] @@ -76,46 +90,59 @@ impl EventListener for ZedListener { } } -struct TerminalView { +struct Terminal { pty_tx: Notifier, term: Arc>>, title: String, } -impl Entity for TerminalView { +impl Entity for Terminal { type Event = (); } -impl TerminalView { +impl Terminal { fn new(cx: &mut ViewContext) -> Self { + //Spawn a task so the Alacritty EventLoop to communicate with us let (events_tx, mut events_rx) = unbounded(); - cx.spawn(|this, mut cx| async move { + cx.spawn_weak(|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(); - }); + match this.upgrade(&cx) { + Some(handle) => { + handle.update(&mut cx, |this, cx| { + this.process_terminal_event(event, cx); + cx.notify(); + }); + } + None => break, + } } }) .detach(); + //TODO: Load from settings let pty_config = PtyConfig { shell: Some(Program::Just("zsh".to_string())), working_directory: None, hold: false, }; + //TODO: Properly configure this let config = Config { pty_config: pty_config.clone(), ..Default::default() }; + + //TODO: derive this let size_info = SizeInfo::new(400., 100.0, 5., 5., 0., 0., false); + //Set up the terminal... let term = Term::new(&config, size_info, ZedListener(events_tx.clone())); let term = Arc::new(FairMutex::new(term)); + //Setup the pty... let pty = tty::new(&pty_config, &size_info, None).expect("Could not create tty"); + //And connect them together let event_loop = EventLoop::new( term.clone(), ZedListener(events_tx.clone()), @@ -124,18 +151,18 @@ impl TerminalView { false, ); + //Kick things off let pty_tx = Notifier(event_loop.channel()); - let _io_thread = event_loop.spawn(); //todo cleanup - - TerminalView { - title: "Terminal".to_string(), + let _io_thread = event_loop.spawn(); + Terminal { + title: DEFAULT_TITLE.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); + workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx))), cx); } fn process_terminal_event( @@ -144,35 +171,94 @@ impl TerminalView { 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()), - _ => {} + Event::Wakeup => cx.notify(), + Event::PtyWrite(out) => self.write_to_pty(&Input(out), cx), + Event::MouseCursorDirty => todo!(), //I think this is outside of Zed's loop + Event::Title(title) => self.title = title, + Event::ResetTitle => self.title = DEFAULT_TITLE.to_string(), + Event::ClipboardStore(_, _) => todo!(), + Event::ClipboardLoad(_, _) => todo!(), + Event::ColorRequest(_, _) => todo!(), + Event::CursorBlinkingChange => todo!(), + Event::Bell => todo!(), + Event::Exit => todo!(), + Event::MouseCursorDirty => todo!(), } // } - 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 shutdown_pty(&mut self) { + self.pty_tx.0.send(Msg::Shutdown).ok(); } - 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()); - } - } + fn history_back(&mut self, _: &HistoryBack, cx: &mut ViewContext) { + self.write_to_pty(&Input(UP_SEQ.to_string()), cx); + + //Noop.. for now... + //This might just need to be forwarded to the terminal? + //Behavior changes based on mode... + } + + fn history_forward(&mut self, _: &HistoryForward, cx: &mut ViewContext) { + self.write_to_pty(&Input(DOWN_SEQ.to_string()), cx); + //Noop.. for now... + //This might just need to be forwarded to the terminal by the pty? + //Behvaior changes based on mode + } + + fn autocomplete(&mut self, _: &AutoComplete, cx: &mut ViewContext) { + self.write_to_pty(&Input(TAB_CHAR.to_string()), cx); + //Noop.. for now... + //This might just need to be forwarded to the terminal by the pty? + //Behvaior changes based on mode + } + + fn write_to_pty(&mut self, input: &Input, _cx: &mut ViewContext) { + self.pty_tx.notify(input.0.clone().into_bytes()); + } + + fn send_sigint(&mut self, _: &SIGINT, cx: &mut ViewContext) { + self.write_to_pty(&Input(ETX_CHAR.to_string()), cx); + } + + fn escape(&mut self, _: &ESCAPE, cx: &mut ViewContext) { + self.write_to_pty(&Input(ESC_CHAR.to_string()), cx); + } + + fn del(&mut self, _: &DEL, cx: &mut ViewContext) { + self.write_to_pty(&Input(DEL_CHAR.to_string()), cx); + } + + fn carriage_return(&mut self, _: &RETURN, cx: &mut ViewContext) { + self.write_to_pty(&Input(CARRIAGE_RETURN_CHAR.to_string()), cx); + } + + fn left(&mut self, _: &LEFT, cx: &mut ViewContext) { + self.write_to_pty(&Input(LEFT_SEQ.to_string()), cx); + } + + fn right(&mut self, _: &RIGHT, cx: &mut ViewContext) { + self.write_to_pty(&Input(RIGHT_SEQ.to_string()), cx); + } + + fn quit(&mut self, _: &Quit, _cx: &mut ViewContext) { + //TODO + // cx.dispatch_action(cx.window_id(), workspace::CloseItem()); + } + + // ShowHistory, + // AutoComplete +} + +impl Drop for Terminal { + fn drop(&mut self) { + self.shutdown_pty(); } } -impl View for TerminalView { +impl View for Terminal { fn ui_name() -> &'static str { - "TerminalView" + "Terminal" } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { @@ -198,6 +284,7 @@ impl TerminalEl { struct LayoutState { lines: Vec, line_height: f32, + cursor: RectF, } impl Element for TerminalEl { @@ -209,9 +296,55 @@ impl Element for TerminalEl { constraint: gpui::SizeConstraint, cx: &mut gpui::LayoutContext, ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - let term = self.term.lock(); + let size = constraint.max; + //Get terminal content + let mut term = self.term.lock(); + + //Set up text rendering + + let text_style = with_font_cache(cx.font_cache.clone(), || TextStyle { + color: Color::white(), + ..Default::default() + }); + let line_height = cx.font_cache.line_height(text_style.font_size); + let em_width = cx + .font_cache() + .em_width(text_style.font_id, text_style.font_size); + + term.resize(SizeInfo::new( + size.x(), + size.y(), + em_width, + line_height, + 0., + 0., + false, + )); + let content = term.renderable_content(); + // //Dual owned system from Neovide + // let mut block_width = cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x; + // if block_width == 0.0 { + // block_width = layout.em_width; + // } + let cursor = RectF::new( + vec2f( + content.cursor.point.column.0 as f32 * em_width, + content.cursor.point.line.0 as f32 * line_height, + ), + vec2f(em_width, line_height), + ); + + // let cursor = Cursor { + // color: selection_style.cursor, + // block_width, + // origin: content_origin + vec2f(x, y), + // line_height: layout.line_height, + // shape: self.cursor_shape, + // block_text, + // } + let mut lines = vec![]; let mut cur_line = vec![]; let mut last_line = 0; @@ -235,11 +368,6 @@ impl Element for TerminalEl { 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, @@ -248,13 +376,13 @@ impl Element for TerminalEl { 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, + cursor, }, ) } @@ -278,19 +406,11 @@ impl Element for TerminalEl { 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), - ); + let new_origin = bounds.origin() + layout.cursor.origin(); + let new_cursor = RectF::new(new_origin, layout.cursor.size()); cx.scene.push_quad(Quad { - bounds, + bounds: new_cursor, background: Some(Color::red()), border: Default::default(), corner_radius: 0., @@ -310,38 +430,8 @@ impl Element for TerminalEl { KeyDown { input: Some(input), .. } => { - dbg!(event); - cx.dispatch_action(KeyInput(input.chars().next().unwrap())); + cx.dispatch_action(Input(input.to_string())); 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, } @@ -354,11 +444,13 @@ impl Element for TerminalEl { _paint: &Self::PaintState, _cx: &gpui::DebugContext, ) -> gpui::serde_json::Value { - unreachable!("Should never be called hopefully") + json!({ + "type": "TerminalElement", + }) } } -impl Item for TerminalView { +impl Item for Terminal { fn tab_content(&self, style: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { let settings = cx.global::(); let search_theme = &settings.theme.search; @@ -378,7 +470,7 @@ impl Item for TerminalView { } fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { - todo!() + SmallVec::new() } fn is_singleton(&self, _cx: &gpui::AppContext) -> bool { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index ae269c00cb..37a408d85f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -33,6 +33,7 @@ pub struct Theme { pub contact_notification: ContactNotification, pub update_notification: UpdateNotification, pub tooltip: TooltipStyle, + // pub terminal: Terminal, } #[derive(Deserialize, Default)] From f58a15bbb1a348abbdccaf63872d6c8231fa2522 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 22 Jun 2022 11:23:29 -0700 Subject: [PATCH 03/60] Removed final stuff from theme.rs --- crates/theme/src/theme.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 37a408d85f..ae269c00cb 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -33,7 +33,6 @@ pub struct Theme { pub contact_notification: ContactNotification, pub update_notification: UpdateNotification, pub tooltip: TooltipStyle, - // pub terminal: Terminal, } #[derive(Deserialize, Default)] From 9e55c60b6a99daf644111b7c98b9c6eeec75ed12 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 24 Jun 2022 15:02:54 -0700 Subject: [PATCH 04/60] working on selection and scrolling in terminals --- Cargo.lock | 1 + assets/keymaps/default.json | 7 +- crates/terminal/Cargo.toml | 1 + crates/terminal/src/terminal.rs | 473 +++++++++++++++++++++----------- 4 files changed, 326 insertions(+), 156 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ccf5b2428d..1a59f23918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4881,6 +4881,7 @@ dependencies = [ "futures", "gpui", "mio-extras", + "ordered-float", "project", "settings", "smallvec", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 19029decdf..f9254176a9 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -363,9 +363,10 @@ "enter": "terminal::RETURN", "left": "terminal::LEFT", "right": "terminal::RIGHT", - "up": "terminal::HistoryBack", - "down": "terminal::HistoryForward", - "tab": "terminal::AutoComplete" + "up": "terminal::UP", + "down": "terminal::DOWN", + "tab": "terminal::TAB", + "cmd-k": "terminal::Clear" } } ] \ No newline at end of file diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 4d0cbb3cfc..3cb4d631f3 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -19,3 +19,4 @@ project = { path = "../project" } smallvec = { version = "1.6", features = ["union"] } mio-extras = "2.0.6" futures = "0.3" +ordered-float = "2.1.1" diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index f6237eed2b..c8f936bda9 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,13 +1,18 @@ use std::sync::Arc; use alacritty_terminal::{ + ansi::Color as AnsiColor, config::{Config, Program, PtyConfig}, - event::{Event, EventListener, Notify}, + event::{Event as AlacTermEvent, EventListener, Notify}, event_loop::{EventLoop, Msg, Notifier}, grid::Indexed, index::Point, sync::FairMutex, - term::{cell::Cell, SizeInfo}, + term::{ + cell::{Cell, Flags}, + color::Rgb, + SizeInfo, + }, tty, Term, }; use futures::{ @@ -18,15 +23,17 @@ use gpui::{ actions, color::Color, elements::*, - fonts::{with_font_cache, TextStyle}, + fonts::{with_font_cache, HighlightStyle, TextStyle, Underline}, geometry::{rect::RectF, vector::vec2f}, impl_internal_actions, json::json, + platform::CursorStyle, text_layout::Line, - Entity, + ClipboardItem, Entity, Event::KeyDown, MutableAppContext, Quad, View, ViewContext, }; +use ordered_float::OrderedFloat; use project::{Project, ProjectPath}; use settings::Settings; use smallvec::SmallVec; @@ -43,6 +50,7 @@ const LEFT_SEQ: &str = "\x1b[D"; const RIGHT_SEQ: &str = "\x1b[C"; const UP_SEQ: &str = "\x1b[A"; const DOWN_SEQ: &str = "\x1b[B"; +const CLEAR_SEQ: &str = "\x1b[2J"; const DEFAULT_TITLE: &str = "Terminal"; #[derive(Clone, Default, Debug, PartialEq, Eq)] @@ -50,59 +58,57 @@ struct Input(String); actions!( terminal, - [ - Deploy, - SIGINT, - ESCAPE, - Quit, - DEL, - RETURN, - LEFT, - RIGHT, - HistoryBack, - HistoryForward, - AutoComplete - ] + [Deploy, SIGINT, ESCAPE, Quit, DEL, RETURN, LEFT, RIGHT, UP, DOWN, TAB, Clear] ); impl_internal_actions!(terminal, [Input]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(Terminal::deploy); cx.add_action(Terminal::write_to_pty); - cx.add_action(Terminal::send_sigint); //TODO figure out how to do this properly + cx.add_action(Terminal::send_sigint); cx.add_action(Terminal::escape); cx.add_action(Terminal::quit); cx.add_action(Terminal::del); - cx.add_action(Terminal::carriage_return); + cx.add_action(Terminal::carriage_return); //TODO figure out how to do this properly. Should we be checking the terminal mode? cx.add_action(Terminal::left); cx.add_action(Terminal::right); - cx.add_action(Terminal::history_back); - cx.add_action(Terminal::history_forward); - cx.add_action(Terminal::autocomplete); + cx.add_action(Terminal::up); + cx.add_action(Terminal::down); + cx.add_action(Terminal::tab); + cx.add_action(Terminal::clear); } #[derive(Clone)] -pub struct ZedListener(UnboundedSender); +pub struct ZedListener(UnboundedSender); impl EventListener for ZedListener { - fn send_event(&self, event: Event) { + fn send_event(&self, event: AlacTermEvent) { self.0.unbounded_send(event).ok(); } } +///A terminal renderer. struct Terminal { pty_tx: Notifier, term: Arc>>, title: String, + has_new_content: bool, + has_bell: bool, //Currently using iTerm bell, show bell emoji in tab until input is received +} + +enum ZedTermEvent { + TitleChanged, + CloseTerminal, } impl Entity for Terminal { - type Event = (); + type Event = ZedTermEvent; } impl Terminal { + ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices fn new(cx: &mut ViewContext) -> Self { - //Spawn a task so the Alacritty EventLoop to communicate with us + //Spawn a task so the Alacritty EventLoop can communicate with us in a view context let (events_tx, mut events_rx) = unbounded(); cx.spawn_weak(|this, mut cx| async move { while let Some(event) = events_rx.next().await { @@ -158,65 +164,109 @@ impl Terminal { title: DEFAULT_TITLE.to_string(), term, pty_tx, + has_new_content: false, + has_bell: false, } } - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx))), cx); - } - + ///Takes events from Alacritty and translates them to behavior on this view fn process_terminal_event( &mut self, event: alacritty_terminal::event::Event, cx: &mut ViewContext, ) { match event { - Event::Wakeup => cx.notify(), - Event::PtyWrite(out) => self.write_to_pty(&Input(out), cx), - Event::MouseCursorDirty => todo!(), //I think this is outside of Zed's loop - Event::Title(title) => self.title = title, - Event::ResetTitle => self.title = DEFAULT_TITLE.to_string(), - Event::ClipboardStore(_, _) => todo!(), - Event::ClipboardLoad(_, _) => todo!(), - Event::ColorRequest(_, _) => todo!(), - Event::CursorBlinkingChange => todo!(), - Event::Bell => todo!(), - Event::Exit => todo!(), - Event::MouseCursorDirty => todo!(), + AlacTermEvent::Wakeup => { + if !cx.is_self_focused() { + //Need to figure out how to trigger a redraw when not in focus + self.has_new_content = true; //Change tab content + cx.emit(ZedTermEvent::TitleChanged); + } else { + cx.notify() + } + } + AlacTermEvent::PtyWrite(out) => self.write_to_pty(&Input(out), cx), + //TODO: + //What this is supposed to do is check the cursor state, then set it on the platform. + //See Processor::reset_mouse_cursor() and Processor::cursor_state() in alacritty/src/input.rs + //to see how this is Calculated. Question: Does this flow make sense with how GPUI hadles + //the mouse? + AlacTermEvent::MouseCursorDirty => { + //Calculate new cursor style. + //Check on correctly handling mouse events for terminals + cx.platform().set_cursor_style(CursorStyle::Arrow); //??? + println!("Mouse cursor dirty") + } + AlacTermEvent::Title(title) => { + self.title = title; + cx.emit(ZedTermEvent::TitleChanged); + } + AlacTermEvent::ResetTitle => { + self.title = DEFAULT_TITLE.to_string(); + cx.emit(ZedTermEvent::TitleChanged); + } + AlacTermEvent::ClipboardStore(_, data) => { + cx.write_to_clipboard(ClipboardItem::new(data)) + } + AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty( + &Input(format( + &cx.read_from_clipboard() + .map(|ci| ci.text().to_string()) + .unwrap_or("".to_string()), + )), + cx, + ), + AlacTermEvent::ColorRequest(index, format) => { + //TODO test this as well + //TODO: change to getting the display colors, like alacrityy, instead of a default + let color = self.term.lock().colors()[index].unwrap_or(Rgb::default()); + self.write_to_pty(&Input(format(color)), cx) + } + AlacTermEvent::CursorBlinkingChange => { + //So, it's our job to set a timer and cause the cursor to blink here + //Which means that I'm going to put this off until someone @ Zed looks at it + } + AlacTermEvent::Bell => { + self.has_bell = true; + cx.emit(ZedTermEvent::TitleChanged); + } + AlacTermEvent::Exit => self.quit(&Quit, cx), } - // } + ///Create a new Terminal + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx))), cx); + } + + ///Send the shutdown message to Alacritty fn shutdown_pty(&mut self) { self.pty_tx.0.send(Msg::Shutdown).ok(); } - fn history_back(&mut self, _: &HistoryBack, cx: &mut ViewContext) { - self.write_to_pty(&Input(UP_SEQ.to_string()), cx); - - //Noop.. for now... - //This might just need to be forwarded to the terminal? - //Behavior changes based on mode... + fn quit(&mut self, _: &Quit, cx: &mut ViewContext) { + cx.emit(ZedTermEvent::CloseTerminal); } - fn history_forward(&mut self, _: &HistoryForward, cx: &mut ViewContext) { - self.write_to_pty(&Input(DOWN_SEQ.to_string()), cx); - //Noop.. for now... - //This might just need to be forwarded to the terminal by the pty? - //Behvaior changes based on mode - } - - fn autocomplete(&mut self, _: &AutoComplete, cx: &mut ViewContext) { - self.write_to_pty(&Input(TAB_CHAR.to_string()), cx); - //Noop.. for now... - //This might just need to be forwarded to the terminal by the pty? - //Behvaior changes based on mode - } - - fn write_to_pty(&mut self, input: &Input, _cx: &mut ViewContext) { + fn write_to_pty(&mut self, input: &Input, cx: &mut ViewContext) { + //iTerm bell behavior, bell stays until terminal is interacted with + self.has_bell = false; + cx.emit(ZedTermEvent::TitleChanged); self.pty_tx.notify(input.0.clone().into_bytes()); } + fn up(&mut self, _: &UP, cx: &mut ViewContext) { + self.write_to_pty(&Input(UP_SEQ.to_string()), cx); + } + + fn down(&mut self, _: &DOWN, cx: &mut ViewContext) { + self.write_to_pty(&Input(DOWN_SEQ.to_string()), cx); + } + + fn tab(&mut self, _: &TAB, cx: &mut ViewContext) { + self.write_to_pty(&Input(TAB_CHAR.to_string()), cx); + } + fn send_sigint(&mut self, _: &SIGINT, cx: &mut ViewContext) { self.write_to_pty(&Input(ETX_CHAR.to_string()), cx); } @@ -241,13 +291,9 @@ impl Terminal { self.write_to_pty(&Input(RIGHT_SEQ.to_string()), cx); } - fn quit(&mut self, _: &Quit, _cx: &mut ViewContext) { - //TODO - // cx.dispatch_action(cx.window_id(), workspace::CloseItem()); + fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { + self.write_to_pty(&Input(CLEAR_SEQ.to_string()), cx); } - - // ShowHistory, - // AutoComplete } impl Drop for Terminal { @@ -269,6 +315,98 @@ impl View for Terminal { // .with_style(theme.terminal.container) .boxed() } + + fn on_focus(&mut self, _: &mut ViewContext) { + self.has_new_content = false; + } +} + +impl Item for Terminal { + fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + let settings = cx.global::(); + let search_theme = &settings.theme.search; //TODO properly integrate themes + + let mut flex = Flex::row(); + + if self.has_bell { + flex.add_child( + Svg::new("icons/zap.svg") + .with_color(tab_theme.label.text.color) + .constrained() + .with_width(search_theme.tab_icon_width) + .aligned() + .boxed(), + ); + }; + + flex.with_child( + Label::new(self.title.clone(), tab_theme.label.clone()) + .aligned() + .contained() + .with_margin_left(if self.has_bell { + search_theme.tab_icon_spacing + } else { + 0. + }) + .boxed(), + ) + .boxed() + } + + 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, _: &gpui::AppContext) -> bool { + self.has_new_content + } + + fn should_update_tab_on_event(event: &Self::Event) -> bool { + matches!(event, &ZedTermEvent::TitleChanged) + } + + fn should_close_item_on_event(event: &Self::Event) -> bool { + matches!(event, &ZedTermEvent::CloseTerminal) + } } struct TerminalEl { @@ -286,7 +424,39 @@ struct LayoutState { line_height: f32, cursor: RectF, } +/* TODO point calculation for selection + * take the current point's x: + * - subtract padding + * - divide by cell width + * - take the minimum of the x coord and the last colum of the size info + * Take the current point's y: + * - Subtract padding + * - Divide by cell height + * - Take the minimum of the y coord and the last line + * + * With this x and y, pass to term::viewport_to_point (module function) + * Also pass in the display offset from the term.grid().display_offset() + * (Display offset is for scrolling) + */ +/* TODO Selection + * 1. On click, calculate the single, double, and triple click based on timings + * 2. Convert mouse location to a terminal point + * 3. Generate each of the three kinds of selection needed + * 4. Assign a selection to the terminal's selection variable + * How to render? + * 1. On mouse moved, calculate a terminal point + * 2. if (lmb_pressed || rmb_pressed) && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode() + * 3. Take the selection from the terminal, call selection.update(), and put it back + */ + +/* TODO Scroll + * 1. Convert scroll to a pixel delta (alacritty/src/input > Processor::mouse_wheel_input) + * 2. Divide by cell height + * 3. Create an alacritty_terminal::Scroll::Delta() object and call `self.terminal.scroll_display(scroll);` + * 4. Maybe do a cx.notify, just in case. + * 5. Also update the selected area, just check out for the logic alacritty/src/event.rs > ActionContext::scroll + */ impl Element for TerminalEl { type LayoutState = LayoutState; type PaintState = (); @@ -323,11 +493,6 @@ impl Element for TerminalEl { let content = term.renderable_content(); - // //Dual owned system from Neovide - // let mut block_width = cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x; - // if block_width == 0.0 { - // block_width = layout.em_width; - // } let cursor = RectF::new( vec2f( content.cursor.point.column.0 as f32 * em_width, @@ -336,45 +501,50 @@ impl Element for TerminalEl { vec2f(em_width, line_height), ); - // let cursor = Cursor { - // color: selection_style.cursor, - // block_width, - // origin: content_origin + vec2f(x, y), - // line_height: layout.line_height, - // shape: self.cursor_shape, - // block_text, - // } - - let mut lines = vec![]; - let mut cur_line = vec![]; + let mut lines: Vec<(String, Option)> = vec![]; let mut last_line = 0; + + let mut cur_chunk = String::new(); + + let mut cur_highlight = HighlightStyle { + color: Some(Color::white()), + ..Default::default() + }; for cell in content.display_iter { let Indexed { point: Point { line, .. }, - cell: Cell { c, .. }, + cell: Cell { + c, fg, flags, .. // TODO: Add bg and flags + }, //TODO: Learn what 'CellExtra does' } = cell; + let new_highlight = make_style_from_cell(fg, flags); + HighlightStyle { + color: Some(alac_color_to_gpui_color(fg)), + ..Default::default() + }; + if line != last_line { - lines.push(cur_line); - cur_line = vec![]; + cur_chunk.push('\n'); 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(); + if new_highlight != cur_highlight { + lines.push((cur_chunk.clone(), Some(cur_highlight.clone()))); + cur_chunk.clear(); + cur_highlight = new_highlight; + } + cur_chunk.push(*c) + } + lines.push((cur_chunk, Some(cur_highlight))); let shaped_lines = layout_highlighted_chunks( - chunks, + lines.iter().map(|(text, style)| (text.as_str(), *style)), &text_style, cx.text_layout_cache, &cx.font_cache, usize::MAX, - line.matches('\n').count() + 1, + last_line as usize, ); ( @@ -450,61 +620,58 @@ impl Element for TerminalEl { } } -impl Item for Terminal { - 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 { +fn make_style_from_cell(fg: &AnsiColor, flags: &Flags) -> HighlightStyle { + let fg = Some(alac_color_to_gpui_color(fg)); + let underline = if flags.contains(Flags::UNDERLINE) { + Some(Underline { + color: fg, + squiggly: false, + thickness: OrderedFloat(1.), + }) + } else { 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(())) + }; + HighlightStyle { + color: fg, + underline, + ..Default::default() + } +} + +fn alac_color_to_gpui_color(allac_color: &AnsiColor) -> Color { + match allac_color { + alacritty_terminal::ansi::Color::Named(n) => match n { + alacritty_terminal::ansi::NamedColor::Black => Color::black(), + alacritty_terminal::ansi::NamedColor::Red => Color::red(), + alacritty_terminal::ansi::NamedColor::Green => Color::green(), + alacritty_terminal::ansi::NamedColor::Yellow => Color::yellow(), + alacritty_terminal::ansi::NamedColor::Blue => Color::blue(), + alacritty_terminal::ansi::NamedColor::Magenta => Color::new(188, 63, 188, 1), + alacritty_terminal::ansi::NamedColor::Cyan => Color::new(17, 168, 205, 1), + alacritty_terminal::ansi::NamedColor::White => Color::white(), + alacritty_terminal::ansi::NamedColor::BrightBlack => Color::new(102, 102, 102, 1), + alacritty_terminal::ansi::NamedColor::BrightRed => Color::new(102, 102, 102, 1), + alacritty_terminal::ansi::NamedColor::BrightGreen => Color::new(35, 209, 139, 1), + alacritty_terminal::ansi::NamedColor::BrightYellow => Color::new(245, 245, 67, 1), + alacritty_terminal::ansi::NamedColor::BrightBlue => Color::new(59, 142, 234, 1), + alacritty_terminal::ansi::NamedColor::BrightMagenta => Color::new(214, 112, 214, 1), + alacritty_terminal::ansi::NamedColor::BrightCyan => Color::new(41, 184, 219, 1), + alacritty_terminal::ansi::NamedColor::BrightWhite => Color::new(229, 229, 229, 1), + alacritty_terminal::ansi::NamedColor::Foreground => Color::white(), + alacritty_terminal::ansi::NamedColor::Background => Color::black(), + alacritty_terminal::ansi::NamedColor::Cursor => Color::white(), + alacritty_terminal::ansi::NamedColor::DimBlack => Color::white(), + alacritty_terminal::ansi::NamedColor::DimRed => Color::white(), + alacritty_terminal::ansi::NamedColor::DimGreen => Color::white(), + alacritty_terminal::ansi::NamedColor::DimYellow => Color::white(), + alacritty_terminal::ansi::NamedColor::DimBlue => Color::white(), + alacritty_terminal::ansi::NamedColor::DimMagenta => Color::white(), + alacritty_terminal::ansi::NamedColor::DimCyan => Color::white(), + alacritty_terminal::ansi::NamedColor::DimWhite => Color::white(), + alacritty_terminal::ansi::NamedColor::BrightForeground => Color::white(), + alacritty_terminal::ansi::NamedColor::DimForeground => Color::white(), + }, //Theme defined + alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, 1), + alacritty_terminal::ansi::Color::Indexed(_) => Color::white(), //Color cube weirdness } } From 2b1fa07e89782b8ce5c9790c9a5a1564fdfe7308 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 24 Jun 2022 15:12:30 -0700 Subject: [PATCH 05/60] Working on selection --- crates/terminal/src/terminal.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index c8f936bda9..ed5d0b00ff 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -7,6 +7,7 @@ use alacritty_terminal::{ event_loop::{EventLoop, Msg, Notifier}, grid::Indexed, index::Point, + selection, sync::FairMutex, term::{ cell::{Cell, Flags}, @@ -15,8 +16,10 @@ use alacritty_terminal::{ }, tty, Term, }; +use editor::CursorShape; use futures::{ channel::mpsc::{unbounded, UnboundedSender}, + future::select, StreamExt, }; use gpui::{ @@ -492,6 +495,7 @@ impl Element for TerminalEl { )); let content = term.renderable_content(); + let selection = content.selection; let cursor = RectF::new( vec2f( @@ -510,6 +514,9 @@ impl Element for TerminalEl { color: Some(Color::white()), ..Default::default() }; + + let select_boxes = vec![]; + for cell in content.display_iter { let Indexed { point: Point { line, .. }, @@ -518,6 +525,9 @@ impl Element for TerminalEl { }, //TODO: Learn what 'CellExtra does' } = cell; + if let Some(selection) = selection { + selection.contains_cell(&cell, ???, CursorShape::Block); + } let new_highlight = make_style_from_cell(fg, flags); HighlightStyle { color: Some(alac_color_to_gpui_color(fg)), From 24d671ed3f0cd795eda2b5ff1f8c313ae7c5c532 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 27 Jun 2022 18:31:42 -0700 Subject: [PATCH 06/60] First bits of polish --- assets/keymaps/default.json | 3 +- crates/terminal/src/element.rs | 331 +++++++++++++++++++++++++++++++ crates/terminal/src/terminal.rs | 340 +++----------------------------- crates/zed/src/main.rs | 1 + 4 files changed, 361 insertions(+), 314 deletions(-) create mode 100644 crates/terminal/src/element.rs diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index f9254176a9..232fd4db1f 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -366,7 +366,8 @@ "up": "terminal::UP", "down": "terminal::DOWN", "tab": "terminal::TAB", - "cmd-k": "terminal::Clear" + "cmd-k": "terminal::Clear", + "cmd-v": "terminal::Paste" } } ] \ No newline at end of file diff --git a/crates/terminal/src/element.rs b/crates/terminal/src/element.rs new file mode 100644 index 0000000000..c58220ac88 --- /dev/null +++ b/crates/terminal/src/element.rs @@ -0,0 +1,331 @@ +use alacritty_terminal::{ + ansi::Color as AnsiColor, + event_loop::Msg, + grid::{Indexed, Scroll}, + index::Point, + sync::FairMutex, + term::{ + cell::{Cell, Flags}, + RenderableCursor, SizeInfo, + }, + Term, +}; +use gpui::{ + color::Color, + elements::*, + fonts::{HighlightStyle, TextStyle, Underline}, + geometry::{rect::RectF, vector::vec2f}, + json::json, + text_layout::Line, + Event, PaintContext, Quad, +}; +use mio_extras::channel::Sender; +use ordered_float::OrderedFloat; +use settings::Settings; +use std::sync::Arc; + +use crate::{Input, ZedListener}; + +const DEBUG_GRID: bool = false; +const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; + +pub struct TerminalEl { + term: Arc>>, + pty_tx: Sender, + size: SizeInfo, +} + +impl TerminalEl { + pub fn new( + term: Arc>>, + pty_tx: Sender, + size: SizeInfo, + ) -> TerminalEl { + TerminalEl { term, pty_tx, size } + } +} + +pub struct LayoutState { + lines: Vec, + line_height: f32, + em_width: f32, + cursor: Option, +} + +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 size = constraint.max; + let settings = cx.global::(); + let theme = &settings.theme.editor; + //Get terminal + let mut term = self.term.lock(); + + //Set up text rendering + let font_cache = cx.font_cache(); + + let font_family_id = settings.buffer_font_family; + let font_family_name = cx.font_cache().family_name(font_family_id).unwrap(); + let font_properties = Default::default(); + let font_id = font_cache + .select_font(font_family_id, &font_properties) + .unwrap(); + let font_size = settings.buffer_font_size; + + let text_style = TextStyle { + color: theme.text_color, + font_family_id: settings.buffer_font_family, + font_family_name, + font_id, + font_size, + font_properties: Default::default(), + underline: Default::default(), + }; + + let line_height = cx.font_cache.line_height(text_style.font_size); + let em_width = cx + .font_cache() + .em_width(text_style.font_id, text_style.font_size) + + 2.; + + //Resize terminal + let new_size = SizeInfo::new(size.x(), size.y(), em_width, line_height, 0., 0., false); + if !new_size.eq(&self.size) { + self.pty_tx.send(Msg::Resize(new_size)).ok(); + term.resize(new_size); + self.size = new_size; + } + + //Start rendering + let content = term.renderable_content(); + + let mut cursor = None; + let mut lines: Vec<(String, Option)> = vec![]; + let mut last_line = 0; + let mut line_count = 1; + let mut cur_chunk = String::new(); + + let mut cur_highlight = HighlightStyle { + color: Some(Color::white()), + ..Default::default() + }; + + for cell in content.display_iter { + let Indexed { + point: Point { line, .. }, + cell: Cell { + c, fg, flags, .. // TODO: Add bg and flags + }, //TODO: Learn what 'CellExtra does' + } = cell; + + if cell.point == content.cursor.point { + cursor = make_cursor(em_width, line_height, content.cursor); + } + + let new_highlight = make_style_from_cell(fg, flags); + + if line != last_line { + line_count += 1; + cur_chunk.push('\n'); + last_line = line.0; + } + + if new_highlight != cur_highlight { + lines.push((cur_chunk.clone(), Some(cur_highlight.clone()))); + cur_chunk.clear(); + cur_highlight = new_highlight; + } + cur_chunk.push(*c) + } + lines.push((cur_chunk, Some(cur_highlight))); + + let shaped_lines = layout_highlighted_chunks( + lines.iter().map(|(text, style)| (text.as_str(), *style)), + &text_style, + cx.text_layout_cache, + &cx.font_cache, + usize::MAX, + line_count, + ); + + ( + constraint.max, + LayoutState { + lines: shaped_lines, + line_height, + em_width, + cursor, + }, + ) + } + + 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 origin = bounds.origin() + vec2f(layout.em_width, 0.); + + let mut line_origin = origin; + for line in &layout.lines { + let boundaries = RectF::new(line_origin, vec2f(bounds.width(), layout.line_height)); + + if boundaries.intersects(visible_bounds) { + line.paint(line_origin, visible_bounds, layout.line_height, cx); + } + + line_origin.set_y(boundaries.max_y()); + } + + if let Some(c) = layout.cursor { + let new_origin = origin + c.origin(); + let new_cursor = RectF::new(new_origin, c.size()); + cx.scene.push_quad(Quad { + bounds: new_cursor, + background: Some(Color::red()), + border: Default::default(), + corner_radius: 0., + }); + } + + if DEBUG_GRID { + draw_debug_grid(bounds, layout, cx); + } + } + + 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 { + Event::ScrollWheel { delta, .. } => { + let vertical_scroll = + (delta.y() / layout.line_height) * ALACRITTY_SCROLL_MULTIPLIER; + let scroll = Scroll::Delta(vertical_scroll.round() as i32); + self.term.lock().scroll_display(scroll); + true + } + Event::KeyDown { + input: Some(input), .. + } => { + cx.dispatch_action(Input(input.to_string())); + true + } + _ => false, + } + } + + fn debug( + &self, + _bounds: gpui::geometry::rect::RectF, + _layout: &Self::LayoutState, + _paint: &Self::PaintState, + _cx: &gpui::DebugContext, + ) -> gpui::serde_json::Value { + json!({ + "type": "TerminalElement", + }) + } +} + +fn make_style_from_cell(fg: &AnsiColor, flags: &Flags) -> HighlightStyle { + let fg = Some(alac_color_to_gpui_color(fg)); + let underline = if flags.contains(Flags::UNDERLINE) { + Some(Underline { + color: fg, + squiggly: false, + thickness: OrderedFloat(1.), + }) + } else { + None + }; + HighlightStyle { + color: fg, + underline, + ..Default::default() + } +} + +fn alac_color_to_gpui_color(allac_color: &AnsiColor) -> Color { + match allac_color { + alacritty_terminal::ansi::Color::Named(n) => match n { + alacritty_terminal::ansi::NamedColor::Black => Color::black(), + alacritty_terminal::ansi::NamedColor::Red => Color::red(), + alacritty_terminal::ansi::NamedColor::Green => Color::green(), + alacritty_terminal::ansi::NamedColor::Yellow => Color::yellow(), + alacritty_terminal::ansi::NamedColor::Blue => Color::blue(), + alacritty_terminal::ansi::NamedColor::Magenta => Color::new(188, 63, 188, 1), + alacritty_terminal::ansi::NamedColor::Cyan => Color::new(17, 168, 205, 1), + alacritty_terminal::ansi::NamedColor::White => Color::white(), + alacritty_terminal::ansi::NamedColor::BrightBlack => Color::new(102, 102, 102, 1), + alacritty_terminal::ansi::NamedColor::BrightRed => Color::new(102, 102, 102, 1), + alacritty_terminal::ansi::NamedColor::BrightGreen => Color::new(35, 209, 139, 1), + alacritty_terminal::ansi::NamedColor::BrightYellow => Color::new(245, 245, 67, 1), + alacritty_terminal::ansi::NamedColor::BrightBlue => Color::new(59, 142, 234, 1), + alacritty_terminal::ansi::NamedColor::BrightMagenta => Color::new(214, 112, 214, 1), + alacritty_terminal::ansi::NamedColor::BrightCyan => Color::new(41, 184, 219, 1), + alacritty_terminal::ansi::NamedColor::BrightWhite => Color::new(229, 229, 229, 1), + alacritty_terminal::ansi::NamedColor::Foreground => Color::white(), + alacritty_terminal::ansi::NamedColor::Background => Color::black(), + alacritty_terminal::ansi::NamedColor::Cursor => Color::white(), + alacritty_terminal::ansi::NamedColor::DimBlack => Color::white(), + alacritty_terminal::ansi::NamedColor::DimRed => Color::white(), + alacritty_terminal::ansi::NamedColor::DimGreen => Color::white(), + alacritty_terminal::ansi::NamedColor::DimYellow => Color::white(), + alacritty_terminal::ansi::NamedColor::DimBlue => Color::white(), + alacritty_terminal::ansi::NamedColor::DimMagenta => Color::white(), + alacritty_terminal::ansi::NamedColor::DimCyan => Color::white(), + alacritty_terminal::ansi::NamedColor::DimWhite => Color::white(), + alacritty_terminal::ansi::NamedColor::BrightForeground => Color::white(), + alacritty_terminal::ansi::NamedColor::DimForeground => Color::white(), + }, //Theme defined + alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, 1), + alacritty_terminal::ansi::Color::Indexed(_) => Color::white(), //Color cube weirdness + } +} + +fn make_cursor(em_width: f32, line_height: f32, cursor: RenderableCursor) -> Option { + Some(RectF::new( + vec2f( + cursor.point.column.0 as f32 * em_width, + cursor.point.line.0 as f32 * line_height, + ), + vec2f(em_width, line_height), + )) +} + +fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) { + for col in 0..(bounds.0[2] / layout.em_width) as usize { + let rect_origin = bounds.origin() + vec2f(col as f32 * layout.em_width, 0.); + let line = RectF::new(rect_origin, vec2f(1., bounds.0[3])); + cx.scene.push_quad(Quad { + bounds: line, + background: Some(Color::green()), + border: Default::default(), + corner_radius: 0., + }); + } + for row in 0..(bounds.0[3] / layout.line_height) as usize { + let rect_origin = bounds.origin() + vec2f(0., row as f32 * layout.line_height); + let line = RectF::new(rect_origin, vec2f(bounds.0[2], 1.)); + cx.scene.push_quad(Quad { + bounds: line, + background: Some(Color::green()), + border: Default::default(), + corner_radius: 0., + }); + } +} diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index ed5d0b00ff..d8335e423b 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,47 +1,29 @@ -use std::sync::Arc; - use alacritty_terminal::{ - ansi::Color as AnsiColor, config::{Config, Program, PtyConfig}, event::{Event as AlacTermEvent, EventListener, Notify}, event_loop::{EventLoop, Msg, Notifier}, - grid::Indexed, - index::Point, - selection, + grid::Scroll, sync::FairMutex, - term::{ - cell::{Cell, Flags}, - color::Rgb, - SizeInfo, - }, + term::{color::Rgb, SizeInfo}, tty, Term, }; -use editor::CursorShape; + use futures::{ channel::mpsc::{unbounded, UnboundedSender}, - future::select, StreamExt, }; use gpui::{ - actions, - color::Color, - elements::*, - fonts::{with_font_cache, HighlightStyle, TextStyle, Underline}, - geometry::{rect::RectF, vector::vec2f}, - impl_internal_actions, - json::json, - platform::CursorStyle, - text_layout::Line, - ClipboardItem, Entity, - Event::KeyDown, - MutableAppContext, Quad, View, ViewContext, + actions, elements::*, impl_internal_actions, platform::CursorStyle, ClipboardItem, Entity, + MutableAppContext, View, ViewContext, }; -use ordered_float::OrderedFloat; use project::{Project, ProjectPath}; use settings::Settings; use smallvec::SmallVec; +use std::sync::Arc; use workspace::{Item, Workspace}; +use crate::element::TerminalEl; + //ASCII Control characters on a keyboard //Consts -> Structs -> Impls -> Functions, Vaguely in order of importance const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c' @@ -53,15 +35,17 @@ const LEFT_SEQ: &str = "\x1b[D"; const RIGHT_SEQ: &str = "\x1b[C"; const UP_SEQ: &str = "\x1b[A"; const DOWN_SEQ: &str = "\x1b[B"; -const CLEAR_SEQ: &str = "\x1b[2J"; +const CLEAR_SEQ: &str = "\x1b[H\x1b[2J"; const DEFAULT_TITLE: &str = "Terminal"; +pub mod element; + #[derive(Clone, Default, Debug, PartialEq, Eq)] -struct Input(String); +pub struct Input(pub String); actions!( terminal, - [Deploy, SIGINT, ESCAPE, Quit, DEL, RETURN, LEFT, RIGHT, UP, DOWN, TAB, Clear] + [SIGINT, ESCAPE, DEL, RETURN, LEFT, RIGHT, UP, DOWN, TAB, Clear, Paste, Deploy, Quit] ); impl_internal_actions!(terminal, [Input]); @@ -79,6 +63,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Terminal::down); cx.add_action(Terminal::tab); cx.add_action(Terminal::clear); + cx.add_action(Terminal::paste); } #[derive(Clone)] @@ -128,20 +113,18 @@ impl Terminal { }) .detach(); - //TODO: Load from settings let pty_config = PtyConfig { shell: Some(Program::Just("zsh".to_string())), working_directory: None, hold: false, }; - //TODO: Properly configure this let config = Config { pty_config: pty_config.clone(), ..Default::default() }; - //TODO: derive this + //TODO figure out how to derive this better let size_info = SizeInfo::new(400., 100.0, 5., 5., 0., 0., false); //Set up the terminal... @@ -189,16 +172,11 @@ impl Terminal { } } AlacTermEvent::PtyWrite(out) => self.write_to_pty(&Input(out), cx), - //TODO: - //What this is supposed to do is check the cursor state, then set it on the platform. - //See Processor::reset_mouse_cursor() and Processor::cursor_state() in alacritty/src/input.rs - //to see how this is Calculated. Question: Does this flow make sense with how GPUI hadles - //the mouse? AlacTermEvent::MouseCursorDirty => { //Calculate new cursor style. + //TODO //Check on correctly handling mouse events for terminals cx.platform().set_cursor_style(CursorStyle::Arrow); //??? - println!("Mouse cursor dirty") } AlacTermEvent::Title(title) => { self.title = title; @@ -251,9 +229,16 @@ impl Terminal { cx.emit(ZedTermEvent::CloseTerminal); } + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + if let Some(item) = cx.read_from_clipboard() { + self.write_to_pty(&Input(item.text().to_owned()), cx); + } + } + fn write_to_pty(&mut self, input: &Input, cx: &mut ViewContext) { //iTerm bell behavior, bell stays until terminal is interacted with self.has_bell = false; + self.term.lock().scroll_display(Scroll::Bottom); cx.emit(ZedTermEvent::TitleChanged); self.pty_tx.notify(input.0.clone().into_bytes()); } @@ -313,7 +298,10 @@ impl View for Terminal { fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { let _theme = cx.global::().theme.clone(); - TerminalEl::new(self.term.clone()) + //TODO: derive this + let size_info = SizeInfo::new(400., 100.0, 5., 5., 0., 0., false); + + TerminalEl::new(self.term.clone(), self.pty_tx.0.clone(), size_info) .contained() // .with_style(theme.terminal.container) .boxed() @@ -411,277 +399,3 @@ impl Item for Terminal { matches!(event, &ZedTermEvent::CloseTerminal) } } - -struct TerminalEl { - term: Arc>>, -} - -impl TerminalEl { - fn new(term: Arc>>) -> TerminalEl { - TerminalEl { term } - } -} - -struct LayoutState { - lines: Vec, - line_height: f32, - cursor: RectF, -} -/* TODO point calculation for selection - * take the current point's x: - * - subtract padding - * - divide by cell width - * - take the minimum of the x coord and the last colum of the size info - * Take the current point's y: - * - Subtract padding - * - Divide by cell height - * - Take the minimum of the y coord and the last line - * - * With this x and y, pass to term::viewport_to_point (module function) - * Also pass in the display offset from the term.grid().display_offset() - * (Display offset is for scrolling) - */ - -/* TODO Selection - * 1. On click, calculate the single, double, and triple click based on timings - * 2. Convert mouse location to a terminal point - * 3. Generate each of the three kinds of selection needed - * 4. Assign a selection to the terminal's selection variable - * How to render? - * 1. On mouse moved, calculate a terminal point - * 2. if (lmb_pressed || rmb_pressed) && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode() - * 3. Take the selection from the terminal, call selection.update(), and put it back - */ - -/* TODO Scroll - * 1. Convert scroll to a pixel delta (alacritty/src/input > Processor::mouse_wheel_input) - * 2. Divide by cell height - * 3. Create an alacritty_terminal::Scroll::Delta() object and call `self.terminal.scroll_display(scroll);` - * 4. Maybe do a cx.notify, just in case. - * 5. Also update the selected area, just check out for the logic alacritty/src/event.rs > ActionContext::scroll - */ -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 size = constraint.max; - //Get terminal content - let mut term = self.term.lock(); - - //Set up text rendering - - let text_style = with_font_cache(cx.font_cache.clone(), || TextStyle { - color: Color::white(), - ..Default::default() - }); - let line_height = cx.font_cache.line_height(text_style.font_size); - let em_width = cx - .font_cache() - .em_width(text_style.font_id, text_style.font_size); - - term.resize(SizeInfo::new( - size.x(), - size.y(), - em_width, - line_height, - 0., - 0., - false, - )); - - let content = term.renderable_content(); - let selection = content.selection; - - let cursor = RectF::new( - vec2f( - content.cursor.point.column.0 as f32 * em_width, - content.cursor.point.line.0 as f32 * line_height, - ), - vec2f(em_width, line_height), - ); - - let mut lines: Vec<(String, Option)> = vec![]; - let mut last_line = 0; - - let mut cur_chunk = String::new(); - - let mut cur_highlight = HighlightStyle { - color: Some(Color::white()), - ..Default::default() - }; - - let select_boxes = vec![]; - - for cell in content.display_iter { - let Indexed { - point: Point { line, .. }, - cell: Cell { - c, fg, flags, .. // TODO: Add bg and flags - }, //TODO: Learn what 'CellExtra does' - } = cell; - - if let Some(selection) = selection { - selection.contains_cell(&cell, ???, CursorShape::Block); - } - let new_highlight = make_style_from_cell(fg, flags); - HighlightStyle { - color: Some(alac_color_to_gpui_color(fg)), - ..Default::default() - }; - - if line != last_line { - cur_chunk.push('\n'); - last_line = line.0; - } - - if new_highlight != cur_highlight { - lines.push((cur_chunk.clone(), Some(cur_highlight.clone()))); - cur_chunk.clear(); - cur_highlight = new_highlight; - } - cur_chunk.push(*c) - } - lines.push((cur_chunk, Some(cur_highlight))); - - let shaped_lines = layout_highlighted_chunks( - lines.iter().map(|(text, style)| (text.as_str(), *style)), - &text_style, - cx.text_layout_cache, - &cx.font_cache, - usize::MAX, - last_line as usize, - ); - - ( - constraint.max, - LayoutState { - lines: shaped_lines, - line_height, - cursor, - }, - ) - } - - 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 new_origin = bounds.origin() + layout.cursor.origin(); - let new_cursor = RectF::new(new_origin, layout.cursor.size()); - - cx.scene.push_quad(Quad { - bounds: new_cursor, - 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), .. - } => { - cx.dispatch_action(Input(input.to_string())); - true - } - _ => false, - } - } - - fn debug( - &self, - _bounds: gpui::geometry::rect::RectF, - _layout: &Self::LayoutState, - _paint: &Self::PaintState, - _cx: &gpui::DebugContext, - ) -> gpui::serde_json::Value { - json!({ - "type": "TerminalElement", - }) - } -} - -fn make_style_from_cell(fg: &AnsiColor, flags: &Flags) -> HighlightStyle { - let fg = Some(alac_color_to_gpui_color(fg)); - let underline = if flags.contains(Flags::UNDERLINE) { - Some(Underline { - color: fg, - squiggly: false, - thickness: OrderedFloat(1.), - }) - } else { - None - }; - HighlightStyle { - color: fg, - underline, - ..Default::default() - } -} - -fn alac_color_to_gpui_color(allac_color: &AnsiColor) -> Color { - match allac_color { - alacritty_terminal::ansi::Color::Named(n) => match n { - alacritty_terminal::ansi::NamedColor::Black => Color::black(), - alacritty_terminal::ansi::NamedColor::Red => Color::red(), - alacritty_terminal::ansi::NamedColor::Green => Color::green(), - alacritty_terminal::ansi::NamedColor::Yellow => Color::yellow(), - alacritty_terminal::ansi::NamedColor::Blue => Color::blue(), - alacritty_terminal::ansi::NamedColor::Magenta => Color::new(188, 63, 188, 1), - alacritty_terminal::ansi::NamedColor::Cyan => Color::new(17, 168, 205, 1), - alacritty_terminal::ansi::NamedColor::White => Color::white(), - alacritty_terminal::ansi::NamedColor::BrightBlack => Color::new(102, 102, 102, 1), - alacritty_terminal::ansi::NamedColor::BrightRed => Color::new(102, 102, 102, 1), - alacritty_terminal::ansi::NamedColor::BrightGreen => Color::new(35, 209, 139, 1), - alacritty_terminal::ansi::NamedColor::BrightYellow => Color::new(245, 245, 67, 1), - alacritty_terminal::ansi::NamedColor::BrightBlue => Color::new(59, 142, 234, 1), - alacritty_terminal::ansi::NamedColor::BrightMagenta => Color::new(214, 112, 214, 1), - alacritty_terminal::ansi::NamedColor::BrightCyan => Color::new(41, 184, 219, 1), - alacritty_terminal::ansi::NamedColor::BrightWhite => Color::new(229, 229, 229, 1), - alacritty_terminal::ansi::NamedColor::Foreground => Color::white(), - alacritty_terminal::ansi::NamedColor::Background => Color::black(), - alacritty_terminal::ansi::NamedColor::Cursor => Color::white(), - alacritty_terminal::ansi::NamedColor::DimBlack => Color::white(), - alacritty_terminal::ansi::NamedColor::DimRed => Color::white(), - alacritty_terminal::ansi::NamedColor::DimGreen => Color::white(), - alacritty_terminal::ansi::NamedColor::DimYellow => Color::white(), - alacritty_terminal::ansi::NamedColor::DimBlue => Color::white(), - alacritty_terminal::ansi::NamedColor::DimMagenta => Color::white(), - alacritty_terminal::ansi::NamedColor::DimCyan => Color::white(), - alacritty_terminal::ansi::NamedColor::DimWhite => Color::white(), - alacritty_terminal::ansi::NamedColor::BrightForeground => Color::white(), - alacritty_terminal::ansi::NamedColor::DimForeground => Color::white(), - }, //Theme defined - alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, 1), - alacritty_terminal::ansi::Color::Indexed(_) => Color::white(), //Color cube weirdness - } -} diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e04b92dc0a..c7a7e24c5a 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -36,6 +36,7 @@ use std::{ thread, time::Duration, }; +use terminal; use theme::{ThemeRegistry, DEFAULT_THEME_NAME}; use util::{ResultExt, TryFutureExt}; use workspace::{self, AppState, NewFile, OpenPaths}; From f28fb5797f7d0f954925f6dd1252f948c7cfe363 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 28 Jun 2022 15:44:41 -0700 Subject: [PATCH 07/60] Fixed scrolling and cursor location --- crates/terminal/src/element.rs | 64 ++++++++++------------------------ 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/crates/terminal/src/element.rs b/crates/terminal/src/element.rs index c58220ac88..111a2fd654 100644 --- a/crates/terminal/src/element.rs +++ b/crates/terminal/src/element.rs @@ -6,7 +6,7 @@ use alacritty_terminal::{ sync::FairMutex, term::{ cell::{Cell, Flags}, - RenderableCursor, SizeInfo, + SizeInfo, }, Term, }; @@ -17,7 +17,7 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, json::json, text_layout::Line, - Event, PaintContext, Quad, + Event, Quad, }; use mio_extras::channel::Sender; use ordered_float::OrderedFloat; @@ -26,7 +26,6 @@ use std::sync::Arc; use crate::{Input, ZedListener}; -const DEBUG_GRID: bool = false; const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; pub struct TerminalEl { @@ -105,7 +104,6 @@ impl Element for TerminalEl { //Start rendering let content = term.renderable_content(); - let mut cursor = None; let mut lines: Vec<(String, Option)> = vec![]; let mut last_line = 0; let mut line_count = 1; @@ -124,10 +122,6 @@ impl Element for TerminalEl { }, //TODO: Learn what 'CellExtra does' } = cell; - if cell.point == content.cursor.point { - cursor = make_cursor(em_width, line_height, content.cursor); - } - let new_highlight = make_style_from_cell(fg, flags); if line != last_line { @@ -154,6 +148,20 @@ impl Element for TerminalEl { line_count, ); + let cursor_line = content.cursor.point.line.0 + content.display_offset as i32; + let mut cursor = None; + if let Some(layout_line) = cursor_line + .try_into() + .ok() + .and_then(|cursor_line: usize| shaped_lines.get(cursor_line)) + { + let cursor_x = layout_line.x_for_index(content.cursor.point.column.0); + cursor = Some(RectF::new( + vec2f(cursor_x, cursor_line as f32 * line_height), + vec2f(em_width, line_height), + )); + } + ( constraint.max, LayoutState { @@ -172,6 +180,7 @@ impl Element for TerminalEl { layout: &mut Self::LayoutState, cx: &mut gpui::PaintContext, ) -> Self::PaintState { + cx.scene.push_layer(Some(visible_bounds)); let origin = bounds.origin() + vec2f(layout.em_width, 0.); let mut line_origin = origin; @@ -190,15 +199,13 @@ impl Element for TerminalEl { let new_cursor = RectF::new(new_origin, c.size()); cx.scene.push_quad(Quad { bounds: new_cursor, - background: Some(Color::red()), + background: Some(Color::white()), border: Default::default(), corner_radius: 0., }); } - if DEBUG_GRID { - draw_debug_grid(bounds, layout, cx); - } + cx.scene.pop_layer(); } fn dispatch_event( @@ -296,36 +303,3 @@ fn alac_color_to_gpui_color(allac_color: &AnsiColor) -> Color { alacritty_terminal::ansi::Color::Indexed(_) => Color::white(), //Color cube weirdness } } - -fn make_cursor(em_width: f32, line_height: f32, cursor: RenderableCursor) -> Option { - Some(RectF::new( - vec2f( - cursor.point.column.0 as f32 * em_width, - cursor.point.line.0 as f32 * line_height, - ), - vec2f(em_width, line_height), - )) -} - -fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) { - for col in 0..(bounds.0[2] / layout.em_width) as usize { - let rect_origin = bounds.origin() + vec2f(col as f32 * layout.em_width, 0.); - let line = RectF::new(rect_origin, vec2f(1., bounds.0[3])); - cx.scene.push_quad(Quad { - bounds: line, - background: Some(Color::green()), - border: Default::default(), - corner_radius: 0., - }); - } - for row in 0..(bounds.0[3] / layout.line_height) as usize { - let rect_origin = bounds.origin() + vec2f(0., row as f32 * layout.line_height); - let line = RectF::new(rect_origin, vec2f(bounds.0[2], 1.)); - cx.scene.push_quad(Quad { - bounds: line, - background: Some(Color::green()), - border: Default::default(), - corner_radius: 0., - }); - } -} From 38ed70d5ccb4f6f2205d393bf12a3737a160e614 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 28 Jun 2022 17:07:18 -0700 Subject: [PATCH 08/60] Added theme support --- crates/terminal/src/element.rs | 72 +++++++++++++++--------------- crates/theme/src/theme.rs | 34 ++++++++++++++ styles/src/styleTree/app.ts | 2 + styles/src/styleTree/terminal.ts | 35 +++++++++++++++ styles/src/themes/cave.ts | 3 ++ styles/src/themes/common/base16.ts | 22 ++++++--- styles/src/themes/common/theme.ts | 3 ++ 7 files changed, 131 insertions(+), 40 deletions(-) create mode 100644 styles/src/styleTree/terminal.ts diff --git a/crates/terminal/src/element.rs b/crates/terminal/src/element.rs index 111a2fd654..04828e8d65 100644 --- a/crates/terminal/src/element.rs +++ b/crates/terminal/src/element.rs @@ -23,6 +23,7 @@ use mio_extras::channel::Sender; use ordered_float::OrderedFloat; use settings::Settings; use std::sync::Arc; +use theme::TerminalStyle; use crate::{Input, ZedListener}; @@ -62,7 +63,8 @@ impl Element for TerminalEl { ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { let size = constraint.max; let settings = cx.global::(); - let theme = &settings.theme.editor; + let editor_theme = &settings.theme.editor; + let terminal_theme = &settings.theme.terminal; //Get terminal let mut term = self.term.lock(); @@ -78,7 +80,7 @@ impl Element for TerminalEl { let font_size = settings.buffer_font_size; let text_style = TextStyle { - color: theme.text_color, + color: editor_theme.text_color, font_family_id: settings.buffer_font_family, font_family_name, font_id, @@ -122,7 +124,7 @@ impl Element for TerminalEl { }, //TODO: Learn what 'CellExtra does' } = cell; - let new_highlight = make_style_from_cell(fg, flags); + let new_highlight = make_style_from_cell(fg, flags, &terminal_theme); if line != last_line { line_count += 1; @@ -248,8 +250,8 @@ impl Element for TerminalEl { } } -fn make_style_from_cell(fg: &AnsiColor, flags: &Flags) -> HighlightStyle { - let fg = Some(alac_color_to_gpui_color(fg)); +fn make_style_from_cell(fg: &AnsiColor, flags: &Flags, style: &TerminalStyle) -> HighlightStyle { + let fg = Some(alac_color_to_gpui_color(fg, style)); let underline = if flags.contains(Flags::UNDERLINE) { Some(Underline { color: fg, @@ -266,38 +268,38 @@ fn make_style_from_cell(fg: &AnsiColor, flags: &Flags) -> HighlightStyle { } } -fn alac_color_to_gpui_color(allac_color: &AnsiColor) -> Color { +fn alac_color_to_gpui_color(allac_color: &AnsiColor, style: &TerminalStyle) -> Color { match allac_color { alacritty_terminal::ansi::Color::Named(n) => match n { - alacritty_terminal::ansi::NamedColor::Black => Color::black(), - alacritty_terminal::ansi::NamedColor::Red => Color::red(), - alacritty_terminal::ansi::NamedColor::Green => Color::green(), - alacritty_terminal::ansi::NamedColor::Yellow => Color::yellow(), - alacritty_terminal::ansi::NamedColor::Blue => Color::blue(), - alacritty_terminal::ansi::NamedColor::Magenta => Color::new(188, 63, 188, 1), - alacritty_terminal::ansi::NamedColor::Cyan => Color::new(17, 168, 205, 1), - alacritty_terminal::ansi::NamedColor::White => Color::white(), - alacritty_terminal::ansi::NamedColor::BrightBlack => Color::new(102, 102, 102, 1), - alacritty_terminal::ansi::NamedColor::BrightRed => Color::new(102, 102, 102, 1), - alacritty_terminal::ansi::NamedColor::BrightGreen => Color::new(35, 209, 139, 1), - alacritty_terminal::ansi::NamedColor::BrightYellow => Color::new(245, 245, 67, 1), - alacritty_terminal::ansi::NamedColor::BrightBlue => Color::new(59, 142, 234, 1), - alacritty_terminal::ansi::NamedColor::BrightMagenta => Color::new(214, 112, 214, 1), - alacritty_terminal::ansi::NamedColor::BrightCyan => Color::new(41, 184, 219, 1), - alacritty_terminal::ansi::NamedColor::BrightWhite => Color::new(229, 229, 229, 1), - alacritty_terminal::ansi::NamedColor::Foreground => Color::white(), - alacritty_terminal::ansi::NamedColor::Background => Color::black(), - alacritty_terminal::ansi::NamedColor::Cursor => Color::white(), - alacritty_terminal::ansi::NamedColor::DimBlack => Color::white(), - alacritty_terminal::ansi::NamedColor::DimRed => Color::white(), - alacritty_terminal::ansi::NamedColor::DimGreen => Color::white(), - alacritty_terminal::ansi::NamedColor::DimYellow => Color::white(), - alacritty_terminal::ansi::NamedColor::DimBlue => Color::white(), - alacritty_terminal::ansi::NamedColor::DimMagenta => Color::white(), - alacritty_terminal::ansi::NamedColor::DimCyan => Color::white(), - alacritty_terminal::ansi::NamedColor::DimWhite => Color::white(), - alacritty_terminal::ansi::NamedColor::BrightForeground => Color::white(), - alacritty_terminal::ansi::NamedColor::DimForeground => Color::white(), + alacritty_terminal::ansi::NamedColor::Black => style.black, + alacritty_terminal::ansi::NamedColor::Red => style.red, + alacritty_terminal::ansi::NamedColor::Green => style.green, + alacritty_terminal::ansi::NamedColor::Yellow => style.yellow, + alacritty_terminal::ansi::NamedColor::Blue => style.blue, + alacritty_terminal::ansi::NamedColor::Magenta => style.magenta, + alacritty_terminal::ansi::NamedColor::Cyan => style.cyan, + alacritty_terminal::ansi::NamedColor::White => style.white, + alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black, + alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red, + alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green, + alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow, + alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue, + alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta, + alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan, + alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white, + alacritty_terminal::ansi::NamedColor::Foreground => style.foreground, + alacritty_terminal::ansi::NamedColor::Background => style.background, + alacritty_terminal::ansi::NamedColor::Cursor => style.cursor, + alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black, + alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red, + alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green, + alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow, + alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue, + alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta, + alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan, + alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white, + alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground, + alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground, }, //Theme defined alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, 1), alacritty_terminal::ansi::Color::Indexed(_) => Color::white(), //Color cube weirdness diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index ae269c00cb..184b1880f0 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -33,6 +33,7 @@ pub struct Theme { pub contact_notification: ContactNotification, pub update_notification: UpdateNotification, pub tooltip: TooltipStyle, + pub terminal: TerminalStyle, } #[derive(Deserialize, Default)] @@ -633,3 +634,36 @@ pub struct HoverPopover { pub prose: TextStyle, pub highlight: Color, } + +#[derive(Clone, Deserialize, Default)] +pub struct TerminalStyle { + pub black: Color, + pub red: Color, + pub green: Color, + pub yellow: Color, + pub blue: Color, + pub magenta: Color, + pub cyan: Color, + pub white: Color, + pub bright_black: Color, + pub bright_red: Color, + pub bright_green: Color, + pub bright_yellow: Color, + pub bright_blue: Color, + pub bright_magenta: Color, + pub bright_cyan: Color, + pub bright_white: Color, + pub foreground: Color, + pub background: Color, + pub cursor: Color, + pub dim_black: Color, + pub dim_red: Color, + pub dim_green: Color, + pub dim_yellow: Color, + pub dim_blue: Color, + pub dim_magenta: Color, + pub dim_cyan: Color, + pub dim_white: Color, + pub bright_foreground: Color, + pub dim_foreground: Color, +} diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index e015895e9c..fe67cf701d 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -14,6 +14,7 @@ import projectDiagnostics from "./projectDiagnostics"; import contactNotification from "./contactNotification"; import updateNotification from "./updateNotification"; import tooltip from "./tooltip"; +import terminal from "./terminal"; export const panel = { padding: { top: 12, bottom: 12 }, @@ -41,5 +42,6 @@ export default function app(theme: Theme): Object { contactNotification: contactNotification(theme), updateNotification: updateNotification(theme), tooltip: tooltip(theme), + terminal: terminal(theme), }; } diff --git a/styles/src/styleTree/terminal.ts b/styles/src/styleTree/terminal.ts new file mode 100644 index 0000000000..ef9e4f93dd --- /dev/null +++ b/styles/src/styleTree/terminal.ts @@ -0,0 +1,35 @@ +import Theme from "../themes/common/theme"; + +export default function terminal(theme: Theme) { + return { + black: theme.ramps.neutral(0).hex(), + red: theme.ramps.red(0.5).hex(), + green: theme.ramps.green(0.5).hex(), + yellow: theme.ramps.yellow(0.5).hex(), + blue: theme.ramps.blue(0.5).hex(), + magenta: theme.ramps.magenta(0.5).hex(), + cyan: theme.ramps.cyan(0.5).hex(), + white: theme.ramps.neutral(7).hex(), + brightBlack: theme.ramps.neutral(2).hex(), + brightRed: theme.ramps.red(0.25).hex(), + brightGreen: theme.ramps.green(0.25).hex(), + brightYellow: theme.ramps.yellow(0.25).hex(), + brightBlue: theme.ramps.blue(0.25).hex(), + brightMagenta: theme.ramps.magenta(0.25).hex(), + brightCyan: theme.ramps.cyan(0.25).hex(), + brightWhite: theme.ramps.neutral(7).hex(), + foreground: theme.ramps.neutral(7).hex(), + background: theme.ramps.neutral(0).hex(), + cursor: theme.ramps.neutral(7).hex(), + dimBlack: theme.ramps.neutral(7).hex(), + dimRed: theme.ramps.red(0.75).hex(), + dimGreen: theme.ramps.green(0.75).hex(), + dimYellow: theme.ramps.yellow(0.75).hex(), + dimBlue: theme.ramps.blue(0.75).hex(), + dimMagenta: theme.ramps.magenta(0.75).hex(), + dimCyan: theme.ramps.cyan(0.75).hex(), + dimWhite: theme.ramps.neutral(5).hex(), + brightForeground: theme.ramps.neutral(7).hex(), + dimForeground: theme.ramps.neutral(0).hex(), + }; +} \ No newline at end of file diff --git a/styles/src/themes/cave.ts b/styles/src/themes/cave.ts index 2e66f4baf4..d588f84c53 100644 --- a/styles/src/themes/cave.ts +++ b/styles/src/themes/cave.ts @@ -26,3 +26,6 @@ const ramps = { export const dark = createTheme(`${name}-dark`, false, ramps); export const light = createTheme(`${name}-light`, true, ramps); + +console.log(JSON.stringify(dark.ramps.neutral.domain())) +console.log(JSON.stringify(light.ramps.neutral.domain())) \ No newline at end of file diff --git a/styles/src/themes/common/base16.ts b/styles/src/themes/common/base16.ts index 21a02cde25..729cf32ee5 100644 --- a/styles/src/themes/common/base16.ts +++ b/styles/src/themes/common/base16.ts @@ -13,15 +13,25 @@ export function colorRamp(color: Color): Scale { export function createTheme( name: string, isLight: boolean, - ramps: { [rampName: string]: Scale }, + color_ramps: { [rampName: string]: Scale }, ): Theme { + let ramps: typeof color_ramps = {}; + // Chromajs mutates the underlying ramp when you call domain. This causes problems because + // we now store the ramps object in the theme so that we can pull colors out of them. + // So instead of calling domain and storing the result, we have to construct new ramps for each + // theme so that we don't modify the passed in ramps. + // This combined with an error in the type definitions for chroma js means we have to cast the colors + // function to any in order to get the colors back out from the original ramps. if (isLight) { - for (var rampName in ramps) { - ramps[rampName] = ramps[rampName].domain([1, 0]); + for (var rampName in color_ramps) { + ramps[rampName] = chroma.scale((color_ramps[rampName].colors as any)()).domain([1, 0]); } - ramps.neutral = ramps.neutral.domain([7, 0]); + ramps.neutral = chroma.scale((color_ramps.neutral.colors as any)()).domain([7, 0]); } else { - ramps.neutral = ramps.neutral.domain([0, 7]); + for (var rampName in color_ramps) { + ramps[rampName] = chroma.scale((color_ramps[rampName].colors as any)()).domain([0, 1]); + } + ramps.neutral = chroma.scale((color_ramps.neutral.colors as any)()).domain([0, 7]); } let blend = isLight ? 0.12 : 0.24; @@ -237,6 +247,7 @@ export function createTheme( return { name, + isLight, backgroundColor, borderColor, textColor, @@ -245,5 +256,6 @@ export function createTheme( syntax, player, shadow, + ramps, }; } diff --git a/styles/src/themes/common/theme.ts b/styles/src/themes/common/theme.ts index 92b1f8eff8..7f32f48974 100644 --- a/styles/src/themes/common/theme.ts +++ b/styles/src/themes/common/theme.ts @@ -1,3 +1,4 @@ +import { Scale } from "chroma-js"; import { FontWeight } from "../../common"; import { withOpacity } from "../../utils/color"; @@ -60,6 +61,7 @@ export interface Syntax { export default interface Theme { name: string; + isLight: boolean, backgroundColor: { // Basically just Title Bar // Lowest background level @@ -155,4 +157,5 @@ export default interface Theme { 8: Player; }, shadow: string; + ramps: { [rampName: string]: Scale }; } From e3834409ddc4f96b1c3ff7c7b7d154ffa0395f35 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 28 Jun 2022 18:28:13 -0700 Subject: [PATCH 09/60] Fixed focus issues with scrolling and input --- assets/keymaps/default.json | 1 - crates/gpui/src/presenter.rs | 14 +++++++++ crates/terminal/src/element.rs | 56 ++++++++++++++++++++++++++------- crates/terminal/src/terminal.rs | 41 +++++++++++++++--------- 4 files changed, 84 insertions(+), 28 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 232fd4db1f..8da4b2a9ca 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -366,7 +366,6 @@ "up": "terminal::UP", "down": "terminal::DOWN", "tab": "terminal::TAB", - "cmd-k": "terminal::Clear", "cmd-v": "terminal::Paste" } } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 88e4d0a498..6285b1be99 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -703,6 +703,20 @@ impl<'a> EventContext<'a> { self.view_stack.last().copied() } + pub fn is_parent_view_focused(&self) -> bool { + if let Some(parent_view_id) = self.view_stack.last() { + self.app.focused_view_id(self.window_id) == Some(*parent_view_id) + } else { + false + } + } + + pub fn focus_parent_view(&mut self) { + if let Some(parent_view_id) = self.view_stack.last() { + self.app.focus(self.window_id, Some(*parent_view_id)) + } + } + pub fn dispatch_any_action(&mut self, action: Box) { self.dispatched_actions.push(DispatchDirective { dispatcher_view_id: self.view_stack.last().copied(), diff --git a/crates/terminal/src/element.rs b/crates/terminal/src/element.rs index 04828e8d65..08d49ec6d8 100644 --- a/crates/terminal/src/element.rs +++ b/crates/terminal/src/element.rs @@ -17,12 +17,12 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, json::json, text_layout::Line, - Event, Quad, + Event, MouseRegion, Quad, }; use mio_extras::channel::Sender; use ordered_float::OrderedFloat; use settings::Settings; -use std::sync::Arc; +use std::{rc::Rc, sync::Arc}; use theme::TerminalStyle; use crate::{Input, ZedListener}; @@ -33,6 +33,7 @@ pub struct TerminalEl { term: Arc>>, pty_tx: Sender, size: SizeInfo, + view_id: usize, } impl TerminalEl { @@ -40,8 +41,14 @@ impl TerminalEl { term: Arc>>, pty_tx: Sender, size: SizeInfo, + view_id: usize, ) -> TerminalEl { - TerminalEl { term, pty_tx, size } + TerminalEl { + term, + pty_tx, + size, + view_id, + } } } @@ -183,6 +190,21 @@ impl Element for TerminalEl { cx: &mut gpui::PaintContext, ) -> Self::PaintState { cx.scene.push_layer(Some(visible_bounds)); + + cx.scene.push_mouse_region(MouseRegion { + view_id: self.view_id, + discriminant: None, + bounds: visible_bounds, + hover: None, + mouse_down: Some(Rc::new(|_, cx| cx.focus_parent_view())), + click: None, + right_mouse_down: None, + right_click: None, + drag: None, + mouse_down_out: None, + right_mouse_down_out: None, + }); + let origin = bounds.origin() + vec2f(layout.em_width, 0.); let mut line_origin = origin; @@ -214,24 +236,34 @@ impl Element for TerminalEl { &mut self, event: &gpui::Event, _bounds: gpui::geometry::rect::RectF, - _visible_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 { - Event::ScrollWheel { delta, .. } => { - let vertical_scroll = - (delta.y() / layout.line_height) * ALACRITTY_SCROLL_MULTIPLIER; - let scroll = Scroll::Delta(vertical_scroll.round() as i32); - self.term.lock().scroll_display(scroll); - true + Event::ScrollWheel { + delta, position, .. + } => { + if visible_bounds.contains_point(*position) { + let vertical_scroll = + (delta.y() / layout.line_height) * ALACRITTY_SCROLL_MULTIPLIER; + let scroll = Scroll::Delta(vertical_scroll.round() as i32); + self.term.lock().scroll_display(scroll); + true + } else { + false + } } Event::KeyDown { input: Some(input), .. } => { - cx.dispatch_action(Input(input.to_string())); - true + if cx.is_parent_view_focused() { + cx.dispatch_action(Input(input.to_string())); + true + } else { + false + } } _ => false, } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index d8335e423b..f375ddbd91 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -84,13 +84,14 @@ struct Terminal { has_bell: bool, //Currently using iTerm bell, show bell emoji in tab until input is received } -enum ZedTermEvent { +enum Event { TitleChanged, CloseTerminal, + Activate, } impl Entity for Terminal { - type Event = ZedTermEvent; + type Event = Event; } impl Terminal { @@ -166,7 +167,7 @@ impl Terminal { if !cx.is_self_focused() { //Need to figure out how to trigger a redraw when not in focus self.has_new_content = true; //Change tab content - cx.emit(ZedTermEvent::TitleChanged); + cx.emit(Event::TitleChanged); } else { cx.notify() } @@ -180,11 +181,11 @@ impl Terminal { } AlacTermEvent::Title(title) => { self.title = title; - cx.emit(ZedTermEvent::TitleChanged); + cx.emit(Event::TitleChanged); } AlacTermEvent::ResetTitle => { self.title = DEFAULT_TITLE.to_string(); - cx.emit(ZedTermEvent::TitleChanged); + cx.emit(Event::TitleChanged); } AlacTermEvent::ClipboardStore(_, data) => { cx.write_to_clipboard(ClipboardItem::new(data)) @@ -209,7 +210,7 @@ impl Terminal { } AlacTermEvent::Bell => { self.has_bell = true; - cx.emit(ZedTermEvent::TitleChanged); + cx.emit(Event::TitleChanged); } AlacTermEvent::Exit => self.quit(&Quit, cx), } @@ -226,7 +227,7 @@ impl Terminal { } fn quit(&mut self, _: &Quit, cx: &mut ViewContext) { - cx.emit(ZedTermEvent::CloseTerminal); + cx.emit(Event::CloseTerminal); } fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { @@ -239,7 +240,7 @@ impl Terminal { //iTerm bell behavior, bell stays until terminal is interacted with self.has_bell = false; self.term.lock().scroll_display(Scroll::Bottom); - cx.emit(ZedTermEvent::TitleChanged); + cx.emit(Event::TitleChanged); self.pty_tx.notify(input.0.clone().into_bytes()); } @@ -301,13 +302,19 @@ impl View for Terminal { //TODO: derive this let size_info = SizeInfo::new(400., 100.0, 5., 5., 0., 0., false); - TerminalEl::new(self.term.clone(), self.pty_tx.0.clone(), size_info) - .contained() - // .with_style(theme.terminal.container) - .boxed() + TerminalEl::new( + self.term.clone(), + self.pty_tx.0.clone(), + size_info, + cx.view_id(), + ) + .contained() + // .with_style(theme.terminal.container) + .boxed() } - fn on_focus(&mut self, _: &mut ViewContext) { + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Activate); self.has_new_content = false; } } @@ -392,10 +399,14 @@ impl Item for Terminal { } fn should_update_tab_on_event(event: &Self::Event) -> bool { - matches!(event, &ZedTermEvent::TitleChanged) + matches!(event, &Event::TitleChanged) } fn should_close_item_on_event(event: &Self::Event) -> bool { - matches!(event, &ZedTermEvent::CloseTerminal) + matches!(event, &Event::CloseTerminal) + } + + fn should_activate_item_on_event(event: &Self::Event) -> bool { + matches!(event, &Event::Activate) } } From db95c0d0e1130acbe50e68d6feac1585cc38d021 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 28 Jun 2022 18:45:26 -0700 Subject: [PATCH 10/60] Removed dead clear code --- crates/terminal/src/terminal.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index f375ddbd91..421cbc0433 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,4 +1,5 @@ use alacritty_terminal::{ + ansi::{ClearMode, Handler}, config::{Config, Program, PtyConfig}, event::{Event as AlacTermEvent, EventListener, Notify}, event_loop::{EventLoop, Msg, Notifier}, @@ -35,7 +36,6 @@ const LEFT_SEQ: &str = "\x1b[D"; const RIGHT_SEQ: &str = "\x1b[C"; const UP_SEQ: &str = "\x1b[A"; const DOWN_SEQ: &str = "\x1b[B"; -const CLEAR_SEQ: &str = "\x1b[H\x1b[2J"; const DEFAULT_TITLE: &str = "Terminal"; pub mod element; @@ -62,7 +62,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Terminal::up); cx.add_action(Terminal::down); cx.add_action(Terminal::tab); - cx.add_action(Terminal::clear); cx.add_action(Terminal::paste); } @@ -279,10 +278,6 @@ impl Terminal { fn right(&mut self, _: &RIGHT, cx: &mut ViewContext) { self.write_to_pty(&Input(RIGHT_SEQ.to_string()), cx); } - - fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { - self.write_to_pty(&Input(CLEAR_SEQ.to_string()), cx); - } } impl Drop for Terminal { From ff44ddc0771e6909fb20465ca487caf9d6ac1799 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Jun 2022 09:38:02 -0700 Subject: [PATCH 11/60] =?UTF-8?q?Fixed=20warnings=20=F0=9F=98=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/terminal/src/terminal.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 421cbc0433..8fc01f2f9a 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,5 +1,4 @@ use alacritty_terminal::{ - ansi::{ClearMode, Handler}, config::{Config, Program, PtyConfig}, event::{Event as AlacTermEvent, EventListener, Notify}, event_loop::{EventLoop, Msg, Notifier}, From c9d23dba6c94eeb014aaf1436c6bf43649ebd488 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Wed, 29 Jun 2022 11:58:12 -0700 Subject: [PATCH 12/60] Update command palette filter from vim mode more proactively --- crates/gpui/src/app.rs | 8 ++------ crates/vim/src/editor_events.rs | 4 ++-- crates/vim/src/vim.rs | 31 ++++++++++++++++++------------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 5fa5544808..218bf08949 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1634,14 +1634,10 @@ impl MutableAppContext { pub fn default_global(&mut self) -> &T { let type_id = TypeId::of::(); self.update(|this| { - if !this.globals.contains_key(&type_id) { + if let Entry::Vacant(entry) = this.cx.globals.entry(type_id) { + entry.insert(Box::new(T::default())); this.notify_global(type_id); } - - this.cx - .globals - .entry(type_id) - .or_insert_with(|| Box::new(T::default())); }); self.globals.get(&type_id).unwrap().downcast_ref().unwrap() } diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 8837f264d3..6ad0cb77c4 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -13,7 +13,7 @@ pub fn init(cx: &mut MutableAppContext) { fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) { cx.update_default_global(|vim: &mut Vim, cx| { vim.editors.insert(editor.id(), editor.downgrade()); - vim.sync_editor_options(cx); + vim.sync_vim_settings(cx); }) } @@ -42,7 +42,7 @@ fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppCont vim.active_editor = None; } } - vim.sync_editor_options(cx); + vim.sync_vim_settings(cx); }) } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 0ee1abc3ab..82567f9829 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -51,6 +51,10 @@ pub fn init(cx: &mut MutableAppContext) { } }); + // Sync initial settings with the rest of the app + Vim::update(cx, |state, cx| state.sync_vim_settings(cx)); + + // Any time settings change, update vim mode to match cx.observe_global::(|cx| { Vim::update(cx, |state, cx| { state.set_enabled(cx.global::().vim_mode, cx) @@ -95,23 +99,23 @@ impl Vim { fn switch_mode(&mut self, mode: Mode, cx: &mut MutableAppContext) { self.state.mode = mode; self.state.operator_stack.clear(); - self.sync_editor_options(cx); + self.sync_vim_settings(cx); } fn push_operator(&mut self, operator: Operator, cx: &mut MutableAppContext) { self.state.operator_stack.push(operator); - self.sync_editor_options(cx); + self.sync_vim_settings(cx); } fn pop_operator(&mut self, cx: &mut MutableAppContext) -> Operator { let popped_operator = self.state.operator_stack.pop().expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config"); - self.sync_editor_options(cx); + self.sync_vim_settings(cx); popped_operator } fn clear_operator(&mut self, cx: &mut MutableAppContext) { self.state.operator_stack.clear(); - self.sync_editor_options(cx); + self.sync_vim_settings(cx); } fn active_operator(&self) -> Option { @@ -125,21 +129,22 @@ impl Vim { if enabled { self.state.mode = Mode::Normal; } - cx.update_default_global::(|filter, _| { - if enabled { - filter.filtered_namespaces.remove("vim"); - } else { - filter.filtered_namespaces.insert("vim"); - } - }); - self.sync_editor_options(cx); + self.sync_vim_settings(cx); } } - fn sync_editor_options(&self, cx: &mut MutableAppContext) { + fn sync_vim_settings(&self, cx: &mut MutableAppContext) { let state = &self.state; let cursor_shape = state.cursor_shape(); + cx.update_default_global::(|filter, _| { + if self.enabled { + filter.filtered_namespaces.remove("vim"); + } else { + filter.filtered_namespaces.insert("vim"); + } + }); + for editor in self.editors.values() { if let Some(editor) = editor.upgrade(cx) { editor.update(cx, |editor, cx| { From 93dfc63f1c43d932153af1144e1af494527d4b96 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Jun 2022 13:07:44 -0700 Subject: [PATCH 13/60] Added a test with max, cludged a fix for resizing --- .swp | Bin 0 -> 12288 bytes assets/keymaps/default.json | 18 +- crates/terminal/Cargo.toml | 3 + crates/terminal/src/terminal.rs | 97 +++++--- .../src/{element.rs => terminal_element.rs} | 215 ++++++++++-------- styles/src/themes/cave.ts | 5 +- 6 files changed, 203 insertions(+), 135 deletions(-) create mode 100644 .swp rename crates/terminal/src/{element.rs => terminal_element.rs} (67%) diff --git a/.swp b/.swp new file mode 100644 index 0000000000000000000000000000000000000000..5f9940d5044d36923e5abbe428dace63080fbef8 GIT binary patch literal 12288 zcmYc?2=nw+u+TGNU|?Vn01*)Uu7-gjH#0l2GA9utfQR&js4Ld>O-y#m&(GF%%q-H& z$xlwq!J}zZYBU5!Lx9d9z!_?6WM}}gR9Q(;L0Bjh#2m$=Aut*OqaiRF0;3@?8Umvs zFd71*Aut*OBP0Y$3K$vc85kItp#Bwt(u`;{lsig|hQMeDjE2By2#kinXb6mkz-S1J zhQMeDjE2By2#kinXb23!5J*g6U{K^|U})us%>Tps{~P!j7#8v~FwEy?V3^I%z%Y}a zfnf$e1H*KF28L<;3=C8F85kzWfq|inp)94C zA)g^NEi*qWD>F4ct2{HcoS`7QI3+Q=Aip@fAT2w;Agwq%zaS;8AU`_|B$$#`ke#1W moL`WgR$P#s4;9NV$Sy8VP0KG&O-s*DElMv>g`k4`bOr#Y_e~4{ literal 0 HcmV?d00001 diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 8da4b2a9ca..c4e98d9094 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -356,16 +356,16 @@ { "context": "Terminal", "bindings": { - "ctrl-c": "terminal::SIGINT", - "escape": "terminal::ESCAPE", + "ctrl-c": "terminal::Sigint", + "escape": "terminal::Escape", "ctrl-d": "terminal::Quit", - "backspace": "terminal::DEL", - "enter": "terminal::RETURN", - "left": "terminal::LEFT", - "right": "terminal::RIGHT", - "up": "terminal::UP", - "down": "terminal::DOWN", - "tab": "terminal::TAB", + "backspace": "terminal::Del", + "enter": "terminal::Return", + "left": "terminal::Left", + "right": "terminal::Right", + "up": "terminal::Up", + "down": "terminal::Down", + "tab": "terminal::Tab", "cmd-v": "terminal::Paste" } } diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 3cb4d631f3..175c741421 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -20,3 +20,6 @@ smallvec = { version = "1.6", features = ["union"] } mio-extras = "2.0.6" futures = "0.3" ordered-float = "2.1.1" + +[dev-dependencies] +gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 8fc01f2f9a..9adcb3122e 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -22,7 +22,7 @@ use smallvec::SmallVec; use std::sync::Arc; use workspace::{Item, Workspace}; -use crate::element::TerminalEl; +use crate::terminal_element::TerminalEl; //ASCII Control characters on a keyboard //Consts -> Structs -> Impls -> Functions, Vaguely in order of importance @@ -37,16 +37,19 @@ const UP_SEQ: &str = "\x1b[A"; const DOWN_SEQ: &str = "\x1b[B"; const DEFAULT_TITLE: &str = "Terminal"; -pub mod element; +pub mod terminal_element; #[derive(Clone, Default, Debug, PartialEq, Eq)] pub struct Input(pub String); +#[derive(Clone, Debug, PartialEq)] +pub struct ScrollTerminal(pub i32); + actions!( terminal, - [SIGINT, ESCAPE, DEL, RETURN, LEFT, RIGHT, UP, DOWN, TAB, Clear, Paste, Deploy, Quit] + [Sigint, Escape, Del, Return, Left, Right, Up, Down, Tab, Clear, Paste, Deploy, Quit] ); -impl_internal_actions!(terminal, [Input]); +impl_internal_actions!(terminal, [Input, ScrollTerminal]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(Terminal::deploy); @@ -62,6 +65,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Terminal::down); cx.add_action(Terminal::tab); cx.add_action(Terminal::paste); + cx.add_action(Terminal::scroll_terminal); } #[derive(Clone)] @@ -74,15 +78,16 @@ impl EventListener for ZedListener { } ///A terminal renderer. -struct Terminal { +pub struct Terminal { pty_tx: Notifier, term: Arc>>, title: String, has_new_content: bool, has_bell: bool, //Currently using iTerm bell, show bell emoji in tab until input is received + cur_size: SizeInfo, } -enum Event { +pub enum Event { TitleChanged, CloseTerminal, Activate, @@ -124,7 +129,7 @@ impl Terminal { }; //TODO figure out how to derive this better - let size_info = SizeInfo::new(400., 100.0, 5., 5., 0., 0., false); + let size_info = SizeInfo::new(200., 100.0, 5., 5., 0., 0., false); //Set up the terminal... let term = Term::new(&config, size_info, ZedListener(events_tx.clone())); @@ -151,6 +156,7 @@ impl Terminal { pty_tx, has_new_content: false, has_bell: false, + cur_size: size_info, } } @@ -214,6 +220,18 @@ impl Terminal { } } + fn set_size(&mut self, new_size: SizeInfo) { + if new_size != self.cur_size { + self.pty_tx.0.send(Msg::Resize(new_size)).ok(); + self.term.lock().resize(new_size); + self.cur_size = new_size; + } + } + + fn scroll_terminal(&mut self, scroll: &ScrollTerminal, _: &mut ViewContext) { + self.term.lock().scroll_display(Scroll::Delta(scroll.0)); + } + ///Create a new Terminal fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx))), cx); @@ -242,39 +260,39 @@ impl Terminal { self.pty_tx.notify(input.0.clone().into_bytes()); } - fn up(&mut self, _: &UP, cx: &mut ViewContext) { + fn up(&mut self, _: &Up, cx: &mut ViewContext) { self.write_to_pty(&Input(UP_SEQ.to_string()), cx); } - fn down(&mut self, _: &DOWN, cx: &mut ViewContext) { + fn down(&mut self, _: &Down, cx: &mut ViewContext) { self.write_to_pty(&Input(DOWN_SEQ.to_string()), cx); } - fn tab(&mut self, _: &TAB, cx: &mut ViewContext) { + fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { self.write_to_pty(&Input(TAB_CHAR.to_string()), cx); } - fn send_sigint(&mut self, _: &SIGINT, cx: &mut ViewContext) { + fn send_sigint(&mut self, _: &Sigint, cx: &mut ViewContext) { self.write_to_pty(&Input(ETX_CHAR.to_string()), cx); } - fn escape(&mut self, _: &ESCAPE, cx: &mut ViewContext) { + fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { self.write_to_pty(&Input(ESC_CHAR.to_string()), cx); } - fn del(&mut self, _: &DEL, cx: &mut ViewContext) { + fn del(&mut self, _: &Del, cx: &mut ViewContext) { self.write_to_pty(&Input(DEL_CHAR.to_string()), cx); } - fn carriage_return(&mut self, _: &RETURN, cx: &mut ViewContext) { + fn carriage_return(&mut self, _: &Return, cx: &mut ViewContext) { self.write_to_pty(&Input(CARRIAGE_RETURN_CHAR.to_string()), cx); } - fn left(&mut self, _: &LEFT, cx: &mut ViewContext) { + fn left(&mut self, _: &Left, cx: &mut ViewContext) { self.write_to_pty(&Input(LEFT_SEQ.to_string()), cx); } - fn right(&mut self, _: &RIGHT, cx: &mut ViewContext) { + fn right(&mut self, _: &Right, cx: &mut ViewContext) { self.write_to_pty(&Input(RIGHT_SEQ.to_string()), cx); } } @@ -291,20 +309,10 @@ impl View for Terminal { } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let _theme = cx.global::().theme.clone(); - - //TODO: derive this - let size_info = SizeInfo::new(400., 100.0, 5., 5., 0., 0., false); - - TerminalEl::new( - self.term.clone(), - self.pty_tx.0.clone(), - size_info, - cx.view_id(), - ) - .contained() - // .with_style(theme.terminal.container) - .boxed() + TerminalEl::new(cx.handle()) + .contained() + // .with_style(theme.terminal.container) + .boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { @@ -404,3 +412,32 @@ impl Item for Terminal { matches!(event, &Event::Activate) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::terminal_element::build_chunks; + use gpui::TestAppContext; + + #[gpui::test] + async fn test_terminal(cx: &mut TestAppContext) { + let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx)); + + terminal.update(cx, |terminal, cx| { + terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx); + terminal.carriage_return(&Return, cx); + }); + + terminal + .condition(cx, |terminal, _cx| { + let term = terminal.term.clone(); + let (chunks, _) = build_chunks( + term.lock().renderable_content().display_iter, + &Default::default(), + ); + let content = chunks.iter().map(|e| e.0.trim()).collect::(); + content.contains("7") + }) + .await; + } +} diff --git a/crates/terminal/src/element.rs b/crates/terminal/src/terminal_element.rs similarity index 67% rename from crates/terminal/src/element.rs rename to crates/terminal/src/terminal_element.rs index 08d49ec6d8..e5149d6cb5 100644 --- a/crates/terminal/src/element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -1,14 +1,11 @@ use alacritty_terminal::{ ansi::Color as AnsiColor, - event_loop::Msg, - grid::{Indexed, Scroll}, + grid::{GridIterator, Indexed}, index::Point, - sync::FairMutex, term::{ cell::{Cell, Flags}, SizeInfo, }, - Term, }; use gpui::{ color::Color, @@ -17,38 +14,28 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, json::json, text_layout::Line, - Event, MouseRegion, Quad, + Event, MouseRegion, PaintContext, Quad, WeakViewHandle, }; -use mio_extras::channel::Sender; use ordered_float::OrderedFloat; use settings::Settings; -use std::{rc::Rc, sync::Arc}; +use std::rc::Rc; use theme::TerminalStyle; -use crate::{Input, ZedListener}; +use crate::{Input, ScrollTerminal, Terminal}; const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; +const MAGIC_VISUAL_WIDTH_MULTIPLIER: f32 = 1.28; //1/8th + .003 so we bias long instead of short + +#[cfg(debug_assertions)] +const DEBUG_GRID: bool = false; pub struct TerminalEl { - term: Arc>>, - pty_tx: Sender, - size: SizeInfo, - view_id: usize, + view: WeakViewHandle, } impl TerminalEl { - pub fn new( - term: Arc>>, - pty_tx: Sender, - size: SizeInfo, - view_id: usize, - ) -> TerminalEl { - TerminalEl { - term, - pty_tx, - size, - view_id, - } + pub fn new(view: WeakViewHandle) -> TerminalEl { + TerminalEl { view } } } @@ -57,6 +44,7 @@ pub struct LayoutState { line_height: f32, em_width: f32, cursor: Option, + cur_size: SizeInfo, } impl Element for TerminalEl { @@ -68,88 +56,56 @@ impl Element for TerminalEl { constraint: gpui::SizeConstraint, cx: &mut gpui::LayoutContext, ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { + let view = self.view.upgrade(cx).unwrap(); let size = constraint.max; let settings = cx.global::(); let editor_theme = &settings.theme.editor; - let terminal_theme = &settings.theme.terminal; - //Get terminal - let mut term = self.term.lock(); //Set up text rendering - let font_cache = cx.font_cache(); - - let font_family_id = settings.buffer_font_family; - let font_family_name = cx.font_cache().family_name(font_family_id).unwrap(); - let font_properties = Default::default(); - let font_id = font_cache - .select_font(font_family_id, &font_properties) - .unwrap(); - let font_size = settings.buffer_font_size; - let text_style = TextStyle { color: editor_theme.text_color, font_family_id: settings.buffer_font_family, - font_family_name, - font_id, - font_size, + font_family_name: cx + .font_cache() + .family_name(settings.buffer_font_family) + .unwrap(), + font_id: cx + .font_cache() + .select_font(settings.buffer_font_family, &Default::default()) + .unwrap(), + font_size: settings.buffer_font_size, font_properties: Default::default(), underline: Default::default(), }; let line_height = cx.font_cache.line_height(text_style.font_size); - let em_width = cx + let cell_width = cx .font_cache() .em_width(text_style.font_id, text_style.font_size) - + 2.; + * MAGIC_VISUAL_WIDTH_MULTIPLIER; - //Resize terminal - let new_size = SizeInfo::new(size.x(), size.y(), em_width, line_height, 0., 0., false); - if !new_size.eq(&self.size) { - self.pty_tx.send(Msg::Resize(new_size)).ok(); - term.resize(new_size); - self.size = new_size; - } + let new_size = SizeInfo::new( + size.x() - cell_width, //Padding. Really should make this more explicit + size.y(), + cell_width, + line_height, + 0., + 0., + false, + ); + view.update(cx.app, |view, _cx| { + view.set_size(new_size); + }); + + let settings = cx.global::(); + let terminal_theme = &settings.theme.terminal; + let term = view.read(cx).term.lock(); - //Start rendering let content = term.renderable_content(); - - let mut lines: Vec<(String, Option)> = vec![]; - let mut last_line = 0; - let mut line_count = 1; - let mut cur_chunk = String::new(); - - let mut cur_highlight = HighlightStyle { - color: Some(Color::white()), - ..Default::default() - }; - - for cell in content.display_iter { - let Indexed { - point: Point { line, .. }, - cell: Cell { - c, fg, flags, .. // TODO: Add bg and flags - }, //TODO: Learn what 'CellExtra does' - } = cell; - - let new_highlight = make_style_from_cell(fg, flags, &terminal_theme); - - if line != last_line { - line_count += 1; - cur_chunk.push('\n'); - last_line = line.0; - } - - if new_highlight != cur_highlight { - lines.push((cur_chunk.clone(), Some(cur_highlight.clone()))); - cur_chunk.clear(); - cur_highlight = new_highlight; - } - cur_chunk.push(*c) - } - lines.push((cur_chunk, Some(cur_highlight))); + let (chunks, line_count) = build_chunks(content.display_iter, &terminal_theme); let shaped_lines = layout_highlighted_chunks( - lines.iter().map(|(text, style)| (text.as_str(), *style)), + chunks.iter().map(|(text, style)| (text.as_str(), *style)), &text_style, cx.text_layout_cache, &cx.font_cache, @@ -167,7 +123,7 @@ impl Element for TerminalEl { let cursor_x = layout_line.x_for_index(content.cursor.point.column.0); cursor = Some(RectF::new( vec2f(cursor_x, cursor_line as f32 * line_height), - vec2f(em_width, line_height), + vec2f(cell_width, line_height), )); } @@ -176,8 +132,9 @@ impl Element for TerminalEl { LayoutState { lines: shaped_lines, line_height, - em_width, + em_width: cell_width, cursor, + cur_size: new_size, }, ) } @@ -192,7 +149,7 @@ impl Element for TerminalEl { cx.scene.push_layer(Some(visible_bounds)); cx.scene.push_mouse_region(MouseRegion { - view_id: self.view_id, + view_id: self.view.id(), discriminant: None, bounds: visible_bounds, hover: None, @@ -205,7 +162,7 @@ impl Element for TerminalEl { right_mouse_down_out: None, }); - let origin = bounds.origin() + vec2f(layout.em_width, 0.); + let origin = bounds.origin() + vec2f(layout.em_width, 0.); //Padding let mut line_origin = origin; for line in &layout.lines { @@ -229,6 +186,11 @@ impl Element for TerminalEl { }); } + #[cfg(debug_assertions)] + if DEBUG_GRID { + draw_debug_grid(bounds, layout, cx); + } + cx.scene.pop_layer(); } @@ -248,8 +210,7 @@ impl Element for TerminalEl { if visible_bounds.contains_point(*position) { let vertical_scroll = (delta.y() / layout.line_height) * ALACRITTY_SCROLL_MULTIPLIER; - let scroll = Scroll::Delta(vertical_scroll.round() as i32); - self.term.lock().scroll_display(scroll); + cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32)); true } else { false @@ -282,6 +243,47 @@ impl Element for TerminalEl { } } +pub(crate) fn build_chunks( + grid_iterator: GridIterator, + theme: &TerminalStyle, +) -> (Vec<(String, Option)>, usize) { + let mut lines: Vec<(String, Option)> = vec![]; + let mut last_line = 0; + let mut line_count = 1; + let mut cur_chunk = String::new(); + + let mut cur_highlight = HighlightStyle { + color: Some(Color::white()), + ..Default::default() + }; + + for cell in grid_iterator { + let Indexed { + point: Point { line, .. }, + cell: Cell { + c, fg, flags, .. // TODO: Add bg and flags + }, //TODO: Learn what 'CellExtra does' + } = cell; + + let new_highlight = make_style_from_cell(fg, flags, theme); + + if line != last_line { + line_count += 1; + cur_chunk.push('\n'); + last_line = line.0; + } + + if new_highlight != cur_highlight { + lines.push((cur_chunk.clone(), Some(cur_highlight.clone()))); + cur_chunk.clear(); + cur_highlight = new_highlight; + } + cur_chunk.push(*c) + } + lines.push((cur_chunk, Some(cur_highlight))); + (lines, line_count) +} + fn make_style_from_cell(fg: &AnsiColor, flags: &Flags, style: &TerminalStyle) -> HighlightStyle { let fg = Some(alac_color_to_gpui_color(fg, style)); let underline = if flags.contains(Flags::UNDERLINE) { @@ -337,3 +339,32 @@ fn alac_color_to_gpui_color(allac_color: &AnsiColor, style: &TerminalStyle) -> C alacritty_terminal::ansi::Color::Indexed(_) => Color::white(), //Color cube weirdness } } + +#[cfg(debug_assertions)] +fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) { + let width = layout.cur_size.width(); + let height = layout.cur_size.height(); + //Alacritty uses 'as usize', so shall we. + for col in 0..(width / layout.em_width).round() as usize { + cx.scene.push_quad(Quad { + bounds: RectF::new( + bounds.origin() + vec2f((col + 1) as f32 * layout.em_width, 0.), + vec2f(1., height), + ), + background: Some(Color::green()), + border: Default::default(), + corner_radius: 0., + }); + } + for row in 0..((height / layout.line_height) + 1.0).round() as usize { + cx.scene.push_quad(Quad { + bounds: RectF::new( + bounds.origin() + vec2f(layout.em_width, row as f32 * layout.line_height), + vec2f(width, 1.), + ), + background: Some(Color::green()), + border: Default::default(), + corner_radius: 0., + }); + } +} diff --git a/styles/src/themes/cave.ts b/styles/src/themes/cave.ts index d588f84c53..aab020d626 100644 --- a/styles/src/themes/cave.ts +++ b/styles/src/themes/cave.ts @@ -25,7 +25,4 @@ const ramps = { }; export const dark = createTheme(`${name}-dark`, false, ramps); -export const light = createTheme(`${name}-light`, true, ramps); - -console.log(JSON.stringify(dark.ramps.neutral.domain())) -console.log(JSON.stringify(light.ramps.neutral.domain())) \ No newline at end of file +export const light = createTheme(`${name}-light`, true, ramps); \ No newline at end of file From 61e8c321bc98ba404483004c2dd8de61cf901e27 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Jun 2022 13:08:02 -0700 Subject: [PATCH 14/60] Remove swp --- .swp | Bin 12288 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .swp diff --git a/.swp b/.swp deleted file mode 100644 index 5f9940d5044d36923e5abbe428dace63080fbef8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmYc?2=nw+u+TGNU|?Vn01*)Uu7-gjH#0l2GA9utfQR&js4Ld>O-y#m&(GF%%q-H& z$xlwq!J}zZYBU5!Lx9d9z!_?6WM}}gR9Q(;L0Bjh#2m$=Aut*OqaiRF0;3@?8Umvs zFd71*Aut*OBP0Y$3K$vc85kItp#Bwt(u`;{lsig|hQMeDjE2By2#kinXb6mkz-S1J zhQMeDjE2By2#kinXb23!5J*g6U{K^|U})us%>Tps{~P!j7#8v~FwEy?V3^I%z%Y}a zfnf$e1H*KF28L<;3=C8F85kzWfq|inp)94C zA)g^NEi*qWD>F4ct2{HcoS`7QI3+Q=Aip@fAT2w;Agwq%zaS;8AU`_|B$$#`ke#1W moL`WgR$P#s4;9NV$Sy8VP0KG&O-s*DElMv>g`k4`bOr#Y_e~4{ From bc728c160d2bb7e5cfaa8d905134321d15784921 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Jun 2022 13:19:25 -0700 Subject: [PATCH 15/60] Properly fixed the issues with the grid :D --- crates/terminal/src/terminal_element.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index e5149d6cb5..d7dcafa1d6 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -24,10 +24,9 @@ use theme::TerminalStyle; use crate::{Input, ScrollTerminal, Terminal}; const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; -const MAGIC_VISUAL_WIDTH_MULTIPLIER: f32 = 1.28; //1/8th + .003 so we bias long instead of short #[cfg(debug_assertions)] -const DEBUG_GRID: bool = false; +const DEBUG_GRID: bool = true; pub struct TerminalEl { view: WeakViewHandle, @@ -60,17 +59,14 @@ impl Element for TerminalEl { let size = constraint.max; let settings = cx.global::(); let editor_theme = &settings.theme.editor; + let font_cache = cx.font_cache(); //Set up text rendering let text_style = TextStyle { color: editor_theme.text_color, font_family_id: settings.buffer_font_family, - font_family_name: cx - .font_cache() - .family_name(settings.buffer_font_family) - .unwrap(), - font_id: cx - .font_cache() + font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(), + font_id: font_cache .select_font(settings.buffer_font_family, &Default::default()) .unwrap(), font_size: settings.buffer_font_size, @@ -78,14 +74,11 @@ impl Element for TerminalEl { underline: Default::default(), }; - let line_height = cx.font_cache.line_height(text_style.font_size); - let cell_width = cx - .font_cache() - .em_width(text_style.font_id, text_style.font_size) - * MAGIC_VISUAL_WIDTH_MULTIPLIER; + let line_height = font_cache.line_height(text_style.font_size); + let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size); let new_size = SizeInfo::new( - size.x() - cell_width, //Padding. Really should make this more explicit + size.x() - cell_width, size.y(), cell_width, line_height, From 581c9af3952548389e229214cf22e592ab7fb95d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 29 Jun 2022 15:09:18 -0700 Subject: [PATCH 16/60] Adjust pane, tab, panel management bindings to match VS Code --- assets/keymaps/default.json | 105 ++++++++++++++------ crates/contacts_panel/src/contacts_panel.rs | 3 + crates/project_panel/src/project_panel.rs | 3 +- crates/workspace/src/pane.rs | 14 ++- crates/workspace/src/sidebar.rs | 54 ++++++---- crates/workspace/src/waiting_room.rs | 13 +-- crates/workspace/src/workspace.rs | 70 ++++++++++--- crates/zed/src/menus.rs | 20 +++- crates/zed/src/zed.rs | 14 ++- 9 files changed, 211 insertions(+), 85 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 0f1e005891..19ba833200 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -210,6 +210,43 @@ { "context": "Pane", "bindings": { + "ctrl-1": [ + "pane::ActivateItem", + 0 + ], + "ctrl-2": [ + "pane::ActivateItem", + 1 + ], + "ctrl-3": [ + "pane::ActivateItem", + 2 + ], + "ctrl-4": [ + "pane::ActivateItem", + 3 + ], + "ctrl-5": [ + "pane::ActivateItem", + 4 + ], + "ctrl-6": [ + "pane::ActivateItem", + 5 + ], + "ctrl-7": [ + "pane::ActivateItem", + 6 + ], + "ctrl-8": [ + "pane::ActivateItem", + 7 + ], + "ctrl-9": [ + "pane::ActivateItem", + 8 + ], + "ctrl-0": "pane::ActivateLastItem", "ctrl--": "pane::GoBack", "shift-ctrl-_": "pane::GoForward", "cmd-shift-T": "pane::ReopenClosedItem", @@ -219,6 +256,43 @@ { "context": "Workspace", "bindings": { + "cmd-1": [ + "workspace::ActivatePane", + 0 + ], + "cmd-2": [ + "workspace::ActivatePane", + 1 + ], + "cmd-3": [ + "workspace::ActivatePane", + 2 + ], + "cmd-4": [ + "workspace::ActivatePane", + 3 + ], + "cmd-5": [ + "workspace::ActivatePane", + 4 + ], + "cmd-6": [ + "workspace::ActivatePane", + 5 + ], + "cmd-7": [ + "workspace::ActivatePane", + 6 + ], + "cmd-8": [ + "workspace::ActivatePane", + 7 + ], + "cmd-9": [ + "workspace::ActivatePane", + 8 + ], + "cmd-b": "workspace::ToggleLeftSidebar", "cmd-shift-F": "project_search::Deploy", "cmd-k cmd-t": "theme_selector::Toggle", "cmd-k cmd-s": "zed::OpenKeymap", @@ -226,6 +300,7 @@ "cmd-p": "file_finder::Toggle", "cmd-shift-P": "command_palette::Toggle", "cmd-shift-M": "diagnostics::Deploy", + "cmd-shift-E": "project_panel::Toggle", "cmd-alt-s": "workspace::SaveAll" } }, @@ -310,34 +385,8 @@ { "context": "Workspace", "bindings": { - "cmd-1": [ - "workspace::ToggleSidebarItemFocus", - { - "side": "Left", - "item_index": 0 - } - ], - "cmd-shift-!": [ - "workspace::ToggleSidebarItem", - { - "side": "Left", - "item_index": 0 - } - ], - "cmd-9": [ - "workspace::ToggleSidebarItemFocus", - { - "side": "Right", - "item_index": 0 - } - ], - "cmd-shift-(": [ - "workspace::ToggleSidebarItem", - { - "side": "Right", - "item_index": 0 - } - ] + "cmd-shift-C": "contacts_panel::Toggle", + "cmd-shift-B": "workspace::ToggleRightSidebar" } }, { diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 1384774cc0..8ffbd17a1d 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -8,6 +8,7 @@ use contact_notification::ContactNotification; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ + actions, elements::*, geometry::{rect::RectF, vector::vec2f}, impl_actions, impl_internal_actions, @@ -24,6 +25,8 @@ use std::{ops::DerefMut, sync::Arc}; use theme::IconButton; use workspace::{sidebar::SidebarItem, JoinProject, ToggleProjectOnline, Workspace}; +actions!(contacts_panel, [Toggle]); + impl_actions!( contacts_panel, [RequestContact, RemoveContact, RespondToContactRequest] diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 78aed09dc6..eaa747f2ac 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -108,7 +108,8 @@ actions!( Cut, Paste, Delete, - Rename + Rename, + Toggle ] ); impl_internal_actions!(project_panel, [Open, ToggleExpanded, DeployContextMenu]); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 29770dc772..5e039b8cd0 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -18,11 +18,15 @@ use settings::Settings; use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc}; use util::ResultExt; +#[derive(Clone, Deserialize, PartialEq)] +pub struct ActivateItem(pub usize); + actions!( pane, [ ActivatePrevItem, ActivateNextItem, + ActivateLastItem, CloseActiveItem, CloseInactiveItems, ReopenClosedItem, @@ -39,9 +43,6 @@ pub struct CloseItem { pub pane: WeakViewHandle, } -#[derive(Clone, Deserialize, PartialEq)] -pub struct ActivateItem(pub usize); - #[derive(Clone, Deserialize, PartialEq)] pub struct GoBack { #[serde(skip_deserializing)] @@ -54,8 +55,8 @@ pub struct GoForward { pub pane: Option>, } -impl_actions!(pane, [GoBack, GoForward]); -impl_internal_actions!(pane, [CloseItem, ActivateItem]); +impl_actions!(pane, [GoBack, GoForward, ActivateItem]); +impl_internal_actions!(pane, [CloseItem]); const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; @@ -63,6 +64,9 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { pane.activate_item(action.0, true, true, cx); }); + cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| { + pane.activate_item(pane.items.len() - 1, true, true, cx); + }); cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { pane.activate_prev_item(cx); }); diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 0cfb7f7865..341026aecf 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -55,7 +55,8 @@ impl Into for &dyn SidebarItemHandle { pub struct Sidebar { side: Side, items: Vec, - active_item_ix: Option, + is_open: bool, + active_item_ix: usize, actual_width: Rc>, custom_width: Rc>, } @@ -83,25 +84,41 @@ pub struct ToggleSidebarItem { pub item_index: usize, } -#[derive(Clone, Debug, Deserialize, PartialEq)] -pub struct ToggleSidebarItemFocus { - pub side: Side, - pub item_index: usize, -} - -impl_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]); +impl_actions!(workspace, [ToggleSidebarItem]); impl Sidebar { pub fn new(side: Side) -> Self { Self { side, items: Default::default(), - active_item_ix: None, + active_item_ix: 0, + is_open: false, actual_width: Rc::new(RefCell::new(260.)), custom_width: Rc::new(RefCell::new(260.)), } } + pub fn is_open(&self) -> bool { + self.is_open + } + + pub fn active_item_ix(&self) -> usize { + self.active_item_ix + } + + pub fn set_open(&mut self, open: bool, cx: &mut ViewContext) { + if open != self.is_open { + self.is_open = open; + cx.notify(); + } + } + + pub fn toggle_open(&mut self, cx: &mut ViewContext) { + if self.is_open {} + self.is_open = !self.is_open; + cx.notify(); + } + pub fn add_item( &mut self, icon_path: &'static str, @@ -133,23 +150,25 @@ impl Sidebar { } pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext) { - self.active_item_ix = Some(item_ix); + self.active_item_ix = item_ix; cx.notify(); } pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext) { - if self.active_item_ix == Some(item_ix) { - self.active_item_ix = None; + if self.active_item_ix == item_ix { + self.is_open = false; } else { - self.active_item_ix = Some(item_ix); + self.active_item_ix = item_ix; } cx.notify(); } pub fn active_item(&self) -> Option<&Rc> { - self.active_item_ix - .and_then(|ix| self.items.get(ix)) - .map(|item| &item.view) + if self.is_open { + self.items.get(self.active_item_ix).map(|item| &item.view) + } else { + None + } } fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox { @@ -249,6 +268,7 @@ impl View for SidebarButtons { let item_style = theme.item; let badge_style = theme.badge; let active_ix = sidebar.active_item_ix; + let is_open = sidebar.is_open; let side = sidebar.side; let group_style = match side { Side::Left => theme.group_left, @@ -267,7 +287,7 @@ impl View for SidebarButtons { item_index: ix, }; MouseEventHandler::new::(ix, cx, move |state, cx| { - let is_active = Some(ix) == active_ix; + let is_active = is_open && ix == active_ix; let style = item_style.style_for(state, is_active); Stack::new() .with_child(Svg::new(icon_path).with_color(style.icon_color).boxed()) diff --git a/crates/workspace/src/waiting_room.rs b/crates/workspace/src/waiting_room.rs index 5052afcf50..e5d765d4d5 100644 --- a/crates/workspace/src/waiting_room.rs +++ b/crates/workspace/src/waiting_room.rs @@ -1,7 +1,4 @@ -use crate::{ - sidebar::{Side, ToggleSidebarItem}, - AppState, ToggleFollow, Workspace, -}; +use crate::{sidebar::Side, AppState, ToggleFollow, Workspace}; use anyhow::Result; use client::{proto, Client, Contact}; use gpui::{ @@ -104,13 +101,7 @@ impl WaitingRoom { &app_state, cx, ); - workspace.toggle_sidebar_item( - &ToggleSidebarItem { - side: Side::Left, - item_index: 0, - }, - cx, - ); + workspace.toggle_sidebar(Side::Left, cx); if let Some((host_peer_id, _)) = workspace.project.read(cx).collaborators().iter().find( |(_, collaborator)| collaborator.replica_id == 0, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b0e4373ca4..68e5b3cb26 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -31,7 +31,7 @@ use postage::prelude::Stream; use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId}; use serde::Deserialize; use settings::Settings; -use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem, ToggleSidebarItemFocus}; +use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem}; use smallvec::SmallVec; use status_bar::StatusBar; pub use status_bar::StatusItemView; @@ -90,6 +90,8 @@ actions!( ActivatePreviousPane, ActivateNextPane, FollowNextCollaborator, + ToggleLeftSidebar, + ToggleRightSidebar, ] ); @@ -104,6 +106,9 @@ pub struct ToggleProjectOnline { pub project: Option>, } +#[derive(Clone, Deserialize, PartialEq)] +pub struct ActivatePane(pub usize); + #[derive(Clone, PartialEq)] pub struct ToggleFollow(pub PeerId); @@ -122,7 +127,7 @@ impl_internal_actions!( RemoveWorktreeFromProject ] ); -impl_actions!(workspace, [ToggleProjectOnline]); +impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]); pub fn init(app_state: Arc, cx: &mut MutableAppContext) { pane::init(cx); @@ -185,7 +190,6 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { }, ); cx.add_action(Workspace::toggle_sidebar_item); - cx.add_action(Workspace::toggle_sidebar_item_focus); cx.add_action(Workspace::focus_center); cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { workspace.activate_previous_pane(cx) @@ -193,6 +197,13 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { workspace.activate_next_pane(cx) }); + cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| { + workspace.toggle_sidebar(Side::Left, cx); + }); + cx.add_action(|workspace: &mut Workspace, _: &ToggleRightSidebar, cx| { + workspace.toggle_sidebar(Side::Right, cx); + }); + cx.add_action(Workspace::activate_pane_at_index); let client = &app_state.client; client.add_view_request_handler(Workspace::handle_follow); @@ -1248,17 +1259,39 @@ impl Workspace { } } + pub fn toggle_sidebar(&mut self, side: Side, cx: &mut ViewContext) { + let sidebar = match side { + Side::Left => &mut self.left_sidebar, + Side::Right => &mut self.right_sidebar, + }; + sidebar.update(cx, |sidebar, cx| { + sidebar.set_open(!sidebar.is_open(), cx); + }); + cx.focus_self(); + cx.notify(); + } + pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext) { let sidebar = match action.side { Side::Left => &mut self.left_sidebar, Side::Right => &mut self.right_sidebar, }; let active_item = sidebar.update(cx, |sidebar, cx| { - sidebar.toggle_item(action.item_index, cx); - sidebar.active_item().map(|item| item.to_any()) + if sidebar.is_open() && sidebar.active_item_ix() == action.item_index { + sidebar.set_open(false, cx); + None + } else { + sidebar.set_open(true, cx); + sidebar.activate_item(action.item_index, cx); + sidebar.active_item().cloned() + } }); if let Some(active_item) = active_item { - cx.focus(active_item); + if active_item.is_focused(cx) { + cx.focus_self(); + } else { + cx.focus(active_item.to_any()); + } } else { cx.focus_self(); } @@ -1267,15 +1300,17 @@ impl Workspace { pub fn toggle_sidebar_item_focus( &mut self, - action: &ToggleSidebarItemFocus, + side: Side, + item_index: usize, cx: &mut ViewContext, ) { - let sidebar = match action.side { + let sidebar = match side { Side::Left => &mut self.left_sidebar, Side::Right => &mut self.right_sidebar, }; let active_item = sidebar.update(cx, |sidebar, cx| { - sidebar.activate_item(action.item_index, cx); + sidebar.set_open(true, cx); + sidebar.activate_item(item_index, cx); sidebar.active_item().cloned() }); if let Some(active_item) = active_item { @@ -1405,6 +1440,15 @@ impl Workspace { } } + fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext) { + let panes = self.center.panes(); + if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { + self.activate_pane(pane, cx); + } else { + self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx); + } + } + pub fn activate_next_pane(&mut self, cx: &mut ViewContext) { let next_pane = { let panes = self.center.panes(); @@ -2481,13 +2525,7 @@ pub fn open_paths( let mut workspace = Workspace::new(project, cx); (app_state.initialize_workspace)(&mut workspace, &app_state, cx); if contains_directory { - workspace.toggle_sidebar_item( - &ToggleSidebarItem { - side: Side::Left, - item_index: 0, - }, - cx, - ); + workspace.toggle_sidebar(Side::Left, cx); } workspace }) diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index dfc3556604..4dbcdb7ddc 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -187,11 +187,21 @@ pub fn menus() -> Vec> { }, MenuItem::Separator, MenuItem::Action { - name: "Project Browser", - action: Box::new(workspace::sidebar::ToggleSidebarItemFocus { - side: workspace::sidebar::Side::Left, - item_index: 0, - }), + name: "Toggle Left Sidebar", + action: Box::new(workspace::ToggleLeftSidebar), + }, + MenuItem::Action { + name: "Toggle Right Sidebar", + action: Box::new(workspace::ToggleRightSidebar), + }, + MenuItem::Separator, + MenuItem::Action { + name: "Project Panel", + action: Box::new(project_panel::Toggle), + }, + MenuItem::Action { + name: "Contacts Panel", + action: Box::new(contacts_panel::Toggle), }, MenuItem::Action { name: "Command Palette", diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 68a713e9af..8341f3639e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -34,7 +34,7 @@ use std::{ }; use util::ResultExt; pub use workspace; -use workspace::{AppState, Workspace}; +use workspace::{sidebar::Side, AppState, Workspace}; #[derive(Deserialize, Clone, PartialEq)] struct OpenBrowser { @@ -128,6 +128,16 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { } }, ); + cx.add_action( + |workspace: &mut Workspace, _: &project_panel::Toggle, cx: &mut ViewContext| { + workspace.toggle_sidebar_item_focus(Side::Left, 0, cx); + }, + ); + cx.add_action( + |workspace: &mut Workspace, _: &contacts_panel::Toggle, cx: &mut ViewContext| { + workspace.toggle_sidebar_item_focus(Side::Right, 0, cx); + }, + ); lsp_status::init(cx); settings::KeymapFileContent::load_defaults(cx); @@ -429,7 +439,7 @@ mod tests { let workspace_1 = cx.root_view::(cx.window_ids()[0]).unwrap(); workspace_1.update(cx, |workspace, cx| { assert_eq!(workspace.worktrees(cx).count(), 2); - assert!(workspace.left_sidebar().read(cx).active_item().is_some()); + assert!(workspace.left_sidebar().read(cx).is_open()); assert!(workspace.active_pane().is_focused(cx)); }); From f22d69aa74719cbfd2339caaeb3a04258d5053a8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 29 Jun 2022 15:33:20 -0700 Subject: [PATCH 17/60] Add other standard macOS bindings for switching to next/prev tab --- assets/keymaps/default.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 19ba833200..7f1c38d0b9 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -13,6 +13,8 @@ "ctrl-c": "menu::Cancel", "shift-cmd-{": "pane::ActivatePrevItem", "shift-cmd-}": "pane::ActivateNextItem", + "alt-cmd-left": "pane::ActivatePrevItem", + "alt-cmd-right": "pane::ActivateNextItem", "cmd-w": "pane::CloseActiveItem", "cmd-shift-W": "workspace::CloseWindow", "alt-cmd-t": "pane::CloseInactiveItems", From 67414deb82328cf4c4632eec71b5dff2a7b794a3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 29 Jun 2022 15:40:32 -0700 Subject: [PATCH 18/60] Add split actions to the application menu --- crates/zed/src/menus.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 4dbcdb7ddc..cadedc118b 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -194,6 +194,27 @@ pub fn menus() -> Vec> { name: "Toggle Right Sidebar", action: Box::new(workspace::ToggleRightSidebar), }, + MenuItem::Submenu(Menu { + name: "Editor Layout", + items: vec![ + MenuItem::Action { + name: "Split Up", + action: Box::new(workspace::SplitUp), + }, + MenuItem::Action { + name: "Split Down", + action: Box::new(workspace::SplitDown), + }, + MenuItem::Action { + name: "Split Left", + action: Box::new(workspace::SplitLeft), + }, + MenuItem::Action { + name: "Split Right", + action: Box::new(workspace::SplitRight), + }, + ], + }), MenuItem::Separator, MenuItem::Action { name: "Project Panel", From 1c038b81abe8293a6a1eecca5e147a3eee86ffb5 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Jun 2022 16:22:05 -0700 Subject: [PATCH 19/60] Finished current working directory stuff --- crates/terminal/src/terminal.rs | 19 +++++++++++++------ crates/terminal/src/terminal_element.rs | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 9adcb3122e..d6ecfce3c3 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -19,7 +19,7 @@ use gpui::{ use project::{Project, ProjectPath}; use settings::Settings; use smallvec::SmallVec; -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; use workspace::{Item, Workspace}; use crate::terminal_element::TerminalEl; @@ -99,7 +99,7 @@ impl Entity for Terminal { impl Terminal { ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices - fn new(cx: &mut ViewContext) -> Self { + fn new(cx: &mut ViewContext, working_directory: Option) -> Self { //Spawn a task so the Alacritty EventLoop can communicate with us in a view context let (events_tx, mut events_rx) = unbounded(); cx.spawn_weak(|this, mut cx| async move { @@ -119,7 +119,7 @@ impl Terminal { let pty_config = PtyConfig { shell: Some(Program::Just("zsh".to_string())), - working_directory: None, + working_directory, hold: false, }; @@ -128,7 +128,7 @@ impl Terminal { ..Default::default() }; - //TODO figure out how to derive this better + //The details here don't matter, the terminal will be resized on layout let size_info = SizeInfo::new(200., 100.0, 5., 5., 0., 0., false); //Set up the terminal... @@ -234,7 +234,14 @@ impl Terminal { ///Create a new Terminal fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx))), cx); + let project = workspace.project().read(cx); + let abs_path = project + .active_entry() + .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) + .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) + .map(|wt| wt.abs_path().to_path_buf()); + + workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path))), cx); } ///Send the shutdown message to Alacritty @@ -421,7 +428,7 @@ mod tests { #[gpui::test] async fn test_terminal(cx: &mut TestAppContext) { - let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx)); + let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None)); terminal.update(cx, |terminal, cx| { terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx); diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index d7dcafa1d6..2002b5c144 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -26,7 +26,7 @@ use crate::{Input, ScrollTerminal, Terminal}; const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; #[cfg(debug_assertions)] -const DEBUG_GRID: bool = true; +const DEBUG_GRID: bool = false; pub struct TerminalEl { view: WeakViewHandle, From ae61a24ad398af20f0d499003449ca5b2dd3cda8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 29 Jun 2022 16:54:01 -0700 Subject: [PATCH 20/60] Show LSP status and auto update status in one status bar indicator --- Cargo.lock | 33 ++-- .../Cargo.toml | 5 +- .../src/activity_indicator.rs} | 187 ++++++++++++------ crates/auto_update/src/auto_update.rs | 104 ++-------- crates/zed/Cargo.toml | 2 +- crates/zed/src/zed.rs | 9 +- 6 files changed, 165 insertions(+), 175 deletions(-) rename crates/{lsp_status => activity_indicator}/Cargo.toml (78%) rename crates/{lsp_status/src/lsp_status.rs => activity_indicator/src/activity_indicator.rs} (60%) diff --git a/Cargo.lock b/Cargo.lock index d1b0e62ca7..8925fa3fe7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "activity_indicator" +version = "0.1.0" +dependencies = [ + "auto_update", + "editor", + "futures", + "gpui", + "language", + "project", + "settings", + "smallvec", + "util", + "workspace", +] + [[package]] name = "addr2line" version = "0.17.0" @@ -2566,21 +2582,6 @@ dependencies = [ "url", ] -[[package]] -name = "lsp_status" -version = "0.1.0" -dependencies = [ - "editor", - "futures", - "gpui", - "language", - "project", - "settings", - "smallvec", - "util", - "workspace", -] - [[package]] name = "malloc_buf" version = "0.0.6" @@ -5971,6 +5972,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" name = "zed" version = "0.42.0" dependencies = [ + "activity_indicator", "anyhow", "assets", "async-compression", @@ -6011,7 +6013,6 @@ dependencies = [ "libc", "log", "lsp", - "lsp_status", "num_cpus", "outline", "parking_lot 0.11.2", diff --git a/crates/lsp_status/Cargo.toml b/crates/activity_indicator/Cargo.toml similarity index 78% rename from crates/lsp_status/Cargo.toml rename to crates/activity_indicator/Cargo.toml index 19d428d3b2..63998fa47b 100644 --- a/crates/lsp_status/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -1,13 +1,14 @@ [package] -name = "lsp_status" +name = "activity_indicator" version = "0.1.0" edition = "2021" [lib] -path = "src/lsp_status.rs" +path = "src/activity_indicator.rs" doctest = false [dependencies] +auto_update = { path = "../auto_update" } editor = { path = "../editor" } language = { path = "../language" } gpui = { path = "../gpui" } diff --git a/crates/lsp_status/src/lsp_status.rs b/crates/activity_indicator/src/activity_indicator.rs similarity index 60% rename from crates/lsp_status/src/lsp_status.rs rename to crates/activity_indicator/src/activity_indicator.rs index fedf5299de..8bc84f911c 100644 --- a/crates/lsp_status/src/lsp_status.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -1,7 +1,8 @@ +use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage}; use editor::Editor; use futures::StreamExt; use gpui::{ - actions, elements::*, platform::CursorStyle, AppContext, Entity, EventContext, ModelHandle, + actions, elements::*, platform::CursorStyle, Action, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, }; use language::{LanguageRegistry, LanguageServerBinaryStatus}; @@ -14,13 +15,18 @@ use workspace::{ItemHandle, StatusItemView, Workspace}; actions!(lsp_status, [ShowErrorMessage]); +const DOWNLOAD_ICON: &'static str = "icons/download-solid-14.svg"; +const WARNING_ICON: &'static str = "icons/warning-solid-14.svg"; +const DONE_ICON: &'static str = "icons/accept.svg"; + pub enum Event { ShowError { lsp_name: Arc, error: String }, } -pub struct LspStatusItem { +pub struct ActivityIndicator { statuses: Vec, project: ModelHandle, + auto_updater: Option>, } struct LspStatus { @@ -29,15 +35,16 @@ struct LspStatus { } pub fn init(cx: &mut MutableAppContext) { - cx.add_action(LspStatusItem::show_error_message); + cx.add_action(ActivityIndicator::show_error_message); + cx.add_action(ActivityIndicator::dismiss_error_message); } -impl LspStatusItem { +impl ActivityIndicator { pub fn new( workspace: &mut Workspace, languages: Arc, cx: &mut ViewContext, - ) -> ViewHandle { + ) -> ViewHandle { let project = workspace.project().clone(); let this = cx.add_view(|cx: &mut ViewContext| { let mut status_events = languages.language_server_binary_statuses(); @@ -63,6 +70,7 @@ impl LspStatusItem { Self { statuses: Default::default(), project: project.clone(), + auto_updater: AutoUpdater::get(cx), } }); cx.subscribe(&this, move |workspace, _, event, cx| match event { @@ -106,6 +114,15 @@ impl LspStatusItem { cx.notify(); } + fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext) { + if let Some(updater) = &self.auto_updater { + updater.update(cx, |updater, cx| { + updater.dismiss_error(cx); + }); + } + cx.notify(); + } + fn pending_language_server_work<'a>( &self, cx: &'a AppContext, @@ -129,25 +146,15 @@ impl LspStatusItem { }) .flatten() } -} - -impl Entity for LspStatusItem { - type Event = Event; -} - -impl View for LspStatusItem { - fn ui_name() -> &'static str { - "LspStatus" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let mut message; - let mut icon = None; - let mut handler = None; + fn content_to_render( + &mut self, + cx: &mut RenderContext, + ) -> (Option<&'static str>, String, Option>) { + // Show any language server has pending activity. let mut pending_work = self.pending_language_server_work(cx); if let Some((lang_server_name, progress_token, progress)) = pending_work.next() { - message = lang_server_name.to_string(); + let mut message = lang_server_name.to_string(); message.push_str(": "); if let Some(progress_message) = progress.message.as_ref() { @@ -164,38 +171,43 @@ impl View for LspStatusItem { if additional_work_count > 0 { write!(&mut message, " + {} more", additional_work_count).unwrap(); } - } else { - drop(pending_work); - let mut downloading = SmallVec::<[_; 3]>::new(); - let mut checking_for_update = SmallVec::<[_; 3]>::new(); - let mut failed = SmallVec::<[_; 3]>::new(); - for status in &self.statuses { - match status.status { - LanguageServerBinaryStatus::CheckingForUpdate => { - checking_for_update.push(status.name.clone()); - } - LanguageServerBinaryStatus::Downloading => { - downloading.push(status.name.clone()); - } - LanguageServerBinaryStatus::Failed { .. } => { - failed.push(status.name.clone()); - } - LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => { - } + return (None, message, None); + } + + // Show any language server installation info. + let mut downloading = SmallVec::<[_; 3]>::new(); + let mut checking_for_update = SmallVec::<[_; 3]>::new(); + let mut failed = SmallVec::<[_; 3]>::new(); + for status in &self.statuses { + match status.status { + LanguageServerBinaryStatus::CheckingForUpdate => { + checking_for_update.push(status.name.clone()); } + LanguageServerBinaryStatus::Downloading => { + downloading.push(status.name.clone()); + } + LanguageServerBinaryStatus::Failed { .. } => { + failed.push(status.name.clone()); + } + LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {} } + } - if !downloading.is_empty() { - icon = Some("icons/download-solid-14.svg"); - message = format!( + if !downloading.is_empty() { + return ( + Some(DOWNLOAD_ICON), + format!( "Downloading {} language server{}...", downloading.join(", "), if downloading.len() > 1 { "s" } else { "" } - ); - } else if !checking_for_update.is_empty() { - icon = Some("icons/download-solid-14.svg"); - message = format!( + ), + None, + ); + } else if !checking_for_update.is_empty() { + return ( + Some(DOWNLOAD_ICON), + format!( "Checking for updates to {} language server{}...", checking_for_update.join(", "), if checking_for_update.len() > 1 { @@ -203,20 +215,68 @@ impl View for LspStatusItem { } else { "" } - ); - } else if !failed.is_empty() { - icon = Some("icons/warning-solid-14.svg"); - message = format!( + ), + None, + ); + } else if !failed.is_empty() { + return ( + Some(WARNING_ICON), + format!( "Failed to download {} language server{}. Click to show error.", failed.join(", "), if failed.len() > 1 { "s" } else { "" } - ); - handler = Some(|_, _, cx: &mut EventContext| cx.dispatch_action(ShowErrorMessage)); - } else { - return Empty::new().boxed(); - } + ), + Some(Box::new(ShowErrorMessage)), + ); } + // Show any application auto-update info. + if let Some(updater) = &self.auto_updater { + // let theme = &cx.global::().theme.workspace.status_bar; + match &updater.read(cx).status() { + AutoUpdateStatus::Checking => ( + Some(DOWNLOAD_ICON), + "Checking for Zed updates…".to_string(), + None, + ), + AutoUpdateStatus::Downloading => ( + Some(DOWNLOAD_ICON), + "Downloading Zed update…".to_string(), + None, + ), + AutoUpdateStatus::Installing => ( + Some(DOWNLOAD_ICON), + "Installing Zed update…".to_string(), + None, + ), + AutoUpdateStatus::Updated => { + (Some(DONE_ICON), "Restart to update Zed".to_string(), None) + } + AutoUpdateStatus::Errored => ( + Some(WARNING_ICON), + "Auto update failed".to_string(), + Some(Box::new(DismissErrorMessage)), + ), + AutoUpdateStatus::Idle => Default::default(), + } + } else { + Default::default() + } + } +} + +impl Entity for ActivityIndicator { + type Event = Event; +} + +impl View for ActivityIndicator { + fn ui_name() -> &'static str { + "ActivityIndicator" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let (icon, message, action) = self.content_to_render(cx); + let mut element = MouseEventHandler::new::(0, cx, |state, cx| { let theme = &cx .global::() @@ -224,7 +284,7 @@ impl View for LspStatusItem { .workspace .status_bar .lsp_status; - let style = if state.hovered && handler.is_some() { + let style = if state.hovered && action.is_some() { theme.hover.as_ref().unwrap_or(&theme.default) } else { &theme.default @@ -238,9 +298,14 @@ impl View for LspStatusItem { .contained() .with_margin_right(style.icon_spacing) .aligned() - .named("warning-icon") + .named("activity-icon") })) - .with_child(Label::new(message, style.message.clone()).aligned().boxed()) + .with_child( + Text::new(message, style.message.clone()) + .with_soft_wrap(false) + .aligned() + .boxed(), + ) .constrained() .with_height(style.height) .contained() @@ -249,16 +314,16 @@ impl View for LspStatusItem { .boxed() }); - if let Some(handler) = handler { + if let Some(action) = action { element = element .with_cursor_style(CursorStyle::PointingHand) - .on_click(handler); + .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())); } element.boxed() } } -impl StatusItemView for LspStatusItem { +impl StatusItemView for ActivityIndicator { fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} } diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 44eb5fe2e8..6e4f171f60 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -3,19 +3,15 @@ mod update_notification; use anyhow::{anyhow, Context, Result}; use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN}; use gpui::{ - actions, - elements::{Empty, MouseEventHandler, Text}, - platform::AppVersion, - AppContext, AsyncAppContext, Element, Entity, ModelContext, ModelHandle, MutableAppContext, - Task, View, ViewContext, WeakViewHandle, + actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, + MutableAppContext, Task, WeakViewHandle, }; use lazy_static::lazy_static; use serde::Deserialize; -use settings::Settings; use smol::{fs::File, io::AsyncReadExt, process::Command}; use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration}; use update_notification::UpdateNotification; -use workspace::{ItemHandle, StatusItemView, Workspace}; +use workspace::Workspace; const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &'static str = "auto-updater-should-show-updated-notification"; @@ -30,7 +26,7 @@ lazy_static! { actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]); -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub enum AutoUpdateStatus { Idle, Checking, @@ -49,10 +45,6 @@ pub struct AutoUpdater { server_url: String, } -pub struct AutoUpdateIndicator { - updater: Option>, -} - #[derive(Deserialize)] struct JsonRelease { version: String, @@ -84,7 +76,6 @@ pub fn init( cx.add_global_action(move |_: &ViewReleaseNotes, cx| { cx.platform().open_url(&format!("{server_url}/releases")); }); - cx.add_action(AutoUpdateIndicator::dismiss_error_message); cx.add_action(UpdateNotification::dismiss); } } @@ -120,7 +111,7 @@ pub fn notify_of_any_new_update( } impl AutoUpdater { - fn get(cx: &mut MutableAppContext) -> Option> { + pub fn get(cx: &mut MutableAppContext) -> Option> { cx.default_global::>>().clone() } @@ -170,6 +161,15 @@ impl AutoUpdater { })); } + pub fn status(&self) -> AutoUpdateStatus { + self.status + } + + pub fn dismiss_error(&mut self, cx: &mut ModelContext) { + self.status = AutoUpdateStatus::Idle; + cx.notify(); + } + async fn update(this: ModelHandle, mut cx: AsyncAppContext) -> Result<()> { let (client, server_url, current_version) = this.read_with(&cx, |this, _| { ( @@ -299,79 +299,3 @@ impl AutoUpdater { }) } } - -impl Entity for AutoUpdateIndicator { - type Event = (); -} - -impl View for AutoUpdateIndicator { - fn ui_name() -> &'static str { - "AutoUpdateIndicator" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - if let Some(updater) = &self.updater { - let theme = &cx.global::().theme.workspace.status_bar; - match &updater.read(cx).status { - AutoUpdateStatus::Checking => Text::new( - "Checking for updates…".to_string(), - theme.auto_update_progress_message.clone(), - ) - .boxed(), - AutoUpdateStatus::Downloading => Text::new( - "Downloading update…".to_string(), - theme.auto_update_progress_message.clone(), - ) - .boxed(), - AutoUpdateStatus::Installing => Text::new( - "Installing update…".to_string(), - theme.auto_update_progress_message.clone(), - ) - .boxed(), - AutoUpdateStatus::Updated => Text::new( - "Restart to update Zed".to_string(), - theme.auto_update_done_message.clone(), - ) - .boxed(), - AutoUpdateStatus::Errored => { - MouseEventHandler::new::(0, cx, |_, cx| { - let theme = &cx.global::().theme.workspace.status_bar; - Text::new( - "Auto update failed".to_string(), - theme.auto_update_done_message.clone(), - ) - .boxed() - }) - .on_click(|_, _, cx| cx.dispatch_action(DismissErrorMessage)) - .boxed() - } - AutoUpdateStatus::Idle => Empty::new().boxed(), - } - } else { - Empty::new().boxed() - } - } -} - -impl StatusItemView for AutoUpdateIndicator { - fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} -} - -impl AutoUpdateIndicator { - pub fn new(cx: &mut ViewContext) -> Self { - let updater = AutoUpdater::get(cx); - if let Some(updater) = &updater { - cx.observe(updater, |_, _, cx| cx.notify()).detach(); - } - Self { updater } - } - - fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext) { - if let Some(updater) = &self.updater { - updater.update(cx, |updater, cx| { - updater.status = AutoUpdateStatus::Idle; - cx.notify(); - }); - } - } -} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 56472be040..c14dce992a 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -15,6 +15,7 @@ name = "Zed" path = "src/main.rs" [dependencies] +activity_indicator = { path = "../activity_indicator" } assets = { path = "../assets" } auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } @@ -37,7 +38,6 @@ gpui = { path = "../gpui" } journal = { path = "../journal" } language = { path = "../language" } lsp = { path = "../lsp" } -lsp_status = { path = "../lsp_status" } outline = { path = "../outline" } project = { path = "../project" } project_panel = { path = "../project_panel" } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8341f3639e..7240aaef2f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -139,7 +139,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { }, ); - lsp_status::init(cx); + activity_indicator::init(cx); settings::KeymapFileContent::load_defaults(cx); } @@ -222,15 +222,14 @@ pub fn initialize_workspace( let diagnostic_summary = cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx)); - let lsp_status = lsp_status::LspStatusItem::new(workspace, app_state.languages.clone(), cx); + let activity_indicator = + activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx); let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); - let auto_update = cx.add_view(|cx| auto_update::AutoUpdateIndicator::new(cx)); let feedback_link = cx.add_view(|_| feedback::FeedbackLink); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(diagnostic_summary, cx); - status_bar.add_left_item(lsp_status, cx); + status_bar.add_left_item(activity_indicator, cx); status_bar.add_right_item(cursor_position, cx); - status_bar.add_right_item(auto_update, cx); status_bar.add_right_item(feedback_link, cx); }); From cde11fe4e7297069e935d08fd57d3cfe09ed373e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Jun 2022 18:27:27 -0700 Subject: [PATCH 21/60] Support for all 24 bits of colors --- crates/terminal/src/terminal.rs | 39 ++++++++++++--- crates/terminal/src/terminal_element.rs | 64 ++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index d6ecfce3c3..6e62ce2a9f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -4,7 +4,7 @@ use alacritty_terminal::{ event_loop::{EventLoop, Msg, Notifier}, grid::Scroll, sync::FairMutex, - term::{color::Rgb, SizeInfo}, + term::{color::Rgb as AlacRgb, SizeInfo}, tty, Term, }; @@ -13,8 +13,8 @@ use futures::{ StreamExt, }; use gpui::{ - actions, elements::*, impl_internal_actions, platform::CursorStyle, ClipboardItem, Entity, - MutableAppContext, View, ViewContext, + actions, color::Color, elements::*, impl_internal_actions, platform::CursorStyle, + ClipboardItem, Entity, MutableAppContext, View, ViewContext, }; use project::{Project, ProjectPath}; use settings::Settings; @@ -22,7 +22,7 @@ use smallvec::SmallVec; use std::{path::PathBuf, sync::Arc}; use workspace::{Item, Workspace}; -use crate::terminal_element::TerminalEl; +use crate::terminal_element::{get_color_at_index, TerminalEl}; //ASCII Control characters on a keyboard //Consts -> Structs -> Impls -> Functions, Vaguely in order of importance @@ -203,9 +203,26 @@ impl Terminal { cx, ), AlacTermEvent::ColorRequest(index, format) => { - //TODO test this as well - //TODO: change to getting the display colors, like alacrityy, instead of a default - let color = self.term.lock().colors()[index].unwrap_or(Rgb::default()); + let color = self.term.lock().colors()[index].unwrap_or_else(|| { + let term_style = &cx.global::().theme.terminal; + match index { + 0..=255 => to_alac_rgb(get_color_at_index(&(index as u8), term_style)), + 256 => to_alac_rgb(term_style.foreground), + 257 => to_alac_rgb(term_style.background), + 258 => to_alac_rgb(term_style.cursor), + 259 => to_alac_rgb(term_style.dim_black), + 260 => to_alac_rgb(term_style.dim_red), + 261 => to_alac_rgb(term_style.dim_green), + 262 => to_alac_rgb(term_style.dim_yellow), + 263 => to_alac_rgb(term_style.dim_blue), + 264 => to_alac_rgb(term_style.dim_magenta), + 265 => to_alac_rgb(term_style.dim_cyan), + 266 => to_alac_rgb(term_style.dim_white), + 267 => to_alac_rgb(term_style.bright_foreground), + 268 => to_alac_rgb(term_style.black), //Dim Background, non-standard + _ => AlacRgb { r: 0, g: 0, b: 0 }, + } + }); self.write_to_pty(&Input(format(color)), cx) } AlacTermEvent::CursorBlinkingChange => { @@ -420,6 +437,14 @@ impl Item for Terminal { } } +fn to_alac_rgb(color: Color) -> AlacRgb { + AlacRgb { + r: color.r, + g: color.g, + b: color.g, + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 2002b5c144..b0c01391ca 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -329,10 +329,59 @@ fn alac_color_to_gpui_color(allac_color: &AnsiColor, style: &TerminalStyle) -> C alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground, }, //Theme defined alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, 1), - alacritty_terminal::ansi::Color::Indexed(_) => Color::white(), //Color cube weirdness + alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style), //Color cube weirdness } } +pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color { + match index { + 0 => style.black, + 1 => style.red, + 2 => style.green, + 3 => style.yellow, + 4 => style.blue, + 5 => style.magenta, + 6 => style.cyan, + 7 => style.white, + 8 => style.bright_black, + 9 => style.bright_red, + 10 => style.bright_green, + 11 => style.bright_yellow, + 12 => style.bright_blue, + 13 => style.bright_magenta, + 14 => style.bright_cyan, + 15 => style.bright_white, + 16..=231 => { + let (r, g, b) = rgb_for_index(index); //Split the index into it's rgb components + let step = (u8::MAX as f32 / 5.).round() as u8; //Split the GPUI range into 5 chunks + Color::new(r * step, g * step, b * step, 1) //Map the rgb components to GPUI's range + } + //Grayscale from black to white, 0 to 24 + 232..=255 => { + let i = 24 - (index - 232); //Align index to 24..0 + let step = (u8::MAX as f32 / 24.).round() as u8; //Split the 256 range grayscale into 24 chunks + Color::new(i * step, i * step, i * step, 1) //Map the rgb components to GPUI's range + } + } +} + +///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube +///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit). +/// +///Wikipedia gives a formula for calculating the index for a given color: +/// +///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) +/// +///This function does the reverse, calculating the r, g, and b components from a given index. +fn rgb_for_index(i: &u8) -> (u8, u8, u8) { + debug_assert!(i >= &16 && i <= &231); + let i = i - 16; + let r = (i - (i % 36)) / 36; + let g = ((i % 36) - (i % 6)) / 6; + let b = (i % 36) % 6; + (r, g, b) +} + #[cfg(debug_assertions)] fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) { let width = layout.cur_size.width(); @@ -361,3 +410,16 @@ fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContex }); } } + +mod tests { + use crate::terminal_element::rgb_for_index; + + #[test] + fn test_rgb_for_index() { + //Test every possible value in the color cube + for i in 16..=231 { + let (r, g, b) = rgb_for_index(&(i as u8)); + assert_eq!(i, 16 + 36 * r + 6 * g + b); + } + } +} From 75a45562449ec7ef62b216f5cab09c4f6ebdf3ce Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Jun 2022 18:29:36 -0700 Subject: [PATCH 22/60] Fixed unused import --- crates/terminal/src/terminal_element.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index b0c01391ca..0b02d918fc 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -412,8 +412,6 @@ fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContex } mod tests { - use crate::terminal_element::rgb_for_index; - #[test] fn test_rgb_for_index() { //Test every possible value in the color cube From ab5247c62e8bb4a287129285effe6d0d603e6a59 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Jun 2022 18:31:52 -0700 Subject: [PATCH 23/60] Actually correctly flag tests --- crates/terminal/src/terminal_element.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 0b02d918fc..2aa32b6367 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -411,6 +411,7 @@ fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContex } } +#[cfg(test)] mod tests { #[test] fn test_rgb_for_index() { From 5bc0acd88cf90f1d14035cac53ce76520da1549e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Jun 2022 18:34:02 -0700 Subject: [PATCH 24/60] Directly qualified function makes cargo happy --- crates/terminal/src/terminal_element.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 2aa32b6367..64cf93c21a 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -417,7 +417,7 @@ mod tests { fn test_rgb_for_index() { //Test every possible value in the color cube for i in 16..=231 { - let (r, g, b) = rgb_for_index(&(i as u8)); + let (r, g, b) = crate::terminal_element::rgb_for_index(&(i as u8)); assert_eq!(i, 16 + 36 * r + 6 * g + b); } } From 06107afdd4f0586099988c8eb7679f12b3bf3ada Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Jun 2022 18:50:08 -0700 Subject: [PATCH 25/60] Added background colors and matched the cursor color --- crates/terminal/src/terminal_element.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 64cf93c21a..d81292d0c2 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -42,8 +42,9 @@ pub struct LayoutState { lines: Vec, line_height: f32, em_width: f32, - cursor: Option, + cursor: Option<(RectF, Color)>, cur_size: SizeInfo, + background_color: Color, } impl Element for TerminalEl { @@ -114,9 +115,12 @@ impl Element for TerminalEl { .and_then(|cursor_line: usize| shaped_lines.get(cursor_line)) { let cursor_x = layout_line.x_for_index(content.cursor.point.column.0); - cursor = Some(RectF::new( - vec2f(cursor_x, cursor_line as f32 * line_height), - vec2f(cell_width, line_height), + cursor = Some(( + RectF::new( + vec2f(cursor_x, cursor_line as f32 * line_height), + vec2f(cell_width, line_height), + ), + terminal_theme.cursor, )); } @@ -128,6 +132,7 @@ impl Element for TerminalEl { em_width: cell_width, cursor, cur_size: new_size, + background_color: terminal_theme.background, }, ) } @@ -155,6 +160,14 @@ impl Element for TerminalEl { right_mouse_down_out: None, }); + //Background + cx.scene.push_quad(Quad { + bounds: visible_bounds, + background: Some(layout.background_color), + border: Default::default(), + corner_radius: 0., + }); + let origin = bounds.origin() + vec2f(layout.em_width, 0.); //Padding let mut line_origin = origin; @@ -168,12 +181,12 @@ impl Element for TerminalEl { line_origin.set_y(boundaries.max_y()); } - if let Some(c) = layout.cursor { + if let Some((c, color)) = layout.cursor { let new_origin = origin + c.origin(); let new_cursor = RectF::new(new_origin, c.size()); cx.scene.push_quad(Quad { bounds: new_cursor, - background: Some(Color::white()), + background: Some(color), border: Default::default(), corner_radius: 0., }); From b96962005e6824448f4904f6bca30dc11adaa616 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Jun 2022 09:54:14 +0200 Subject: [PATCH 26/60] Keep looking for a newer snapshot before broadcasting it --- crates/project/src/worktree.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 472dacc0ea..3afbcb1dd7 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -980,7 +980,11 @@ impl LocalWorktree { } } - while let Ok(snapshot) = snapshots_to_send_rx.recv().await { + while let Ok(mut snapshot) = snapshots_to_send_rx.recv().await { + while let Ok(newer_snapshot) = snapshots_to_send_rx.try_recv() { + snapshot = newer_snapshot; + } + let message = snapshot.build_update(&prev_snapshot, project_id, worktree_id, true); rpc.request(message).await?; From 5df0a6a425e17a3667355bcb4f7436ad9b24ee22 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Jun 2022 10:20:46 +0200 Subject: [PATCH 27/60] Coalesce as many fs events as possible before processing them --- crates/project/src/worktree.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 3afbcb1dd7..8af81f6d1a 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -45,6 +45,7 @@ use std::{ os::unix::prelude::{OsStrExt, OsStringExt}, path::{Path, PathBuf}, sync::{atomic::AtomicUsize, Arc}, + task::Poll, time::{Duration, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap}; @@ -2073,7 +2074,12 @@ impl BackgroundScanner { } futures::pin_mut!(events_rx); - while let Some(events) = events_rx.next().await { + + while let Some(mut events) = events_rx.next().await { + while let Poll::Ready(Some(additional_events)) = futures::poll!(events_rx.next()) { + events.extend(additional_events); + } + if self.notify.unbounded_send(ScanState::Scanning).is_err() { break; } From 09bb3ddeb88a250f743502849d4f18bd9b423d97 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Jun 2022 14:06:41 +0200 Subject: [PATCH 28/60] Split worktree updates and only send 256 entries at a time --- crates/collab/src/rpc.rs | 2 + crates/collab/src/rpc/store.rs | 3 + crates/project/src/project.rs | 1 + crates/project/src/worktree.rs | 150 ++++++++++++++++++--------------- crates/rpc/proto/zed.proto | 2 + 5 files changed, 88 insertions(+), 70 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 79c1e53a0b..e5a1f4dd1a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -804,6 +804,7 @@ impl Server { .collect(), visible: worktree.visible, scan_id: shared_worktree.scan_id, + is_complete: worktree.is_complete, }) }) .collect::>(); @@ -963,6 +964,7 @@ impl Server { &request.payload.removed_entries, &request.payload.updated_entries, request.payload.scan_id, + request.payload.is_last_update, )?; (connection_ids, metadata_changed, extension_counts.clone()) }; diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index d1eb4a3be6..1d36e971e2 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -62,6 +62,7 @@ pub struct Worktree { #[serde(skip)] pub diagnostic_summaries: BTreeMap, pub scan_id: u64, + pub is_complete: bool, } #[derive(Default)] @@ -615,6 +616,7 @@ impl Store { removed_entries: &[u64], updated_entries: &[proto::Entry], scan_id: u64, + is_last_update: bool, ) -> Result<(Vec, bool, HashMap)> { let project = self.write_project(project_id, connection_id)?; let connection_ids = project.connection_ids(); @@ -657,6 +659,7 @@ impl Store { } worktree.scan_id = scan_id; + worktree.is_complete = is_last_update; Ok(( connection_ids, metadata_changed, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8ce07a6abd..97b8b3a525 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4447,6 +4447,7 @@ impl Project { diagnostic_summaries: Default::default(), visible: worktree.visible, scan_id: 0, + is_complete: false, }; let (worktree, load_task) = Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 8af81f6d1a..5d105df446 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -83,7 +83,7 @@ pub struct RemoteWorktree { project_id: u64, client: Arc, updates_tx: Option>, - last_scan_id_rx: watch::Receiver, + snapshot_updated_rx: watch::Receiver<()>, replica_id: ReplicaId, diagnostic_summaries: TreeMap, visible: bool, @@ -97,6 +97,7 @@ pub struct Snapshot { entries_by_path: SumTree, entries_by_id: SumTree, scan_id: usize, + is_complete: bool, } #[derive(Clone)] @@ -191,12 +192,12 @@ impl Worktree { entries_by_path: Default::default(), entries_by_id: Default::default(), scan_id: worktree.scan_id as usize, + is_complete: worktree.is_complete, }; let (updates_tx, mut updates_rx) = mpsc::unbounded(); let background_snapshot = Arc::new(Mutex::new(snapshot.clone())); let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel(); - let (mut last_scan_id_tx, last_scan_id_rx) = watch::channel_with(worktree.scan_id as usize); let worktree_handle = cx.add_model(|_: &mut ModelContext| { Worktree::Remote(RemoteWorktree { project_id: project_remote_id, @@ -204,7 +205,7 @@ impl Worktree { snapshot: snapshot.clone(), background_snapshot: background_snapshot.clone(), updates_tx: Some(updates_tx), - last_scan_id_rx, + snapshot_updated_rx: snapshot_updated_rx.clone(), client: client.clone(), diagnostic_summaries: TreeMap::from_ordered_entries( worktree.diagnostic_summaries.into_iter().map(|summary| { @@ -279,11 +280,7 @@ impl Worktree { async move { while let Some(_) = snapshot_updated_rx.recv().await { if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.poll_snapshot(cx); - let this = this.as_remote_mut().unwrap(); - *last_scan_id_tx.borrow_mut() = this.snapshot.scan_id; - }); + this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); } else { break; } @@ -450,6 +447,7 @@ impl LocalWorktree { entries_by_path: Default::default(), entries_by_id: Default::default(), scan_id: 0, + is_complete: true, }, }; if let Some(metadata) = metadata { @@ -910,22 +908,20 @@ impl LocalWorktree { async move { let mut prev_snapshot = match snapshots_to_send_rx.recv().await { Ok(snapshot) => { - if let Err(error) = rpc - .request(proto::UpdateWorktree { - project_id, - worktree_id, - root_name: snapshot.root_name().to_string(), - updated_entries: snapshot - .entries_by_path - .iter() - .filter(|e| !e.is_ignored) - .map(Into::into) - .collect(), - removed_entries: Default::default(), - scan_id: snapshot.scan_id as u64, - }) - .await - { + let update = proto::UpdateWorktree { + project_id, + worktree_id, + root_name: snapshot.root_name().to_string(), + updated_entries: snapshot + .entries_by_path + .iter() + .map(Into::into) + .collect(), + removed_entries: Default::default(), + scan_id: snapshot.scan_id as u64, + is_last_update: true, + }; + if let Err(error) = send_worktree_update(&rpc, update).await { let _ = share_tx.send(Err(error)); return Err(anyhow!("failed to send initial update worktree")); } else { @@ -947,48 +943,16 @@ impl LocalWorktree { })?; } - // Stream ignored entries in chunks. - { - let mut ignored_entries = prev_snapshot - .entries_by_path - .iter() - .filter(|e| e.is_ignored); - let mut ignored_entries_to_send = Vec::new(); - loop { - #[cfg(any(test, feature = "test-support"))] - const CHUNK_SIZE: usize = 2; - #[cfg(not(any(test, feature = "test-support")))] - const CHUNK_SIZE: usize = 256; - - let entry = ignored_entries.next(); - if ignored_entries_to_send.len() >= CHUNK_SIZE || entry.is_none() { - rpc.request(proto::UpdateWorktree { - project_id, - worktree_id, - root_name: prev_snapshot.root_name().to_string(), - updated_entries: mem::take(&mut ignored_entries_to_send), - removed_entries: Default::default(), - scan_id: prev_snapshot.scan_id as u64, - }) - .await?; - } - - if let Some(entry) = entry { - ignored_entries_to_send.push(entry.into()); - } else { - break; - } - } - } - while let Ok(mut snapshot) = snapshots_to_send_rx.recv().await { while let Ok(newer_snapshot) = snapshots_to_send_rx.try_recv() { snapshot = newer_snapshot; } - let message = - snapshot.build_update(&prev_snapshot, project_id, worktree_id, true); - rpc.request(message).await?; + send_worktree_update( + &rpc, + snapshot.build_update(&prev_snapshot, project_id, worktree_id, true), + ) + .await?; prev_snapshot = snapshot; } @@ -1063,15 +1027,25 @@ impl RemoteWorktree { Ok(()) } - fn wait_for_snapshot(&self, scan_id: usize) -> impl Future { - let mut rx = self.last_scan_id_rx.clone(); - async move { - while let Some(applied_scan_id) = rx.next().await { - if applied_scan_id >= scan_id { - return; + fn wait_for_snapshot( + &self, + scan_id: usize, + cx: &mut ModelContext, + ) -> Task> { + let mut rx = self.snapshot_updated_rx.clone(); + cx.spawn_weak(|worktree, cx| async move { + while rx.recv().await.is_some() { + let snapshot = worktree + .upgrade(&cx)? + .read_with(&cx, |worktree, _| worktree.snapshot()); + if snapshot.scan_id > scan_id + || (snapshot.scan_id == scan_id && snapshot.is_complete) + { + break; } } - } + None + }) } pub fn update_diagnostic_summary( @@ -1098,7 +1072,7 @@ impl RemoteWorktree { scan_id: usize, cx: &mut ModelContext, ) -> Task> { - let wait_for_snapshot = self.wait_for_snapshot(scan_id); + let wait_for_snapshot = self.wait_for_snapshot(scan_id, cx); cx.spawn(|this, mut cx| async move { wait_for_snapshot.await; this.update(&mut cx, |worktree, _| { @@ -1117,7 +1091,7 @@ impl RemoteWorktree { scan_id: usize, cx: &mut ModelContext, ) -> Task> { - let wait_for_snapshot = self.wait_for_snapshot(scan_id); + let wait_for_snapshot = self.wait_for_snapshot(scan_id, cx); cx.spawn(|this, mut cx| async move { wait_for_snapshot.await; this.update(&mut cx, |worktree, _| { @@ -1210,6 +1184,7 @@ impl Snapshot { self.entries_by_path.edit(entries_by_path_edits, &()); self.entries_by_id.edit(entries_by_id_edits, &()); self.scan_id = update.scan_id as usize; + self.is_complete = update.is_last_update; Ok(()) } @@ -1352,6 +1327,7 @@ impl LocalSnapshot { .collect(), visible, scan_id: self.scan_id as u64, + is_complete: true, } } @@ -1418,6 +1394,7 @@ impl LocalSnapshot { updated_entries, removed_entries, scan_id: self.scan_id as u64, + is_last_update: true, } } @@ -2732,6 +2709,38 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { } } +async fn send_worktree_update( + client: &Arc, + mut update: proto::UpdateWorktree, +) -> Result<()> { + #[cfg(any(test, feature = "test-support"))] + const MAX_CHUNK_SIZE: usize = 2; + #[cfg(not(any(test, feature = "test-support")))] + const MAX_CHUNK_SIZE: usize = 256; + + loop { + let chunk_size = cmp::min(update.updated_entries.len(), MAX_CHUNK_SIZE); + let updated_entries = update.updated_entries.drain(..chunk_size).collect(); + let is_last_update = update.updated_entries.is_empty(); + client + .request(proto::UpdateWorktree { + project_id: update.project_id, + worktree_id: update.worktree_id, + root_name: update.root_name.clone(), + updated_entries, + removed_entries: mem::take(&mut update.removed_entries), + scan_id: update.scan_id, + is_last_update, + }) + .await?; + if is_last_update { + break; + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -2941,6 +2950,7 @@ mod tests { root_name: Default::default(), root_char_bag: Default::default(), scan_id: 0, + is_complete: true, }, }; initial_snapshot.insert_entry( diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 69ccae1704..8291b8ac98 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -195,6 +195,7 @@ message UpdateWorktree { repeated Entry updated_entries = 4; repeated uint64 removed_entries = 5; uint64 scan_id = 6; + bool is_last_update = 7; } message CreateProjectEntry { @@ -772,6 +773,7 @@ message Worktree { repeated DiagnosticSummary diagnostic_summaries = 4; bool visible = 5; uint64 scan_id = 6; + bool is_complete = 7; } message File { From 845c79ee05cd77b59ab78440612a77fea05b48c7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Jun 2022 14:29:06 +0200 Subject: [PATCH 29/60] Respond to join project request before sharing project completes This ensures the guest doesn't observe a huge delay when joining. --- crates/project/src/project.rs | 5 +++-- crates/project/src/worktree.rs | 8 +++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 97b8b3a525..10485b79e4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1332,12 +1332,13 @@ impl Project { let client = self.client.clone(); cx.foreground() .spawn(async move { - share.await?; client.send(proto::RespondToJoinProjectRequest { requester_id, project_id, allow, - }) + })?; + share.await?; + anyhow::Ok(()) }) .detach_and_log_err(cx); } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 5d105df446..6e183aa127 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2718,10 +2718,11 @@ async fn send_worktree_update( #[cfg(not(any(test, feature = "test-support")))] const MAX_CHUNK_SIZE: usize = 256; - loop { + let mut is_last_update = false; + while !is_last_update { let chunk_size = cmp::min(update.updated_entries.len(), MAX_CHUNK_SIZE); let updated_entries = update.updated_entries.drain(..chunk_size).collect(); - let is_last_update = update.updated_entries.is_empty(); + is_last_update = update.updated_entries.is_empty(); client .request(proto::UpdateWorktree { project_id: update.project_id, @@ -2733,9 +2734,6 @@ async fn send_worktree_update( is_last_update, }) .await?; - if is_last_update { - break; - } } Ok(()) From 484af8c7c488ba6180e1852bde89ab344c6c48ae Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Jun 2022 16:49:56 +0200 Subject: [PATCH 30/60] Split worktree updates when a peer joins an already-shared project --- crates/collab/src/integration_tests.rs | 1 + crates/collab/src/rpc.rs | 63 +++++++--- crates/project/src/project.rs | 52 ++++---- crates/project/src/worktree.rs | 167 ++++++------------------- crates/rpc/proto/zed.proto | 12 +- crates/rpc/src/proto.rs | 26 ++++ 6 files changed, 137 insertions(+), 184 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index facef17b63..3da1fc8692 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -1472,6 +1472,7 @@ async fn test_collaborating_with_diagnostics( // Join project as client C and observe the diagnostics. let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await; + deterministic.run_until_parked(); project_c.read_with(cx_c, |project, cx| { assert_eq!( project.diagnostic_summaries(cx).collect::>(), diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index e5a1f4dd1a..b3dc965ff3 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -791,21 +791,10 @@ impl Server { let worktrees = project .worktrees .iter() - .filter_map(|(id, shared_worktree)| { - let worktree = project.worktrees.get(&id)?; - Some(proto::Worktree { - id: *id, - root_name: worktree.root_name.clone(), - entries: shared_worktree.entries.values().cloned().collect(), - diagnostic_summaries: shared_worktree - .diagnostic_summaries - .values() - .cloned() - .collect(), - visible: worktree.visible, - scan_id: shared_worktree.scan_id, - is_complete: worktree.is_complete, - }) + .map(|(id, worktree)| proto::WorktreeMetadata { + id: *id, + root_name: worktree.root_name.clone(), + visible: worktree.visible, }) .collect::>(); @@ -841,14 +830,15 @@ impl Server { } } - for (receipt, replica_id) in receipts_with_replica_ids { + // First, we send the metadata associated with each worktree. + for (receipt, replica_id) in &receipts_with_replica_ids { self.peer.respond( - receipt, + receipt.clone(), proto::JoinProjectResponse { variant: Some(proto::join_project_response::Variant::Accept( proto::join_project_response::Accept { worktrees: worktrees.clone(), - replica_id: replica_id as u32, + replica_id: *replica_id as u32, collaborators: collaborators.clone(), language_servers: project.language_servers.clone(), }, @@ -856,6 +846,43 @@ impl Server { }, )?; } + + for (worktree_id, worktree) in &project.worktrees { + #[cfg(any(test, feature = "test-support"))] + const MAX_CHUNK_SIZE: usize = 2; + #[cfg(not(any(test, feature = "test-support")))] + const MAX_CHUNK_SIZE: usize = 256; + + // Stream this worktree's entries. + let message = proto::UpdateWorktree { + project_id: project_id.to_proto(), + worktree_id: *worktree_id, + root_name: worktree.root_name.clone(), + updated_entries: worktree.entries.values().cloned().collect(), + removed_entries: Default::default(), + scan_id: worktree.scan_id, + is_last_update: worktree.is_complete, + }; + for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { + for (receipt, _) in &receipts_with_replica_ids { + self.peer.send(receipt.sender_id, update.clone())?; + } + } + + // Stream this worktree's diagnostics. + for summary in worktree.diagnostic_summaries.values() { + for (receipt, _) in &receipts_with_replica_ids { + self.peer.send( + receipt.sender_id, + proto::UpdateDiagnosticSummary { + project_id: project_id.to_proto(), + worktree_id: *worktree_id, + summary: Some(summary.clone()), + }, + )?; + } + } + } } self.update_user_contacts(host_user_id).await?; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 10485b79e4..806498a224 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -487,10 +487,9 @@ impl Project { let mut worktrees = Vec::new(); for worktree in response.worktrees { - let (worktree, load_task) = cx + let worktree = cx .update(|cx| Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx)); worktrees.push(worktree); - load_task.detach(); } let (opened_buffer_tx, opened_buffer_rx) = watch::channel(); @@ -4441,19 +4440,9 @@ impl Project { { this.worktrees.push(WorktreeHandle::Strong(old_worktree)); } else { - let worktree = proto::Worktree { - id: worktree.id, - root_name: worktree.root_name, - entries: Default::default(), - diagnostic_summaries: Default::default(), - visible: worktree.visible, - scan_id: 0, - is_complete: false, - }; - let (worktree, load_task) = + let worktree = Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx); this.add_worktree(&worktree, cx); - load_task.detach(); } } @@ -4477,8 +4466,8 @@ impl Project { if let Some(worktree) = this.worktree_for_id(worktree_id, cx) { worktree.update(cx, |worktree, _| { let worktree = worktree.as_remote_mut().unwrap(); - worktree.update_from_remote(envelope) - })?; + worktree.update_from_remote(envelope.payload); + }); } Ok(()) }) @@ -7996,7 +7985,10 @@ mod tests { } #[gpui::test(retries = 5)] - async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) { + async fn test_rescan_and_remote_updates( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { let dir = temp_tree(json!({ "a": { "file1": "", @@ -8040,17 +8032,24 @@ mod tests { // Create a remote copy of this worktree. let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); let initial_snapshot = tree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); - let (remote, load_task) = cx.update(|cx| { + let remote = cx.update(|cx| { Worktree::remote( 1, 1, - initial_snapshot.to_proto(&Default::default(), true), + proto::WorktreeMetadata { + id: initial_snapshot.id().to_proto(), + root_name: initial_snapshot.root_name().into(), + visible: true, + }, rpc.clone(), cx, ) }); - // tree - load_task.await; + remote.update(cx, |remote, _| { + let update = initial_snapshot.build_initial_update(1); + remote.as_remote_mut().unwrap().update_from_remote(update); + }); + deterministic.run_until_parked(); cx.read(|cx| { assert!(!buffer2.read(cx).is_dirty()); @@ -8116,19 +8115,16 @@ mod tests { // Update the remote worktree. Check that it becomes consistent with the // local worktree. remote.update(cx, |remote, cx| { - let update_message = tree.read(cx).as_local().unwrap().snapshot().build_update( + let update = tree.read(cx).as_local().unwrap().snapshot().build_update( &initial_snapshot, 1, 1, true, ); - remote - .as_remote_mut() - .unwrap() - .snapshot - .apply_remote_update(update_message) - .unwrap(); - + remote.as_remote_mut().unwrap().update_from_remote(update); + }); + deterministic.run_until_parked(); + remote.read_with(cx, |remote, _| { assert_eq!( remote .paths() diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 6e183aa127..217a512c71 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -7,7 +7,7 @@ use super::{ }; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context, Result}; -use client::{proto, Client, TypedEnvelope}; +use client::{proto, Client}; use clock::ReplicaId; use collections::HashMap; use futures::{ @@ -40,7 +40,6 @@ use std::{ ffi::{OsStr, OsString}, fmt, future::Future, - mem, ops::{Deref, DerefMut}, os::unix::prelude::{OsStrExt, OsStringExt}, path::{Path, PathBuf}, @@ -173,10 +172,10 @@ impl Worktree { pub fn remote( project_remote_id: u64, replica_id: ReplicaId, - worktree: proto::Worktree, + worktree: proto::WorktreeMetadata, client: Arc, cx: &mut MutableAppContext, - ) -> (ModelHandle, Task<()>) { + ) -> ModelHandle { let remote_id = worktree.id; let root_char_bag: CharBag = worktree .root_name @@ -191,8 +190,8 @@ impl Worktree { root_char_bag, entries_by_path: Default::default(), entries_by_id: Default::default(), - scan_id: worktree.scan_id as usize, - is_complete: worktree.is_complete, + scan_id: 0, + is_complete: false, }; let (updates_tx, mut updates_rx) = mpsc::unbounded(); @@ -207,90 +206,37 @@ impl Worktree { updates_tx: Some(updates_tx), snapshot_updated_rx: snapshot_updated_rx.clone(), client: client.clone(), - diagnostic_summaries: TreeMap::from_ordered_entries( - worktree.diagnostic_summaries.into_iter().map(|summary| { - ( - PathKey(PathBuf::from(summary.path).into()), - DiagnosticSummary { - language_server_id: summary.language_server_id as usize, - error_count: summary.error_count as usize, - warning_count: summary.warning_count as usize, - }, - ) - }), - ), + diagnostic_summaries: Default::default(), visible, }) }); - let deserialize_task = cx.spawn({ - let worktree_handle = worktree_handle.clone(); - |cx| async move { - let (entries_by_path, entries_by_id) = cx - .background() - .spawn(async move { - let mut entries_by_path_edits = Vec::new(); - let mut entries_by_id_edits = Vec::new(); - for entry in worktree.entries { - match Entry::try_from((&root_char_bag, entry)) { - Ok(entry) => { - entries_by_id_edits.push(Edit::Insert(PathEntry { - id: entry.id, - path: entry.path.clone(), - is_ignored: entry.is_ignored, - scan_id: 0, - })); - entries_by_path_edits.push(Edit::Insert(entry)); - } - Err(err) => log::warn!("error for remote worktree entry {:?}", err), - } - } - - let mut entries_by_path = SumTree::new(); - let mut entries_by_id = SumTree::new(); - entries_by_path.edit(entries_by_path_edits, &()); - entries_by_id.edit(entries_by_id_edits, &()); - - (entries_by_path, entries_by_id) - }) - .await; - - { - let mut snapshot = background_snapshot.lock(); - snapshot.entries_by_path = entries_by_path; - snapshot.entries_by_id = entries_by_id; + cx.background() + .spawn(async move { + while let Some(update) = updates_rx.next().await { + if let Err(error) = background_snapshot.lock().apply_remote_update(update) { + log::error!("error applying worktree update: {}", error); + } snapshot_updated_tx.send(()).await.ok(); } + }) + .detach(); - cx.background() - .spawn(async move { - while let Some(update) = updates_rx.next().await { - if let Err(error) = - background_snapshot.lock().apply_remote_update(update) - { - log::error!("error applying worktree update: {}", error); - } - snapshot_updated_tx.send(()).await.ok(); - } - }) - .detach(); - - cx.spawn(|mut cx| { - let this = worktree_handle.downgrade(); - async move { - while let Some(_) = snapshot_updated_rx.recv().await { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); - } else { - break; - } - } + cx.spawn(|mut cx| { + let this = worktree_handle.downgrade(); + async move { + while let Some(_) = snapshot_updated_rx.recv().await { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); + } else { + break; } - }) - .detach(); + } } - }); - (worktree_handle, deserialize_task) + }) + .detach(); + + worktree_handle } pub fn as_local(&self) -> Option<&LocalWorktree> { @@ -1015,16 +961,12 @@ impl RemoteWorktree { self.updates_tx.take(); } - pub fn update_from_remote( - &mut self, - envelope: TypedEnvelope, - ) -> Result<()> { + pub fn update_from_remote(&mut self, update: proto::UpdateWorktree) { if let Some(updates_tx) = &self.updates_tx { updates_tx - .unbounded_send(envelope.payload) + .unbounded_send(update) .expect("consumer runs to completion"); } - Ok(()) } fn wait_for_snapshot( @@ -1162,7 +1104,7 @@ impl Snapshot { for entry_id in update.removed_entries { let entry = self .entry_for_id(ProjectEntryId::from_proto(entry_id)) - .ok_or_else(|| anyhow!("unknown entry"))?; + .ok_or_else(|| anyhow!("unknown entry {}", entry_id))?; entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone()))); entries_by_id_edits.push(Edit::Remove(entry.id)); } @@ -1306,28 +1248,16 @@ impl LocalSnapshot { } #[cfg(test)] - pub(crate) fn to_proto( - &self, - diagnostic_summaries: &TreeMap, - visible: bool, - ) -> proto::Worktree { + pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree { let root_name = self.root_name.clone(); - proto::Worktree { - id: self.id.0 as u64, + proto::UpdateWorktree { + project_id, + worktree_id: self.id().to_proto(), root_name, - entries: self - .entries_by_path - .iter() - .filter(|e| !e.is_ignored) - .map(Into::into) - .collect(), - diagnostic_summaries: diagnostic_summaries - .iter() - .map(|(path, summary)| summary.to_proto(&path.0)) - .collect(), - visible, + updated_entries: self.entries_by_path.iter().map(Into::into).collect(), + removed_entries: Default::default(), scan_id: self.scan_id as u64, - is_complete: true, + is_last_update: true, } } @@ -2709,31 +2639,14 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { } } -async fn send_worktree_update( - client: &Arc, - mut update: proto::UpdateWorktree, -) -> Result<()> { +async fn send_worktree_update(client: &Arc, update: proto::UpdateWorktree) -> Result<()> { #[cfg(any(test, feature = "test-support"))] const MAX_CHUNK_SIZE: usize = 2; #[cfg(not(any(test, feature = "test-support")))] const MAX_CHUNK_SIZE: usize = 256; - let mut is_last_update = false; - while !is_last_update { - let chunk_size = cmp::min(update.updated_entries.len(), MAX_CHUNK_SIZE); - let updated_entries = update.updated_entries.drain(..chunk_size).collect(); - is_last_update = update.updated_entries.is_empty(); - client - .request(proto::UpdateWorktree { - project_id: update.project_id, - worktree_id: update.worktree_id, - root_name: update.root_name.clone(), - updated_entries, - removed_entries: mem::take(&mut update.removed_entries), - scan_id: update.scan_id, - is_last_update, - }) - .await?; + for update in proto::split_worktree_update(update, MAX_CHUNK_SIZE) { + client.request(update).await?; } Ok(()) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 8291b8ac98..e3ca60c251 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -168,7 +168,7 @@ message JoinProjectResponse { message Accept { uint32 replica_id = 1; - repeated Worktree worktrees = 2; + repeated WorktreeMetadata worktrees = 2; repeated Collaborator collaborators = 3; repeated LanguageServer language_servers = 4; } @@ -766,16 +766,6 @@ message User { string avatar_url = 3; } -message Worktree { - uint64 id = 1; - string root_name = 2; - repeated Entry entries = 3; - repeated DiagnosticSummary diagnostic_summaries = 4; - bool visible = 5; - uint64 scan_id = 6; - bool is_complete = 7; -} - message File { uint64 worktree_id = 1; optional uint64 entry_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index ecee370986..6200f37cd2 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -5,6 +5,7 @@ use futures::{SinkExt as _, StreamExt as _}; use prost::Message as _; use serde::Serialize; use std::any::{Any, TypeId}; +use std::{cmp, iter, mem}; use std::{ fmt::Debug, io, @@ -390,6 +391,31 @@ impl From for u128 { } } +pub fn split_worktree_update( + mut message: UpdateWorktree, + max_chunk_size: usize, +) -> impl Iterator { + let mut done = false; + iter::from_fn(move || { + if done { + return None; + } + + let chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size); + let updated_entries = message.updated_entries.drain(..chunk_size).collect(); + done = message.updated_entries.is_empty(); + Some(UpdateWorktree { + project_id: message.project_id, + worktree_id: message.worktree_id, + root_name: message.root_name.clone(), + updated_entries, + removed_entries: mem::take(&mut message.removed_entries), + scan_id: message.scan_id, + is_last_update: done && message.is_last_update, + }) + }) +} + #[cfg(test)] mod tests { use super::*; From 4ee8ee5a066e91164d85e2ea698f45f591e02375 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Jun 2022 18:04:19 +0200 Subject: [PATCH 31/60] Ensure newer snapshots are always detected in `wait_for_snapshot` --- crates/project/src/project.rs | 8 ++--- crates/project/src/worktree.rs | 64 ++++++++++++++++++++-------------- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 806498a224..ec27433049 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1081,7 +1081,7 @@ impl Project { .ok_or_else(|| anyhow!("missing entry in response"))?; worktree .update(&mut cx, |worktree, cx| { - worktree.as_remote().unwrap().insert_entry( + worktree.as_remote_mut().unwrap().insert_entry( entry, response.worktree_scan_id as usize, cx, @@ -1124,7 +1124,7 @@ impl Project { .ok_or_else(|| anyhow!("missing entry in response"))?; worktree .update(&mut cx, |worktree, cx| { - worktree.as_remote().unwrap().insert_entry( + worktree.as_remote_mut().unwrap().insert_entry( entry, response.worktree_scan_id as usize, cx, @@ -1167,7 +1167,7 @@ impl Project { .ok_or_else(|| anyhow!("missing entry in response"))?; worktree .update(&mut cx, |worktree, cx| { - worktree.as_remote().unwrap().insert_entry( + worktree.as_remote_mut().unwrap().insert_entry( entry, response.worktree_scan_id as usize, cx, @@ -1200,7 +1200,7 @@ impl Project { .await?; worktree .update(&mut cx, move |worktree, cx| { - worktree.as_remote().unwrap().delete_entry( + worktree.as_remote_mut().unwrap().delete_entry( entry_id, response.worktree_scan_id as usize, cx, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 217a512c71..5eb3c3dbd6 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -9,7 +9,7 @@ use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context, Result}; use client::{proto, Client}; use clock::ReplicaId; -use collections::HashMap; +use collections::{HashMap, VecDeque}; use futures::{ channel::{ mpsc::{self, UnboundedSender}, @@ -82,7 +82,7 @@ pub struct RemoteWorktree { project_id: u64, client: Arc, updates_tx: Option>, - snapshot_updated_rx: watch::Receiver<()>, + snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>, replica_id: ReplicaId, diagnostic_summaries: TreeMap, visible: bool, @@ -204,7 +204,7 @@ impl Worktree { snapshot: snapshot.clone(), background_snapshot: background_snapshot.clone(), updates_tx: Some(updates_tx), - snapshot_updated_rx: snapshot_updated_rx.clone(), + snapshot_subscriptions: Default::default(), client: client.clone(), diagnostic_summaries: Default::default(), visible, @@ -227,7 +227,18 @@ impl Worktree { async move { while let Some(_) = snapshot_updated_rx.recv().await { if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); + this.update(&mut cx, |this, cx| { + this.poll_snapshot(cx); + let this = this.as_remote_mut().unwrap(); + while let Some((scan_id, _)) = this.snapshot_subscriptions.front() { + if this.observed_snapshot(*scan_id) { + let (_, tx) = this.snapshot_subscriptions.pop_front().unwrap(); + let _ = tx.send(()); + } else { + break; + } + } + }); } else { break; } @@ -969,25 +980,26 @@ impl RemoteWorktree { } } - fn wait_for_snapshot( - &self, - scan_id: usize, - cx: &mut ModelContext, - ) -> Task> { - let mut rx = self.snapshot_updated_rx.clone(); - cx.spawn_weak(|worktree, cx| async move { - while rx.recv().await.is_some() { - let snapshot = worktree - .upgrade(&cx)? - .read_with(&cx, |worktree, _| worktree.snapshot()); - if snapshot.scan_id > scan_id - || (snapshot.scan_id == scan_id && snapshot.is_complete) - { - break; - } + fn observed_snapshot(&self, scan_id: usize) -> bool { + self.scan_id > scan_id || (self.scan_id == scan_id && self.is_complete) + } + + fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future { + let (tx, rx) = oneshot::channel(); + if self.observed_snapshot(scan_id) { + let _ = tx.send(()); + } else { + match self + .snapshot_subscriptions + .binary_search_by_key(&scan_id, |probe| probe.0) + { + Ok(ix) | Err(ix) => self.snapshot_subscriptions.insert(ix, (scan_id, tx)), } - None - }) + } + + async move { + let _ = rx.await; + } } pub fn update_diagnostic_summary( @@ -1009,12 +1021,12 @@ impl RemoteWorktree { } pub fn insert_entry( - &self, + &mut self, entry: proto::Entry, scan_id: usize, cx: &mut ModelContext, ) -> Task> { - let wait_for_snapshot = self.wait_for_snapshot(scan_id, cx); + let wait_for_snapshot = self.wait_for_snapshot(scan_id); cx.spawn(|this, mut cx| async move { wait_for_snapshot.await; this.update(&mut cx, |worktree, _| { @@ -1028,12 +1040,12 @@ impl RemoteWorktree { } pub(crate) fn delete_entry( - &self, + &mut self, id: ProjectEntryId, scan_id: usize, cx: &mut ModelContext, ) -> Task> { - let wait_for_snapshot = self.wait_for_snapshot(scan_id, cx); + let wait_for_snapshot = self.wait_for_snapshot(scan_id); cx.spawn(|this, mut cx| async move { wait_for_snapshot.await; this.update(&mut cx, |worktree, _| { From 66486870aaeaa1719ec18b9573e1b6d59a941c8c Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 30 Jun 2022 12:32:53 -0700 Subject: [PATCH 32/60] Fix vim editor focus selection issues, cancel vim operators on escape and unbound keys --- Cargo.lock | 1 + assets/keymaps/vim.json | 22 +++--- crates/search/src/buffer_search.rs | 2 +- crates/vim/Cargo.toml | 1 + crates/vim/src/editor_events.rs | 13 +++- crates/vim/src/normal.rs | 2 +- crates/vim/src/normal/delete.rs | 40 ++++++++++- crates/vim/src/state.rs | 27 +++++--- crates/vim/src/vim.rs | 104 +++++++++++++++++++++++++---- crates/vim/src/vim_test_context.rs | 26 +++++++- crates/zed/src/zed.rs | 1 + styles/package-lock.json | 1 + 12 files changed, 198 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 07e688c6b8..88e9dc731a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5768,6 +5768,7 @@ dependencies = [ "language", "log", "project", + "search", "serde", "settings", "util", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 6fcd5d3d12..0d2a611d46 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -37,16 +37,12 @@ "ignorePunctuation": true } ], - "escape": [ - "vim::SwitchMode", - "Normal" - ] + "escape": "editor::Cancel" } }, { - "context": "Editor && vim_mode == normal", + "context": "Editor && vim_mode == normal && vim_operator == none", "bindings": { - "escape": "editor::Cancel", "c": [ "vim::PushOperator", "Change" @@ -92,7 +88,13 @@ "p": "vim::Paste", "u": "editor::Undo", "ctrl-r": "editor::Redo", - "ctrl-o": "pane::GoBack" + "ctrl-o": "pane::GoBack", + "/": [ + "buffer_search::Deploy", + { + "focus": true + } + ] } }, { @@ -146,11 +148,5 @@ "escape": "vim::NormalBefore", "ctrl-c": "vim::NormalBefore" } - }, - { - "context": "Editor && mode == singleline", - "bindings": { - "escape": "editor::Cancel" - } } ] \ No newline at end of file diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index de9e8af9c4..59f78cdc33 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -58,7 +58,7 @@ fn add_toggle_option_action(option: SearchOption, cx: &mut MutableApp } pub struct BufferSearchBar { - query_editor: ViewHandle, + pub query_editor: ViewHandle, active_editor: Option>, active_match_index: Option, active_editor_subscription: Option, diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 56b0aec8cc..5bc32cd5bd 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -14,6 +14,7 @@ command_palette = { path = "../command_palette" } editor = { path = "../editor" } gpui = { path = "../gpui" } language = { path = "../language" } +search = { path = "../search" } serde = { version = "1.0", features = ["derive", "rc"] } settings = { path = "../settings" } workspace = { path = "../workspace" } diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 6ad0cb77c4..c68e5182b0 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -29,8 +29,17 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont } })); - if editor.read(cx).mode() != EditorMode::Full { - vim.switch_mode(Mode::Insert, cx); + if !vim.enabled { + return; + } + + let editor = editor.read(cx); + if editor.selections.newest::(cx).is_empty() { + if editor.mode() != EditorMode::Full { + vim.switch_mode(Mode::Insert, cx); + } + } else { + vim.switch_mode(Mode::Visual { line: false }, cx); } }); } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 55c9779581..4c6dfd2d60 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1165,7 +1165,7 @@ mod test { The quick brown fox [jump}s over the lazy dog"}, - Mode::Normal, + Mode::Visual { line: false }, ); cx.simulate_keystroke("y"); cx.set_state( diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index cea607e9f3..c5c823c79e 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -40,7 +40,7 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) { mod test { use indoc::indoc; - use crate::vim_test_context::VimTestContext; + use crate::{state::Mode, vim_test_context::VimTestContext}; #[gpui::test] async fn test_delete_h(cx: &mut gpui::TestAppContext) { @@ -390,4 +390,42 @@ mod test { the lazy"}, ); } + + #[gpui::test] + async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.set_state( + indoc! {" + The quick brown + fox ju|mps over + the lazy dog"}, + Mode::Normal, + ); + + // Canceling operator twice reverts to normal mode with no active operator + cx.simulate_keystrokes(["d", "escape", "k"]); + assert_eq!(cx.active_operator(), None); + assert_eq!(cx.mode(), Mode::Normal); + cx.assert_editor_state(indoc! {" + The qu|ick brown + fox jumps over + the lazy dog"}); + } + + #[gpui::test] + async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.set_state( + indoc! {" + The quick brown + fox ju|mps over + the lazy dog"}, + Mode::Normal, + ); + + // Canceling operator twice reverts to normal mode with no active operator + cx.simulate_keystrokes(["d", "y"]); + assert_eq!(cx.active_operator(), None); + assert_eq!(cx.mode(), Mode::Normal); + } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index a08b8bd2d2..e36cb7203d 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -37,7 +37,14 @@ pub struct VimState { impl VimState { pub fn cursor_shape(&self) -> CursorShape { match self.mode { - Mode::Normal | Mode::Visual { .. } => CursorShape::Block, + Mode::Normal => { + if self.operator_stack.is_empty() { + CursorShape::Block + } else { + CursorShape::Underscore + } + } + Mode::Visual { .. } => CursorShape::Block, Mode::Insert => CursorShape::Bar, } } @@ -73,20 +80,20 @@ impl VimState { context.set.insert("VimControl".to_string()); } - if let Some(operator) = &self.operator_stack.last() { - operator.set_context(&mut context); - } + Operator::set_context(self.operator_stack.last(), &mut context); + context } } impl Operator { - pub fn set_context(&self, context: &mut Context) { - let operator_context = match self { - Operator::Namespace(Namespace::G) => "g", - Operator::Change => "c", - Operator::Delete => "d", - Operator::Yank => "y", + pub fn set_context(operator: Option<&Operator>, context: &mut Context) { + let operator_context = match operator { + Some(Operator::Namespace(Namespace::G)) => "g", + Some(Operator::Change) => "c", + Some(Operator::Delete) => "d", + Some(Operator::Yank) => "y", + None => "none", } .to_owned(); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 82567f9829..5655e51e29 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -11,7 +11,7 @@ mod visual; use collections::HashMap; use command_palette::CommandPaletteFilter; -use editor::{Bias, CursorShape, Editor, Input}; +use editor::{Bias, Cancel, CursorShape, Editor, Input}; use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle}; use serde::Deserialize; @@ -34,6 +34,7 @@ pub fn init(cx: &mut MutableAppContext) { insert::init(cx); motion::init(cx); + // Vim Actions cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| { Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx)) }); @@ -42,7 +43,11 @@ pub fn init(cx: &mut MutableAppContext) { Vim::update(cx, |vim, cx| vim.push_operator(operator, cx)) }, ); + + // Editor Actions cx.add_action(|_: &mut Editor, _: &Input, cx| { + // If we have an unbound input with an active operator, cancel that operator. Otherwise forward + // the input to the editor if Vim::read(cx).active_operator().is_some() { // Defer without updating editor MutableAppContext::defer(cx, |cx| Vim::update(cx, |vim, cx| vim.clear_operator(cx))) @@ -50,6 +55,20 @@ pub fn init(cx: &mut MutableAppContext) { cx.propagate_action() } }); + cx.add_action(|_: &mut Editor, _: &Cancel, cx| { + // If we are in a non normal mode or have an active operator, swap to normal mode + // Otherwise forward cancel on to the editor + let vim = Vim::read(cx); + if vim.state.mode != Mode::Normal || vim.active_operator().is_some() { + MutableAppContext::defer(cx, |cx| { + Vim::update(cx, |state, cx| { + state.switch_mode(Mode::Normal, cx); + }); + }); + } else { + cx.propagate_action(); + } + }); // Sync initial settings with the rest of the app Vim::update(cx, |state, cx| state.sync_vim_settings(cx)); @@ -97,9 +116,46 @@ impl Vim { } fn switch_mode(&mut self, mode: Mode, cx: &mut MutableAppContext) { + let previous_mode = self.state.mode; self.state.mode = mode; self.state.operator_stack.clear(); + + // Sync editor settings like clip mode self.sync_vim_settings(cx); + + // Adjust selections + for editor in self.editors.values() { + if let Some(editor) = editor.upgrade(cx) { + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + // If empty selections + if self.state.empty_selections_only() { + let new_head = map.clip_point(selection.head(), Bias::Left); + selection.collapse_to(new_head, selection.goal) + } else { + if matches!(mode, Mode::Visual { line: false }) + && !matches!(previous_mode, Mode::Visual { .. }) + && !selection.reversed + && !selection.is_empty() + { + // Mode wasn't visual mode before, but is now. We need to move the end + // back by one character so that the region to be modifed stays the same + *selection.end.column_mut() = + selection.end.column().saturating_sub(1); + selection.end = map.clip_point(selection.end, Bias::Left); + } + + selection.set_head( + map.clip_point(selection.head(), Bias::Left), + selection.goal, + ); + } + }); + }) + }) + } + } } fn push_operator(&mut self, operator: Operator, cx: &mut MutableAppContext) { @@ -127,7 +183,7 @@ impl Vim { self.enabled = enabled; self.state = Default::default(); if enabled { - self.state.mode = Mode::Normal; + self.switch_mode(Mode::Normal, cx); } self.sync_vim_settings(cx); } @@ -156,17 +212,6 @@ impl Vim { matches!(state.mode, Mode::Visual { line: true }); let context_layer = state.keymap_context_layer(); editor.set_keymap_context_layer::(context_layer); - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - selection.set_head( - map.clip_point(selection.head(), Bias::Left), - selection.goal, - ); - if state.empty_selections_only() { - selection.collapse_to(selection.head(), selection.goal) - } - }); - }) } else { editor.set_cursor_shape(CursorShape::Bar, cx); editor.set_clip_at_line_ends(false, cx); @@ -182,6 +227,9 @@ impl Vim { #[cfg(test)] mod test { + use indoc::indoc; + use search::BufferSearchBar; + use crate::{state::Mode, vim_test_context::VimTestContext}; #[gpui::test] @@ -226,4 +274,34 @@ mod test { cx.enable_vim(); assert_eq!(cx.mode(), Mode::Normal); } + + #[gpui::test] + async fn test_buffer_search_switches_mode(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state( + indoc! {" + The quick brown + fox ju|mps over + the lazy dog"}, + Mode::Normal, + ); + cx.simulate_keystroke("/"); + + assert_eq!(cx.mode(), Mode::Visual { line: false }); + + let search_bar = cx.workspace(|workspace, cx| { + workspace + .active_pane() + .read(cx) + .toolbar() + .read(cx) + .item_of_type::() + .expect("Buffer search bar should be deployed") + }); + + search_bar.read_with(cx.cx, |bar, cx| { + assert_eq!(bar.query_editor.read(cx).text(cx), "jumps"); + }) + } } diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index 52d4778b38..57d0174703 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/crates/vim/src/vim_test_context.rs @@ -1,14 +1,16 @@ use std::ops::{Deref, DerefMut}; use editor::test::EditorTestContext; -use gpui::json::json; +use gpui::{json::json, AppContext, ViewHandle}; use project::Project; +use search::{BufferSearchBar, ProjectSearchBar}; use workspace::{pane, AppState, WorkspaceHandle}; use crate::{state::Operator, *}; pub struct VimTestContext<'a> { cx: EditorTestContext<'a>, + workspace: ViewHandle, } impl<'a> VimTestContext<'a> { @@ -16,6 +18,7 @@ impl<'a> VimTestContext<'a> { cx.update(|cx| { editor::init(cx); pane::init(cx); + search::init(cx); crate::init(cx); settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap(); @@ -37,6 +40,19 @@ impl<'a> VimTestContext<'a> { .await; let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx)); + + // Setup search toolbars + workspace.update(cx, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.toolbar().update(cx, |toolbar, cx| { + let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx)); + toolbar.add_item(buffer_search_bar, cx); + let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + toolbar.add_item(project_search_bar, cx); + }) + }); + }); + project .update(cx, |project, cx| { project.find_or_create_local_worktree("/root", true, cx) @@ -64,9 +80,17 @@ impl<'a> VimTestContext<'a> { window_id, editor, }, + workspace, } } + pub fn workspace(&mut self, read: F) -> T + where + F: FnOnce(&Workspace, &AppContext) -> T, + { + self.workspace.read_with(self.cx.cx, read) + } + pub fn enable_vim(&mut self) { self.cx.update(|cx| { cx.update_global(|settings: &mut Settings, _| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 7240aaef2f..548b726af9 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -97,6 +97,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_action({ let app_state = app_state.clone(); move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { + println!("open settings"); open_config_file(&SETTINGS_PATH, app_state.clone(), cx); } }); diff --git a/styles/package-lock.json b/styles/package-lock.json index 2eb6d3a1bf..49304dc2fa 100644 --- a/styles/package-lock.json +++ b/styles/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "styles", "version": "1.0.0", "license": "ISC", "dependencies": { From c5351a12761416405426b380ad7246a68447ed03 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Jun 2022 14:51:22 -0700 Subject: [PATCH 33/60] Ensure that usernames, user ids, and client ids match in random collaboration test This makes the logs easier to interpret --- crates/client/src/client.rs | 17 ++++++++--------- crates/collab/src/db.rs | 2 +- crates/collab/src/integration_tests.rs | 19 ++++++++++++------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 084d43af1b..538b0fa4b0 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -28,10 +28,7 @@ use std::{ convert::TryFrom, fmt::Write as _, future::Future, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, Weak, - }, + sync::{Arc, Weak}, time::{Duration, Instant}, }; use thiserror::Error; @@ -232,12 +229,8 @@ impl Drop for Subscription { impl Client { pub fn new(http: Arc) -> Arc { - lazy_static! { - static ref NEXT_CLIENT_ID: AtomicUsize = AtomicUsize::default(); - } - Arc::new(Self { - id: NEXT_CLIENT_ID.fetch_add(1, Ordering::SeqCst), + id: 0, peer: Peer::new(), http, state: Default::default(), @@ -257,6 +250,12 @@ impl Client { self.http.clone() } + #[cfg(any(test, feature = "test-support"))] + pub fn set_id(&mut self, id: usize) -> &Self { + self.id = id; + self + } + #[cfg(any(test, feature = "test-support"))] pub fn tear_down(&self) { let mut state = self.state.write(); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index ce494db59b..6c40996ae4 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -2117,7 +2117,7 @@ pub mod tests { Self { background, users: Default::default(), - next_user_id: Mutex::new(1), + next_user_id: Mutex::new(0), projects: Default::default(), worktree_extensions: Default::default(), next_project_id: Mutex::new(1), diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 3da1fc8692..d901bd060c 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -4426,8 +4426,16 @@ async fn test_random_collaboration( let mut server = TestServer::start(cx.foreground(), cx.background()).await; let db = server.app_state.db.clone(); let host_user_id = db.create_user("host", None, false).await.unwrap(); - for username in ["guest-1", "guest-2", "guest-3", "guest-4"] { + let mut available_guests = vec![ + "guest-1".to_string(), + "guest-2".to_string(), + "guest-3".to_string(), + "guest-4".to_string(), + ]; + + for username in &available_guests { let guest_user_id = db.create_user(username, None, false).await.unwrap(); + assert_eq!(*username, format!("guest-{}", guest_user_id)); server .app_state .db @@ -4621,12 +4629,7 @@ async fn test_random_collaboration( } else { max_operations }; - let mut available_guests = vec![ - "guest-1".to_string(), - "guest-2".to_string(), - "guest-3".to_string(), - "guest-4".to_string(), - ]; + let mut operations = 0; while operations < max_operations { if operations == disconnect_host_at { @@ -4729,6 +4732,7 @@ async fn test_random_collaboration( server.disconnect_client(removed_guest_id); deterministic.advance_clock(RECEIVE_TIMEOUT); deterministic.start_waiting(); + log::info!("Waiting for guest {} to exit...", removed_guest_id); let (guest, guest_project, mut guest_cx, guest_err) = guest.await; deterministic.finish_waiting(); server.allow_connections(); @@ -4945,6 +4949,7 @@ impl TestServer { Arc::get_mut(&mut client) .unwrap() + .set_id(user_id.0 as usize) .override_authenticate(move |cx| { cx.spawn(|_| async move { let access_token = "the-token".to_string(); From b81135e10b2cd9470789c825d768bffe3be13e46 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Jun 2022 15:07:40 -0700 Subject: [PATCH 34/60] Stop waiting for snapshot updates when disconnected from host --- crates/project/src/worktree.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 5eb3c3dbd6..8dee1c8261 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -970,6 +970,7 @@ impl RemoteWorktree { pub fn disconnected_from_host(&mut self) { self.updates_tx.take(); + self.snapshot_subscriptions.clear(); } pub fn update_from_remote(&mut self, update: proto::UpdateWorktree) { From 5fdbc38f460ba983e6edbd02d2d5c06f55a47d34 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Jun 2022 15:46:31 -0700 Subject: [PATCH 35/60] Don't update worktrees' snapshots in the middle of processing fs events --- crates/project/src/worktree.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 8dee1c8261..e6af57bf5c 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -125,7 +125,10 @@ impl DerefMut for LocalSnapshot { #[derive(Clone, Debug)] enum ScanState { Idle, - Scanning, + /// The worktree is performing its initial scan of the filesystem. + Initializing, + /// The worktree is updating in response to filesystem events. + Updating, Err(Arc), } @@ -334,7 +337,7 @@ impl Worktree { Self::Local(worktree) => { let is_fake_fs = worktree.fs.is_fake(); worktree.snapshot = worktree.background_snapshot.lock().clone(); - if worktree.is_scanning() { + if matches!(worktree.scan_state(), ScanState::Initializing) { if worktree.poll_task.is_none() { worktree.poll_task = Some(cx.spawn_weak(|this, mut cx| async move { if is_fake_fs { @@ -390,7 +393,8 @@ impl LocalWorktree { .context("failed to stat worktree path")?; let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded(); - let (mut last_scan_state_tx, last_scan_state_rx) = watch::channel_with(ScanState::Scanning); + let (mut last_scan_state_tx, last_scan_state_rx) = + watch::channel_with(ScanState::Initializing); let tree = cx.add_model(move |cx: &mut ModelContext| { let mut snapshot = LocalSnapshot { abs_path, @@ -527,18 +531,14 @@ impl LocalWorktree { let mut scan_state_rx = self.last_scan_state_rx.clone(); async move { let mut scan_state = Some(scan_state_rx.borrow().clone()); - while let Some(ScanState::Scanning) = scan_state { + while let Some(ScanState::Initializing | ScanState::Updating) = scan_state { scan_state = scan_state_rx.recv().await; } } } - fn is_scanning(&self) -> bool { - if let ScanState::Scanning = *self.last_scan_state_rx.borrow() { - true - } else { - false - } + fn scan_state(&self) -> ScanState { + self.last_scan_state_rx.borrow().clone() } pub fn snapshot(&self) -> LocalSnapshot { @@ -947,7 +947,7 @@ impl LocalWorktree { fn broadcast_snapshot(&self) -> impl Future { let mut to_send = None; - if !self.is_scanning() { + if matches!(self.scan_state(), ScanState::Idle) { if let Some(share) = self.share.as_ref() { to_send = Some((self.snapshot(), share.snapshots_tx.clone())); } @@ -1975,7 +1975,7 @@ impl BackgroundScanner { } async fn run(mut self, events_rx: impl Stream>) { - if self.notify.unbounded_send(ScanState::Scanning).is_err() { + if self.notify.unbounded_send(ScanState::Initializing).is_err() { return; } @@ -2000,7 +2000,7 @@ impl BackgroundScanner { events.extend(additional_events); } - if self.notify.unbounded_send(ScanState::Scanning).is_err() { + if self.notify.unbounded_send(ScanState::Updating).is_err() { break; } From db05e3238926fa7903fdea7cdec6001a64cd38c6 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 30 Jun 2022 16:46:26 -0700 Subject: [PATCH 36/60] Prevent creating extra language server instances if there already exists one for that workspace --- crates/lsp/src/lsp.rs | 25 +- crates/project/src/lsp_command.rs | 14 +- crates/project/src/project.rs | 658 +++++++++++++++++------------- 3 files changed, 391 insertions(+), 306 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 0bef424104..682d8f1823 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -11,7 +11,7 @@ use serde_json::{json, value::RawValue, Value}; use smol::{ channel, io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, - process, + process::{self, Child}, }; use std::{ future::Future, @@ -44,6 +44,7 @@ pub struct LanguageServer { io_tasks: Mutex>, Task>)>>, output_done_rx: Mutex>, root_path: PathBuf, + _server: Option, } pub struct Subscription { @@ -118,11 +119,20 @@ impl LanguageServer { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) + .kill_on_drop(true) .spawn()?; + let stdin = server.stdin.take().unwrap(); - let stdout = server.stdout.take().unwrap(); - let mut server = - Self::new_internal(server_id, stdin, stdout, root_path, cx, |notification| { + let stout = server.stdout.take().unwrap(); + + let mut server = Self::new_internal( + server_id, + stdin, + stout, + Some(server), + root_path, + cx, + |notification| { log::info!( "unhandled notification {}:\n{}", notification.method, @@ -131,7 +141,8 @@ impl LanguageServer { ) .unwrap() ); - }); + }, + ); if let Some(name) = binary_path.file_name() { server.name = name.to_string_lossy().to_string(); } @@ -142,6 +153,7 @@ impl LanguageServer { server_id: usize, stdin: Stdin, stdout: Stdout, + server: Option, root_path: &Path, cx: AsyncAppContext, mut on_unhandled_notification: F, @@ -242,6 +254,7 @@ impl LanguageServer { io_tasks: Mutex::new(Some((input_task, output_task))), output_done_rx: Mutex::new(Some(output_done_rx)), root_path: root_path.to_path_buf(), + _server: server, } } @@ -608,6 +621,7 @@ impl LanguageServer { 0, stdin_writer, stdout_reader, + None, Path::new("/"), cx.clone(), |_| {}, @@ -617,6 +631,7 @@ impl LanguageServer { 0, stdout_writer, stdin_reader, + None, Path::new("/"), cx.clone(), move |msg| { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index ee2bf37aa1..5897b5fcb6 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -242,7 +242,7 @@ impl LspCommand for PerformRename { .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) - .cloned() + .map(|(adapter, server)| (adapter.clone(), server.clone())) }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; Project::deserialize_workspace_edit( @@ -359,7 +359,7 @@ impl LspCommand for GetDefinition { .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) - .cloned() + .map(|(adapter, server)| (adapter.clone(), server.clone())) }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; @@ -388,8 +388,8 @@ impl LspCommand for GetDefinition { .update(&mut cx, |this, cx| { this.open_local_buffer_via_lsp( target_uri, - lsp_adapter.clone(), - language_server.clone(), + language_server.server_id(), + lsp_adapter.name(), cx, ) }) @@ -599,7 +599,7 @@ impl LspCommand for GetReferences { .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) - .cloned() + .map(|(adapter, server)| (adapter.clone(), server.clone())) }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; @@ -609,8 +609,8 @@ impl LspCommand for GetReferences { .update(&mut cx, |this, cx| { this.open_local_buffer_via_lsp( lsp_location.uri, - lsp_adapter.clone(), - language_server.clone(), + language_server.server_id(), + lsp_adapter.name(), cx, ) }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8ce07a6abd..828ffc3c05 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -65,6 +65,14 @@ pub trait Item: Entity { fn entry_id(&self, cx: &AppContext) -> Option; } +pub enum LanguageServerState { + Starting(Task>>), + Running { + adapter: Arc, + server: Arc, + }, +} + pub struct ProjectStore { db: Arc, projects: Vec>, @@ -74,10 +82,8 @@ pub struct Project { worktrees: Vec, active_entry: Option, languages: Arc, - language_servers: - HashMap<(WorktreeId, LanguageServerName), (Arc, Arc)>, - started_language_servers: - HashMap<(WorktreeId, LanguageServerName), Task>>>, + language_servers: HashMap, + language_server_ids: HashMap<(WorktreeId, LanguageServerName), usize>, language_server_statuses: BTreeMap, language_server_settings: Arc>, last_workspace_edits_by_language_server: HashMap, @@ -179,7 +185,7 @@ pub struct LanguageServerStatus { pub name: String, pub pending_work: BTreeMap, pub has_pending_diagnostic_updates: bool, - progress_tokens: HashSet, + pub progress_tokens: HashSet, } #[derive(Clone, Debug, Serialize)] @@ -437,7 +443,7 @@ impl Project { next_entry_id: Default::default(), next_diagnostic_group_id: Default::default(), language_servers: Default::default(), - started_language_servers: Default::default(), + language_server_ids: Default::default(), language_server_statuses: Default::default(), last_workspace_edits_by_language_server: Default::default(), language_server_settings: Default::default(), @@ -536,7 +542,7 @@ impl Project { }), }, language_servers: Default::default(), - started_language_servers: Default::default(), + language_server_ids: Default::default(), language_server_settings: Default::default(), language_server_statuses: response .language_servers @@ -691,7 +697,7 @@ impl Project { if let Some(lsp_adapter) = language.lsp_adapter() { if !settings.enable_language_server(Some(&language.name())) { let lsp_name = lsp_adapter.name(); - for (worktree_id, started_lsp_name) in self.started_language_servers.keys() { + for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { if lsp_name == *started_lsp_name { language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); } @@ -1538,8 +1544,8 @@ impl Project { fn open_local_buffer_via_lsp( &mut self, abs_path: lsp::Url, - lsp_adapter: Arc, - lsp_server: Arc, + language_server_id: usize, + language_server_name: LanguageServerName, cx: &mut ModelContext, ) -> Task>> { cx.spawn(|this, mut cx| async move { @@ -1557,9 +1563,9 @@ impl Project { }) .await?; this.update(&mut cx, |this, cx| { - this.language_servers.insert( - (worktree.read(cx).id(), lsp_adapter.name()), - (lsp_adapter, lsp_server), + this.language_server_ids.insert( + (worktree.read(cx).id(), language_server_name), + language_server_id, ); }); (worktree, PathBuf::new()) @@ -1726,9 +1732,16 @@ impl Project { if let Some(adapter) = language.lsp_adapter() { language_id = adapter.id_for_language(language.name().as_ref()); language_server = self - .language_servers + .language_server_ids .get(&(worktree_id, adapter.name())) - .cloned(); + .and_then(|id| self.language_servers.get(&id)) + .and_then(|server_state| { + if let LanguageServerState::Running { server, .. } = server_state { + Some(server.clone()) + } else { + None + } + }); } } @@ -1739,7 +1752,7 @@ impl Project { } } - if let Some((_, server)) = language_server { + if let Some(server) = language_server { server .notify::( lsp::DidOpenTextDocumentParams { @@ -1816,9 +1829,9 @@ impl Project { } } BufferEvent::Edited { .. } => { - let (_, language_server) = self - .language_server_for_buffer(buffer.read(cx), cx)? - .clone(); + let language_server = self + .language_server_for_buffer(buffer.read(cx), cx) + .map(|(_, server)| server.clone())?; let buffer = buffer.read(cx); let file = File::from_dyn(buffer.file())?; let abs_path = file.as_local()?.abs_path(cx); @@ -1907,16 +1920,19 @@ impl Project { fn language_servers_for_worktree( &self, worktree_id: WorktreeId, - ) -> impl Iterator, Arc)> { - self.language_servers.iter().filter_map( - move |((language_server_worktree_id, _), server)| { + ) -> impl Iterator, &Arc)> { + self.language_server_ids + .iter() + .filter_map(move |((language_server_worktree_id, _), id)| { if *language_server_worktree_id == worktree_id { - Some(server) - } else { - None + if let Some(LanguageServerState::Running { adapter, server }) = + self.language_servers.get(&id) + { + return Some((adapter, server)); + } } - }, - ) + None + }) } fn assign_language_to_buffer( @@ -1960,7 +1976,8 @@ impl Project { return; }; let key = (worktree_id, adapter.name()); - self.started_language_servers + + self.language_server_ids .entry(key.clone()) .or_insert_with(|| { let server_id = post_inc(&mut self.next_language_server_id); @@ -1971,218 +1988,240 @@ impl Project { self.client.http_client(), cx, ); - cx.spawn_weak(|this, mut cx| async move { - let language_server = language_server?.await.log_err()?; - let language_server = language_server - .initialize(adapter.initialization_options()) - .await - .log_err()?; - let this = this.upgrade(&cx)?; - let disk_based_diagnostics_progress_token = - adapter.disk_based_diagnostics_progress_token(); + self.language_servers.insert( + server_id, + LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move { + let language_server = language_server?.await.log_err()?; + let language_server = language_server + .initialize(adapter.initialization_options()) + .await + .log_err()?; + let this = this.upgrade(&cx)?; + let disk_based_diagnostics_progress_token = + adapter.disk_based_diagnostics_progress_token(); - language_server - .on_notification::({ - let this = this.downgrade(); - let adapter = adapter.clone(); - move |params, mut cx| { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.on_lsp_diagnostics_published( - server_id, params, &adapter, cx, - ); - }); - } - } - }) - .detach(); - - language_server - .on_request::({ - let settings = this - .read_with(&cx, |this, _| this.language_server_settings.clone()); - move |params, _| { - let settings = settings.lock().clone(); - async move { - Ok(params - .items - .into_iter() - .map(|item| { - if let Some(section) = &item.section { - settings - .get(section) - .cloned() - .unwrap_or(serde_json::Value::Null) - } else { - settings.clone() - } - }) - .collect()) - } - } - }) - .detach(); - - // Even though we don't have handling for these requests, respond to them to - // avoid stalling any language server like `gopls` which waits for a response - // to these requests when initializing. - language_server - .on_request::({ - let this = this.downgrade(); - move |params, mut cx| async move { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, _| { - if let Some(status) = - this.language_server_statuses.get_mut(&server_id) - { - if let lsp::NumberOrString::String(token) = params.token - { - status.progress_tokens.insert(token); - } - } - }); - } - Ok(()) - } - }) - .detach(); - language_server - .on_request::(|_, _| async { - Ok(()) - }) - .detach(); - - language_server - .on_request::({ - let this = this.downgrade(); - let adapter = adapter.clone(); - let language_server = language_server.clone(); - move |params, cx| { - Self::on_lsp_workspace_edit( - this, - params, - server_id, - adapter.clone(), - language_server.clone(), - cx, - ) - } - }) - .detach(); - - language_server - .on_notification::({ - let this = this.downgrade(); - move |params, mut cx| { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.on_lsp_progress( - params, - server_id, - disk_based_diagnostics_progress_token, - cx, - ); - }); - } - } - }) - .detach(); - - this.update(&mut cx, |this, cx| { - this.language_servers - .insert(key.clone(), (adapter.clone(), language_server.clone())); - this.language_server_statuses.insert( - server_id, - LanguageServerStatus { - name: language_server.name().to_string(), - pending_work: Default::default(), - has_pending_diagnostic_updates: false, - progress_tokens: Default::default(), - }, - ); language_server - .notify::( - lsp::DidChangeConfigurationParams { - settings: this.language_server_settings.lock().clone(), - }, - ) - .ok(); - - if let Some(project_id) = this.shared_remote_id() { - this.client - .send(proto::StartLanguageServer { - project_id, - server: Some(proto::LanguageServer { - id: server_id as u64, - name: language_server.name().to_string(), - }), - }) - .log_err(); - } - - // Tell the language server about every open buffer in the worktree that matches the language. - for buffer in this.opened_buffers.values() { - if let Some(buffer_handle) = buffer.upgrade(cx) { - let buffer = buffer_handle.read(cx); - let file = if let Some(file) = File::from_dyn(buffer.file()) { - file - } else { - continue; - }; - let language = if let Some(language) = buffer.language() { - language - } else { - continue; - }; - if file.worktree.read(cx).id() != key.0 - || language.lsp_adapter().map(|a| a.name()) - != Some(key.1.clone()) - { - continue; + .on_notification::({ + let this = this.downgrade(); + let adapter = adapter.clone(); + move |params, mut cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.on_lsp_diagnostics_published( + server_id, params, &adapter, cx, + ); + }); + } } + }) + .detach(); - let file = file.as_local()?; - let versions = this - .buffer_snapshots - .entry(buffer.remote_id()) - .or_insert_with(|| vec![(0, buffer.text_snapshot())]); - let (version, initial_snapshot) = versions.last().unwrap(); - let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); - let language_id = adapter.id_for_language(language.name().as_ref()); - language_server - .notify::( - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - uri, - language_id.unwrap_or_default(), - *version, - initial_snapshot.text(), - ), - }, - ) - .log_err()?; - buffer_handle.update(cx, |buffer, cx| { - buffer.set_completion_triggers( - language_server - .capabilities() - .completion_provider - .as_ref() - .and_then(|provider| { - provider.trigger_characters.clone() + language_server + .on_request::({ + let settings = this.read_with(&cx, |this, _| { + this.language_server_settings.clone() + }); + move |params, _| { + let settings = settings.lock().clone(); + async move { + Ok(params + .items + .into_iter() + .map(|item| { + if let Some(section) = &item.section { + settings + .get(section) + .cloned() + .unwrap_or(serde_json::Value::Null) + } else { + settings.clone() + } }) - .unwrap_or(Vec::new()), + .collect()) + } + } + }) + .detach(); + + // Even though we don't have handling for these requests, respond to them to + // avoid stalling any language server like `gopls` which waits for a response + // to these requests when initializing. + language_server + .on_request::({ + let this = this.downgrade(); + move |params, mut cx| async move { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, _| { + if let Some(status) = + this.language_server_statuses.get_mut(&server_id) + { + if let lsp::NumberOrString::String(token) = + params.token + { + status.progress_tokens.insert(token); + } + } + }); + } + Ok(()) + } + }) + .detach(); + language_server + .on_request::(|_, _| async { + Ok(()) + }) + .detach(); + + language_server + .on_request::({ + let this = this.downgrade(); + let adapter = adapter.clone(); + let language_server = language_server.clone(); + move |params, cx| { + Self::on_lsp_workspace_edit( + this, + params, + server_id, + adapter.clone(), + language_server.clone(), cx, ) - }); + } + }) + .detach(); + + language_server + .on_notification::({ + let this = this.downgrade(); + move |params, mut cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.on_lsp_progress( + params, + server_id, + disk_based_diagnostics_progress_token, + cx, + ); + }); + } + } + }) + .detach(); + + this.update(&mut cx, |this, cx| { + // If the language server for this key doesn't match the server id, don't store the + // server. + if this + .language_server_ids + .get(&key) + .map(|id| id != &server_id) + .unwrap_or(false) + { + return None; } - } - cx.notify(); - Some(()) - }); + this.language_servers.insert( + server_id, + LanguageServerState::Running { + adapter: adapter.clone(), + server: language_server.clone(), + }, + ); + this.language_server_statuses.insert( + server_id, + LanguageServerStatus { + name: language_server.name().to_string(), + pending_work: Default::default(), + has_pending_diagnostic_updates: false, + progress_tokens: Default::default(), + }, + ); + language_server + .notify::( + lsp::DidChangeConfigurationParams { + settings: this.language_server_settings.lock().clone(), + }, + ) + .ok(); - Some(language_server) - }) + if let Some(project_id) = this.shared_remote_id() { + this.client + .send(proto::StartLanguageServer { + project_id, + server: Some(proto::LanguageServer { + id: server_id as u64, + name: language_server.name().to_string(), + }), + }) + .log_err(); + } + + // Tell the language server about every open buffer in the worktree that matches the language. + for buffer in this.opened_buffers.values() { + if let Some(buffer_handle) = buffer.upgrade(cx) { + let buffer = buffer_handle.read(cx); + let file = if let Some(file) = File::from_dyn(buffer.file()) { + file + } else { + continue; + }; + let language = if let Some(language) = buffer.language() { + language + } else { + continue; + }; + if file.worktree.read(cx).id() != key.0 + || language.lsp_adapter().map(|a| a.name()) + != Some(key.1.clone()) + { + continue; + } + + let file = file.as_local()?; + let versions = this + .buffer_snapshots + .entry(buffer.remote_id()) + .or_insert_with(|| vec![(0, buffer.text_snapshot())]); + let (version, initial_snapshot) = versions.last().unwrap(); + let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + let language_id = + adapter.id_for_language(language.name().as_ref()); + language_server + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + uri, + language_id.unwrap_or_default(), + *version, + initial_snapshot.text(), + ), + }, + ) + .log_err()?; + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + language_server + .capabilities() + .completion_provider + .as_ref() + .and_then(|provider| { + provider.trigger_characters.clone() + }) + .unwrap_or(Vec::new()), + cx, + ) + }); + } + } + + cx.notify(); + Some(language_server) + }) + })), + ); + + server_id }); } @@ -2193,26 +2232,28 @@ impl Project { cx: &mut ModelContext, ) -> Task<()> { let key = (worktree_id, adapter_name); - if let Some((_, language_server)) = self.language_servers.remove(&key) { - self.language_server_statuses - .remove(&language_server.server_id()); - cx.notify(); - } - - if let Some(started_language_server) = self.started_language_servers.remove(&key) { + if let Some(server_id) = self.language_server_ids.remove(&key) { + let server_state = self.language_servers.remove(&server_id); cx.spawn_weak(|this, mut cx| async move { - if let Some(language_server) = started_language_server.await { - if let Some(shutdown) = language_server.shutdown() { + let server = match server_state { + Some(LanguageServerState::Starting(started_language_server)) => { + started_language_server.await + } + Some(LanguageServerState::Running { server, .. }) => Some(server), + None => None, + }; + + if let Some(server) = server { + if let Some(shutdown) = server.shutdown() { shutdown.await; } + } - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.language_server_statuses - .remove(&language_server.server_id()); - cx.notify(); - }); - } + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.language_server_statuses.remove(&server_id); + cx.notify(); + }); } }) } else { @@ -2498,14 +2539,16 @@ impl Project { } pub fn set_language_server_settings(&mut self, settings: serde_json::Value) { - for (_, server) in self.language_servers.values() { - server - .notify::( - lsp::DidChangeConfigurationParams { - settings: settings.clone(), - }, - ) - .ok(); + for server_state in self.language_servers.values() { + if let LanguageServerState::Running { server, .. } = server_state { + server + .notify::( + lsp::DidChangeConfigurationParams { + settings: settings.clone(), + }, + ) + .ok(); + } } *self.language_server_settings.lock() = settings; } @@ -2968,30 +3011,36 @@ impl Project { pub fn symbols(&self, query: &str, cx: &mut ModelContext) -> Task>> { if self.is_local() { let mut requests = Vec::new(); - for ((worktree_id, _), (lsp_adapter, language_server)) in self.language_servers.iter() { + for ((worktree_id, _), server_id) in self.language_server_ids.iter() { let worktree_id = *worktree_id; if let Some(worktree) = self .worktree_for_id(worktree_id, cx) .and_then(|worktree| worktree.read(cx).as_local()) { - let lsp_adapter = lsp_adapter.clone(); - let worktree_abs_path = worktree.abs_path().clone(); - requests.push( - language_server - .request::(lsp::WorkspaceSymbolParams { - query: query.to_string(), - ..Default::default() - }) - .log_err() - .map(move |response| { - ( - lsp_adapter, - worktree_id, - worktree_abs_path, - response.unwrap_or_default(), + if let Some(LanguageServerState::Running { adapter, server }) = + self.language_servers.get(server_id) + { + let adapter = adapter.clone(); + let worktree_abs_path = worktree.abs_path().clone(); + requests.push( + server + .request::( + lsp::WorkspaceSymbolParams { + query: query.to_string(), + ..Default::default() + }, ) - }), - ); + .log_err() + .map(move |response| { + ( + adapter, + worktree_id, + worktree_abs_path, + response.unwrap_or_default(), + ) + }), + ); + } } } @@ -3074,11 +3123,11 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { if self.is_local() { - let (lsp_adapter, language_server) = if let Some(server) = self.language_servers.get(&( + let language_server_id = if let Some(id) = self.language_server_ids.get(&( symbol.source_worktree_id, symbol.language_server_name.clone(), )) { - server.clone() + *id } else { return Task::ready(Err(anyhow!( "language server for worktree and language not found" @@ -3101,7 +3150,12 @@ impl Project { return Task::ready(Err(anyhow!("invalid symbol path"))); }; - self.open_local_buffer_via_lsp(symbol_uri, lsp_adapter, language_server, cx) + self.open_local_buffer_via_lsp( + symbol_uri, + language_server_id, + symbol.language_server_name.clone(), + cx, + ) } else if let Some(project_id) = self.remote_id() { let request = self.client.request(proto::OpenBufferForSymbol { project_id, @@ -3152,8 +3206,8 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); - let (_, lang_server) = - if let Some(server) = self.language_server_for_buffer(source_buffer, cx) { + let lang_server = + if let Some((_, server)) = self.language_server_for_buffer(source_buffer, cx) { server.clone() } else { return Task::ready(Ok(Default::default())); @@ -3310,7 +3364,7 @@ impl Project { let buffer_id = buffer.remote_id(); if self.is_local() { - let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(buffer, cx) + let lang_server = if let Some((_, server)) = self.language_server_for_buffer(buffer, cx) { server.clone() } else { @@ -3407,7 +3461,7 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); - let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(buffer, cx) + let lang_server = if let Some((_, server)) = self.language_server_for_buffer(buffer, cx) { server.clone() } else { @@ -3494,8 +3548,8 @@ impl Project { if self.is_local() { let buffer = buffer_handle.read(cx); let (lsp_adapter, lang_server) = - if let Some(server) = self.language_server_for_buffer(buffer, cx) { - server.clone() + if let Some((adapter, server)) = self.language_server_for_buffer(buffer, cx) { + (adapter.clone(), server.clone()) } else { return Task::ready(Ok(Default::default())); }; @@ -3531,8 +3585,8 @@ impl Project { this, edit, push_to_history, - lsp_adapter, - lang_server, + lsp_adapter.clone(), + lang_server.clone(), &mut cx, ) .await @@ -3661,8 +3715,8 @@ impl Project { .update(cx, |this, cx| { this.open_local_buffer_via_lsp( op.text_document.uri, - lsp_adapter.clone(), - language_server.clone(), + language_server.server_id(), + lsp_adapter.name(), cx, ) }) @@ -3956,9 +4010,10 @@ impl Project { let buffer = buffer_handle.read(cx); if self.is_local() { let file = File::from_dyn(buffer.file()).and_then(File::as_local); - if let Some((file, (_, language_server))) = - file.zip(self.language_server_for_buffer(buffer, cx).cloned()) - { + if let Some((file, language_server)) = file.zip( + self.language_server_for_buffer(buffer, cx) + .map(|(_, server)| server.clone()), + ) { let lsp_params = request.to_lsp(&file.abs_path(cx), cx); return cx.spawn(|this, cx| async move { if !request.check_capabilities(&language_server.capabilities()) { @@ -5574,14 +5629,21 @@ impl Project { &self, buffer: &Buffer, cx: &AppContext, - ) -> Option<&(Arc, Arc)> { + ) -> Option<(&Arc, &Arc)> { if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { let worktree_id = file.worktree_id(cx); - self.language_servers - .get(&(worktree_id, language.lsp_adapter()?.name())) - } else { - None + let key = (worktree_id, language.lsp_adapter()?.name()); + + if let Some(server_id) = self.language_server_ids.get(&key) { + if let Some(LanguageServerState::Running { adapter, server }) = + self.language_servers.get(&server_id) + { + return Some((adapter, server)); + } + } } + + None } } @@ -5741,8 +5803,16 @@ impl Entity for Project { let shutdown_futures = self .language_servers .drain() - .filter_map(|(_, (_, server))| server.shutdown()) + .filter_map(|(_, server_state)| { + // TODO: Handle starting servers? + if let LanguageServerState::Running { server, .. } = server_state { + server.shutdown() + } else { + None + } + }) .collect::>(); + Some( async move { futures::future::join_all(shutdown_futures).await; From 5e7651e92e4fbd839bf3d39d456ebabe3588f560 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 30 Jun 2022 17:00:29 -0700 Subject: [PATCH 37/60] Kill starting servers as well as currently running ones --- crates/project/src/project.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 828ffc3c05..ef58e51c54 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5803,12 +5803,12 @@ impl Entity for Project { let shutdown_futures = self .language_servers .drain() - .filter_map(|(_, server_state)| { - // TODO: Handle starting servers? - if let LanguageServerState::Running { server, .. } = server_state { - server.shutdown() - } else { - None + .map(|(_, server_state)| async { + match server_state { + LanguageServerState::Running { server, .. } => server.shutdown()?.await, + LanguageServerState::Starting(starting_server) => { + starting_server.await?.shutdown()?.await + } } }) .collect::>(); From f495185a4efdc13e37de9b7e32e9e34c57b3d649 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 30 Jun 2022 17:09:23 -0700 Subject: [PATCH 38/60] add a comment explaining the three language server collections on project --- crates/project/src/project.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ef58e51c54..34f27c83fa 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -65,19 +65,25 @@ pub trait Item: Entity { fn entry_id(&self, cx: &AppContext) -> Option; } -pub enum LanguageServerState { - Starting(Task>>), - Running { - adapter: Arc, - server: Arc, - }, -} - pub struct ProjectStore { db: Arc, projects: Vec>, } +// Language server state is stored across 3 collections: +// language_servers => +// a mapping from unique server id to LanguageServerState which can either be a task for a +// server in the process of starting, or a running server with adapter and language server arcs +// language_server_ids => a mapping from worktreeId and server name to the unique server id +// language_server_statuses => a mapping from unique server id to the current server status +// +// Multiple worktrees can map to the same language server for example when you jump to the definition +// of a file in the standard library. So language_server_ids is used to look up which server is active +// for a given worktree and language server name +// +// When starting a language server, first the id map is checked to make sure a server isn't already available +// for that worktree. If there is one, it finishes early. Otherwise, a new id is allocated and and +// the Starting variant of LanguageServerState is stored in the language_servers map. pub struct Project { worktrees: Vec, active_entry: Option, @@ -180,12 +186,20 @@ pub enum Event { ContactCancelledJoinRequest(Arc), } +pub enum LanguageServerState { + Starting(Task>>), + Running { + adapter: Arc, + server: Arc, + }, +} + #[derive(Serialize)] pub struct LanguageServerStatus { pub name: String, pub pending_work: BTreeMap, pub has_pending_diagnostic_updates: bool, - pub progress_tokens: HashSet, + progress_tokens: HashSet, } #[derive(Clone, Debug, Serialize)] From 37b75132b727a97f6fec0d8a00b0ad43a33456b3 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 30 Jun 2022 17:18:28 -0700 Subject: [PATCH 39/60] Minor comment change --- crates/project/src/project.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 34f27c83fa..3fc90d87bc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2125,7 +2125,7 @@ impl Project { this.update(&mut cx, |this, cx| { // If the language server for this key doesn't match the server id, don't store the - // server. + // server. Which will cause it to be dropped, killing the process if this .language_server_ids .get(&key) @@ -2135,6 +2135,8 @@ impl Project { return None; } + // Update language_servers collection with Running variant of LanguageServerState + // indicating that the server is up and running and ready this.language_servers.insert( server_id, LanguageServerState::Running { From 38ca4aab311be49ebe0bec4d032ae0f08ab726ad Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 30 Jun 2022 17:24:32 -0700 Subject: [PATCH 40/60] add assertion to test_definition ensuring no new language servers are created --- crates/project/src/project.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3fc90d87bc..adb9088ba6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -7743,6 +7743,10 @@ mod tests { .await .unwrap(); + // Assert no new language server started + cx.foreground().run_until_parked(); + assert!(fake_servers.try_next().is_err()); + assert_eq!(definitions.len(), 1); let definition = definitions.pop().unwrap(); cx.update(|cx| { From 6b50dda28ad4b7dcd50c6c3fc43003486c13da0e Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 30 Jun 2022 17:40:50 -0700 Subject: [PATCH 41/60] Fix failing test due to change in stop_language_server function --- crates/project/src/project.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index adb9088ba6..4f5c3b613c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2249,6 +2249,9 @@ impl Project { ) -> Task<()> { let key = (worktree_id, adapter_name); if let Some(server_id) = self.language_server_ids.remove(&key) { + self.language_server_statuses.remove(&server_id); + cx.notify(); + let server_state = self.language_servers.remove(&server_id); cx.spawn_weak(|this, mut cx| async move { let server = match server_state { From 832cc3dd19e2a08d8c1c987f536dc4c76b58912c Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 30 Jun 2022 17:50:30 -0700 Subject: [PATCH 42/60] Clear language server id for all worktrees when stopping a language server --- crates/project/src/project.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4f5c3b613c..d0ddaa1fab 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2249,6 +2249,10 @@ impl Project { ) -> Task<()> { let key = (worktree_id, adapter_name); if let Some(server_id) = self.language_server_ids.remove(&key) { + // Remove other entries for this language server + self.language_server_ids + .retain(|_, other_id| other_id != &server_id); + self.language_server_statuses.remove(&server_id); cx.notify(); From 8a105bf12fd1589ac4ec14189cb932a69522cc57 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Jun 2022 18:04:31 -0700 Subject: [PATCH 43/60] WIP - try representing snapshots_to_send as a watch --- crates/project/src/worktree.rs | 196 ++++++++++++++++----------------- 1 file changed, 93 insertions(+), 103 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index e6af57bf5c..af7cf02cb8 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -134,7 +134,7 @@ enum ScanState { struct ShareState { project_id: u64, - snapshots_tx: Sender, + snapshots_tx: watch::Sender, _maintain_remote_snapshot: Option>>, } @@ -334,38 +334,9 @@ impl Worktree { fn poll_snapshot(&mut self, cx: &mut ModelContext) { match self { - Self::Local(worktree) => { - let is_fake_fs = worktree.fs.is_fake(); - worktree.snapshot = worktree.background_snapshot.lock().clone(); - if matches!(worktree.scan_state(), ScanState::Initializing) { - if worktree.poll_task.is_none() { - worktree.poll_task = Some(cx.spawn_weak(|this, mut cx| async move { - if is_fake_fs { - #[cfg(any(test, feature = "test-support"))] - cx.background().simulate_random_delay().await; - } else { - smol::Timer::after(Duration::from_millis(100)).await; - } - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.as_local_mut().unwrap().poll_task = None; - this.poll_snapshot(cx); - }); - } - })); - } - } else { - worktree.poll_task.take(); - cx.emit(Event::UpdatedEntries); - } - } - Self::Remote(worktree) => { - worktree.snapshot = worktree.background_snapshot.lock().clone(); - cx.emit(Event::UpdatedEntries); - } + Self::Local(worktree) => worktree.poll_snapshot(cx), + Self::Remote(worktree) => worktree.poll_snapshot(cx), }; - - cx.notify(); } } @@ -441,9 +412,8 @@ impl LocalWorktree { last_scan_state_tx.blocking_send(scan_state).ok(); this.update(&mut cx, |this, cx| { this.poll_snapshot(cx); - this.as_local().unwrap().broadcast_snapshot() - }) - .await; + this.as_local_mut().unwrap().broadcast_snapshot() + }); } else { break; } @@ -527,6 +497,40 @@ impl LocalWorktree { Ok(updated) } + fn poll_snapshot(&mut self, cx: &mut ModelContext) { + match self.scan_state() { + ScanState::Idle => { + self.snapshot = self.background_snapshot.lock().clone(); + self.poll_task.take(); + cx.emit(Event::UpdatedEntries); + } + ScanState::Initializing => { + self.snapshot = self.background_snapshot.lock().clone(); + if self.poll_task.is_none() { + let is_fake_fs = self.fs.is_fake(); + self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move { + if is_fake_fs { + #[cfg(any(test, feature = "test-support"))] + cx.background().simulate_random_delay().await; + } else { + smol::Timer::after(Duration::from_millis(100)).await; + } + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.as_local_mut().unwrap().poll_task = None; + this.poll_snapshot(cx); + }); + } + })); + } + cx.emit(Event::UpdatedEntries); + } + ScanState::Updating => {} + ScanState::Err(_) => {} + } + cx.notify(); + } + pub fn scan_complete(&self) -> impl Future { let mut scan_state_rx = self.last_scan_state_rx.clone(); async move { @@ -666,16 +670,15 @@ impl LocalWorktree { Some(cx.spawn(|this, mut cx| async move { delete.await?; - this.update(&mut cx, |this, _| { - let this = this.as_local_mut().unwrap(); - let mut snapshot = this.background_snapshot.lock(); - snapshot.delete_entry(entry_id); - }); this.update(&mut cx, |this, cx| { + let this = this.as_local_mut().unwrap(); + { + let mut snapshot = this.background_snapshot.lock(); + snapshot.delete_entry(entry_id); + } this.poll_snapshot(cx); - this.as_local().unwrap().broadcast_snapshot() - }) - .await; + this.broadcast_snapshot(); + }); Ok(()) })) } @@ -712,10 +715,10 @@ impl LocalWorktree { }) .await?; this.update(&mut cx, |this, cx| { + let this = this.as_local_mut().unwrap(); this.poll_snapshot(cx); - this.as_local().unwrap().broadcast_snapshot() - }) - .await; + this.broadcast_snapshot(); + }); Ok(entry) })) } @@ -752,10 +755,10 @@ impl LocalWorktree { }) .await?; this.update(&mut cx, |this, cx| { + let this = this.as_local_mut().unwrap(); this.poll_snapshot(cx); - this.as_local().unwrap().broadcast_snapshot() - }) - .await; + this.broadcast_snapshot() + }); Ok(entry) })) } @@ -790,10 +793,10 @@ impl LocalWorktree { }) .await?; this.update(&mut cx, |this, cx| { + let this = this.as_local_mut().unwrap(); this.poll_snapshot(cx); - this.as_local().unwrap().broadcast_snapshot() - }) - .await; + this.broadcast_snapshot(); + }); Ok(entry) }) } @@ -826,45 +829,42 @@ impl LocalWorktree { let this = this .upgrade(&cx) .ok_or_else(|| anyhow!("worktree was dropped"))?; - let (entry, snapshot, snapshots_tx) = this.read_with(&cx, |this, _| { - let this = this.as_local().unwrap(); - let mut snapshot = this.background_snapshot.lock(); - entry.is_ignored = snapshot - .ignore_stack_for_path(&path, entry.is_dir()) - .is_path_ignored(&path, entry.is_dir()); - if let Some(old_path) = old_path { - snapshot.remove_path(&old_path); + this.update(&mut cx, |this, cx| { + let this = this.as_local_mut().unwrap(); + let inserted_entry; + { + let mut snapshot = this.background_snapshot.lock(); + entry.is_ignored = snapshot + .ignore_stack_for_path(&path, entry.is_dir()) + .is_path_ignored(&path, entry.is_dir()); + if let Some(old_path) = old_path { + snapshot.remove_path(&old_path); + } + inserted_entry = snapshot.insert_entry(entry, fs.as_ref()); + snapshot.scan_id += 1; } - let entry = snapshot.insert_entry(entry, fs.as_ref()); - snapshot.scan_id += 1; - let snapshots_tx = this.share.as_ref().map(|s| s.snapshots_tx.clone()); - (entry, snapshot.clone(), snapshots_tx) - }); - this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); - - if let Some(snapshots_tx) = snapshots_tx { - snapshots_tx.send(snapshot).await.ok(); - } - - Ok(entry) + this.poll_snapshot(cx); + this.broadcast_snapshot(); + Ok(inserted_entry) + }) }) } pub fn share(&mut self, project_id: u64, cx: &mut ModelContext) -> Task> { let (share_tx, share_rx) = oneshot::channel(); - let (snapshots_to_send_tx, snapshots_to_send_rx) = - smol::channel::unbounded::(); + if self.share.is_some() { let _ = share_tx.send(Ok(())); } else { + let (snapshots_tx, mut snapshots_rx) = watch::channel_with(self.snapshot()); let rpc = self.client.clone(); let worktree_id = cx.model_id() as u64; let maintain_remote_snapshot = cx.background().spawn({ let rpc = rpc.clone(); let diagnostic_summaries = self.diagnostic_summaries.clone(); async move { - let mut prev_snapshot = match snapshots_to_send_rx.recv().await { - Ok(snapshot) => { + let mut prev_snapshot = match snapshots_rx.recv().await { + Some(snapshot) => { let update = proto::UpdateWorktree { project_id, worktree_id, @@ -886,8 +886,10 @@ impl LocalWorktree { snapshot } } - Err(error) => { - let _ = share_tx.send(Err(error.into())); + None => { + share_tx + .send(Err(anyhow!("worktree dropped before share completed"))) + .ok(); return Err(anyhow!("failed to send initial update worktree")); } }; @@ -900,11 +902,7 @@ impl LocalWorktree { })?; } - while let Ok(mut snapshot) = snapshots_to_send_rx.recv().await { - while let Ok(newer_snapshot) = snapshots_to_send_rx.try_recv() { - snapshot = newer_snapshot; - } - + while let Some(snapshot) = snapshots_rx.recv().await { send_worktree_update( &rpc, snapshot.build_update(&prev_snapshot, project_id, worktree_id, true), @@ -919,18 +917,12 @@ impl LocalWorktree { }); self.share = Some(ShareState { project_id, - snapshots_tx: snapshots_to_send_tx.clone(), + snapshots_tx, _maintain_remote_snapshot: Some(maintain_remote_snapshot), }); } - cx.spawn_weak(|this, cx| async move { - if let Some(this) = this.upgrade(&cx) { - this.read_with(&cx, |this, _| { - let this = this.as_local().unwrap(); - let _ = snapshots_to_send_tx.try_send(this.snapshot()); - }); - } + cx.foreground().spawn(async move { share_rx .await .unwrap_or_else(|_| Err(anyhow!("share ended"))) @@ -945,19 +937,11 @@ impl LocalWorktree { self.share.is_some() } - fn broadcast_snapshot(&self) -> impl Future { - let mut to_send = None; + fn broadcast_snapshot(&mut self) { if matches!(self.scan_state(), ScanState::Idle) { - if let Some(share) = self.share.as_ref() { - to_send = Some((self.snapshot(), share.snapshots_tx.clone())); - } - } - - async move { - if let Some((snapshot, snapshots_to_send_tx)) = to_send { - if let Err(err) = snapshots_to_send_tx.send(snapshot).await { - log::error!("error submitting snapshot to send {}", err); - } + let snapshot = self.snapshot(); + if let Some(share) = self.share.as_mut() { + *share.snapshots_tx.borrow_mut() = snapshot; } } } @@ -968,6 +952,12 @@ impl RemoteWorktree { self.snapshot.clone() } + fn poll_snapshot(&mut self, cx: &mut ModelContext) { + self.snapshot = self.background_snapshot.lock().clone(); + cx.emit(Event::UpdatedEntries); + cx.notify(); + } + pub fn disconnected_from_host(&mut self) { self.updates_tx.take(); self.snapshot_subscriptions.clear(); From ebe733a3938ff7578617d71118f365ff0695d380 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 30 Jun 2022 19:11:21 -0700 Subject: [PATCH 44/60] Restart language server using original root path rather than the path of the buffer restarted from --- crates/lsp/src/lsp.rs | 4 +++ crates/project/src/project.rs | 52 +++++++++++++++++++++++++++++------ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 682d8f1823..8282f1c8ff 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -493,6 +493,10 @@ impl LanguageServer { self.server_id } + pub fn root_path(&self) -> &PathBuf { + &self.root_path + } + pub fn request( &self, params: T::Params, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d0ddaa1fab..995b7bdb3e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2241,23 +2241,33 @@ impl Project { }); } + // Returns a list of all of the worktrees which no longer have a language server and the root path + // for the stopped server fn stop_language_server( &mut self, worktree_id: WorktreeId, adapter_name: LanguageServerName, cx: &mut ModelContext, - ) -> Task<()> { + ) -> Task<(Option, Vec)> { let key = (worktree_id, adapter_name); if let Some(server_id) = self.language_server_ids.remove(&key) { - // Remove other entries for this language server - self.language_server_ids - .retain(|_, other_id| other_id != &server_id); + // Remove other entries for this language server as well + let mut orphaned_worktrees = vec![worktree_id]; + let other_keys = self.language_server_ids.keys().cloned().collect::>(); + for other_key in other_keys { + if self.language_server_ids.get(&other_key) == Some(&server_id) { + self.language_server_ids.remove(&other_key); + orphaned_worktrees.push(other_key.0); + } + } self.language_server_statuses.remove(&server_id); cx.notify(); let server_state = self.language_servers.remove(&server_id); cx.spawn_weak(|this, mut cx| async move { + let mut root_path = None; + let server = match server_state { Some(LanguageServerState::Starting(started_language_server)) => { started_language_server.await @@ -2267,6 +2277,7 @@ impl Project { }; if let Some(server) = server { + root_path = Some(server.root_path().clone()); if let Some(shutdown) = server.shutdown() { shutdown.await; } @@ -2278,9 +2289,11 @@ impl Project { cx.notify(); }); } + + (root_path, orphaned_worktrees) }) } else { - Task::ready(()) + Task::ready((None, Vec::new())) } } @@ -2311,7 +2324,7 @@ impl Project { fn restart_language_server( &mut self, worktree_id: WorktreeId, - worktree_path: Arc, + fallback_path: Arc, language: Arc, cx: &mut ModelContext, ) { @@ -2321,12 +2334,33 @@ impl Project { return; }; - let stop = self.stop_language_server(worktree_id, adapter.name(), cx); + let server_name = adapter.name(); + let stop = self.stop_language_server(worktree_id, server_name.clone(), cx); cx.spawn_weak(|this, mut cx| async move { - stop.await; + let (original_root_path, orphaned_worktrees) = stop.await; if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - this.start_language_server(worktree_id, worktree_path, language, cx); + // Attempt to restart using original server path. Fallback to passed in + // path if we could not retrieve the root path + let root_path = original_root_path + .map(|path_buf| Arc::from(path_buf.as_path())) + .unwrap_or(fallback_path); + + this.start_language_server(worktree_id, root_path, language, cx); + + // Lookup new server id and set it for each of the orphaned worktrees + if let Some(new_server_id) = this + .language_server_ids + .get(&(worktree_id, server_name.clone())) + .cloned() + { + for orphaned_worktree in orphaned_worktrees { + this.language_server_ids.insert( + (orphaned_worktree, server_name.clone()), + new_server_id.clone(), + ); + } + } }); } }) From 092284b062a2774cb039577b170ce9281a4025d0 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Jun 2022 19:21:42 -0700 Subject: [PATCH 45/60] Fully functional background colors :D --- Cargo.lock | 1 + crates/terminal/Cargo.toml | 2 + crates/terminal/print256color.sh | 96 ++++++ crates/terminal/src/terminal_element.rs | 391 +++++++++++++++--------- crates/terminal/truecolor.sh | 19 ++ 5 files changed, 359 insertions(+), 150 deletions(-) create mode 100755 crates/terminal/print256color.sh create mode 100755 crates/terminal/truecolor.sh diff --git a/Cargo.lock b/Cargo.lock index 1a59f23918..85f3fc3a90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4880,6 +4880,7 @@ dependencies = [ "editor", "futures", "gpui", + "itertools", "mio-extras", "ordered-float", "project", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 175c741421..0bbc056922 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -20,6 +20,8 @@ smallvec = { version = "1.6", features = ["union"] } mio-extras = "2.0.6" futures = "0.3" ordered-float = "2.1.1" +itertools = "0.10" + [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/terminal/print256color.sh b/crates/terminal/print256color.sh new file mode 100755 index 0000000000..99e3d8c9f9 --- /dev/null +++ b/crates/terminal/print256color.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# Tom Hale, 2016. MIT Licence. +# Print out 256 colours, with each number printed in its corresponding colour +# See http://askubuntu.com/questions/821157/print-a-256-color-test-pattern-in-the-terminal/821163#821163 + +set -eu # Fail on errors or undeclared variables + +printable_colours=256 + +# Return a colour that contrasts with the given colour +# Bash only does integer division, so keep it integral +function contrast_colour { + local r g b luminance + colour="$1" + + if (( colour < 16 )); then # Initial 16 ANSI colours + (( colour == 0 )) && printf "15" || printf "0" + return + fi + + # Greyscale # rgb_R = rgb_G = rgb_B = (number - 232) * 10 + 8 + if (( colour > 231 )); then # Greyscale ramp + (( colour < 244 )) && printf "15" || printf "0" + return + fi + + # All other colours: + # 6x6x6 colour cube = 16 + 36*R + 6*G + B # Where RGB are [0..5] + # See http://stackoverflow.com/a/27165165/5353461 + + # r=$(( (colour-16) / 36 )) + g=$(( ((colour-16) % 36) / 6 )) + # b=$(( (colour-16) % 6 )) + + # If luminance is bright, print number in black, white otherwise. + # Green contributes 587/1000 to human perceived luminance - ITU R-REC-BT.601 + (( g > 2)) && printf "0" || printf "15" + return + + # Uncomment the below for more precise luminance calculations + + # # Calculate percieved brightness + # # See https://www.w3.org/TR/AERT#color-contrast + # # and http://www.itu.int/rec/R-REC-BT.601 + # # Luminance is in range 0..5000 as each value is 0..5 + # luminance=$(( (r * 299) + (g * 587) + (b * 114) )) + # (( $luminance > 2500 )) && printf "0" || printf "15" +} + +# Print a coloured block with the number of that colour +function print_colour { + local colour="$1" contrast + contrast=$(contrast_colour "$1") + printf "\e[48;5;%sm" "$colour" # Start block of colour + printf "\e[38;5;%sm%3d" "$contrast" "$colour" # In contrast, print number + printf "\e[0m " # Reset colour +} + +# Starting at $1, print a run of $2 colours +function print_run { + local i + for (( i = "$1"; i < "$1" + "$2" && i < printable_colours; i++ )) do + print_colour "$i" + done + printf " " +} + +# Print blocks of colours +function print_blocks { + local start="$1" i + local end="$2" # inclusive + local block_cols="$3" + local block_rows="$4" + local blocks_per_line="$5" + local block_length=$((block_cols * block_rows)) + + # Print sets of blocks + for (( i = start; i <= end; i += (blocks_per_line-1) * block_length )) do + printf "\n" # Space before each set of blocks + # For each block row + for (( row = 0; row < block_rows; row++ )) do + # Print block columns for all blocks on the line + for (( block = 0; block < blocks_per_line; block++ )) do + print_run $(( i + (block * block_length) )) "$block_cols" + done + (( i += block_cols )) # Prepare to print the next row + printf "\n" + done + done +} + +print_run 0 16 # The first 16 colours are spread over the whole spectrum +printf "\n" +print_blocks 16 231 6 6 3 # 6x6x6 colour cube between 16 and 231 inclusive +print_blocks 232 255 12 2 1 # Not 50, but 24 Shades of Grey diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index d81292d0c2..5124a9ea71 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -14,39 +14,76 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, json::json, text_layout::Line, - Event, MouseRegion, PaintContext, Quad, WeakViewHandle, + Event, FontCache, MouseRegion, PaintContext, Quad, SizeConstraint, WeakViewHandle, }; +use itertools::Itertools; use ordered_float::OrderedFloat; use settings::Settings; -use std::rc::Rc; +use std::{iter, rc::Rc}; use theme::TerminalStyle; use crate::{Input, ScrollTerminal, Terminal}; +///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. const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; +///Used to display the grid as passed to Alacritty and the TTY. +///Useful for debugging inconsistencies between behavior and display #[cfg(debug_assertions)] const DEBUG_GRID: bool = false; +///The GPUI element that paints the terminal. pub struct TerminalEl { view: WeakViewHandle, } +///Represents a span of cells in a single line in the terminal's grid. +///This is used for drawing background rectangles +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub struct LineSpan { + start: i32, + end: i32, + line: usize, + color: Color, +} + +impl LineSpan { + ///Creates a new LineSpan. `start` must be <= `end`. + ///If `start` == `end`, then this span is considered to be over a + /// single cell + fn new(start: i32, end: i32, line: usize, color: Color) -> LineSpan { + debug_assert!(start <= end); + LineSpan { + start, + end, + line, + color, + } + } +} + +struct CellWidth(f32); +struct LineHeight(f32); + +///The information generated during layout that is nescessary for painting +pub struct LayoutState { + lines: Vec, + line_height: LineHeight, + em_width: CellWidth, + cursor: Option<(RectF, Color)>, + cur_size: SizeInfo, + background_color: Color, + background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan +} + impl TerminalEl { pub fn new(view: WeakViewHandle) -> TerminalEl { TerminalEl { view } } } -pub struct LayoutState { - lines: Vec, - line_height: f32, - em_width: f32, - cursor: Option<(RectF, Color)>, - cur_size: SizeInfo, - background_color: Color, -} - impl Element for TerminalEl { type LayoutState = LayoutState; type PaintState = (); @@ -56,73 +93,56 @@ impl Element for TerminalEl { constraint: gpui::SizeConstraint, cx: &mut gpui::LayoutContext, ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - let view = self.view.upgrade(cx).unwrap(); - let size = constraint.max; - let settings = cx.global::(); - let editor_theme = &settings.theme.editor; - let font_cache = cx.font_cache(); - - //Set up text rendering - let text_style = TextStyle { - color: editor_theme.text_color, - font_family_id: settings.buffer_font_family, - font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(), - font_id: font_cache - .select_font(settings.buffer_font_family, &Default::default()) - .unwrap(), - font_size: settings.buffer_font_size, - font_properties: Default::default(), - underline: Default::default(), - }; - - let line_height = font_cache.line_height(text_style.font_size); - let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size); - - let new_size = SizeInfo::new( - size.x() - cell_width, - size.y(), - cell_width, - line_height, - 0., - 0., - false, + //Settings immutably borrows cx here for the settings and font cache + //and we need to modify the cx to resize the terminal. So instead of + //storing Settings or the font_cache(), we toss them ASAP and then reborrow later + let text_style = make_text_style(cx.font_cache(), cx.global::()); + let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size)); + let cell_width = CellWidth( + cx.font_cache() + .em_advance(text_style.font_id, text_style.font_size), ); - view.update(cx.app, |view, _cx| { - view.set_size(new_size); - }); + let view_handle = self.view.upgrade(cx).unwrap(); - let settings = cx.global::(); - let terminal_theme = &settings.theme.terminal; - let term = view.read(cx).term.lock(); + //Tell the view our new size. Requires a mutable borrow of cx and the view + let cur_size = make_new_size(constraint, &cell_width, &line_height); + //Note that set_size locks and mutates the terminal. + //TODO: Would be nice to lock once for the whole of layout + view_handle.update(cx.app, |view, _cx| view.set_size(cur_size)); + //Now that we're done with the mutable portion, grab the immutable settings and view again + let terminal_theme = &(cx.global::()).theme.terminal; + let term = view_handle.read(cx).term.lock(); let content = term.renderable_content(); - let (chunks, line_count) = build_chunks(content.display_iter, &terminal_theme); + //And we're off! Begin layouting + let (chunks, line_count) = build_chunks(content.display_iter, &terminal_theme); + let backgrounds = chunks + .iter() + .filter(|(_, _, line_span)| line_span != &LineSpan::default()) + .map(|(_, _, line_span)| *line_span) + .collect(); let shaped_lines = layout_highlighted_chunks( - chunks.iter().map(|(text, style)| (text.as_str(), *style)), + chunks + .iter() + .map(|(text, style, _)| (text.as_str(), *style)), &text_style, cx.text_layout_cache, - &cx.font_cache, + cx.font_cache(), usize::MAX, line_count, ); - let cursor_line = content.cursor.point.line.0 + content.display_offset as i32; - let mut cursor = None; - if let Some(layout_line) = cursor_line - .try_into() - .ok() - .and_then(|cursor_line: usize| shaped_lines.get(cursor_line)) - { - let cursor_x = layout_line.x_for_index(content.cursor.point.column.0); - cursor = Some(( - RectF::new( - vec2f(cursor_x, cursor_line as f32 * line_height), - vec2f(cell_width, line_height), - ), - terminal_theme.cursor, - )); - } + let background_rects = make_background_rects(backgrounds, &shaped_lines, &line_height); + + let cursor = make_cursor_rect( + content.cursor.point, + &shaped_lines, + content.display_offset, + &line_height, + &cell_width, + ) + .map(|cursor_rect| (cursor_rect, terminal_theme.cursor)); ( constraint.max, @@ -131,7 +151,8 @@ impl Element for TerminalEl { line_height, em_width: cell_width, cursor, - cur_size: new_size, + cur_size, + background_rects, background_color: terminal_theme.background, }, ) @@ -148,44 +169,47 @@ impl Element for TerminalEl { cx.scene.push_mouse_region(MouseRegion { view_id: self.view.id(), - discriminant: None, - bounds: visible_bounds, - hover: None, mouse_down: Some(Rc::new(|_, cx| cx.focus_parent_view())), - click: None, - right_mouse_down: None, - right_click: None, - drag: None, - mouse_down_out: None, - right_mouse_down_out: None, + bounds: visible_bounds, + ..Default::default() }); - //Background + let origin = bounds.origin() + vec2f(layout.em_width.0, 0.); + + //Start us off with a nice simple background color cx.scene.push_quad(Quad { - bounds: visible_bounds, + bounds: RectF::new(bounds.origin(), bounds.size()), background: Some(layout.background_color), border: Default::default(), corner_radius: 0., }); - let origin = bounds.origin() + vec2f(layout.em_width, 0.); //Padding + //Draw cell backgrounds + for background_rect in &layout.background_rects { + let new_origin = origin + background_rect.0.origin(); + cx.scene.push_quad(Quad { + bounds: RectF::new(new_origin, background_rect.0.size()), + background: Some(background_rect.1), + border: Default::default(), + corner_radius: 0., + }) + } - let mut line_origin = origin; + //Draw text + let mut line_origin = origin.clone(); for line in &layout.lines { - let boundaries = RectF::new(line_origin, vec2f(bounds.width(), layout.line_height)); - + let boundaries = RectF::new(line_origin, vec2f(bounds.width(), layout.line_height.0)); if boundaries.intersects(visible_bounds) { - line.paint(line_origin, visible_bounds, layout.line_height, cx); + line.paint(line_origin, visible_bounds, layout.line_height.0, cx); } - line_origin.set_y(boundaries.max_y()); } + //Draw cursor if let Some((c, color)) = layout.cursor { let new_origin = origin + c.origin(); - let new_cursor = RectF::new(new_origin, c.size()); cx.scene.push_quad(Quad { - bounds: new_cursor, + bounds: RectF::new(new_origin, c.size()), background: Some(color), border: Default::default(), corner_radius: 0., @@ -215,7 +239,7 @@ impl Element for TerminalEl { } => { if visible_bounds.contains_point(*position) { let vertical_scroll = - (delta.y() / layout.line_height) * ALACRITTY_SCROLL_MULTIPLIER; + (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER; cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32)); true } else { @@ -249,67 +273,134 @@ impl Element for TerminalEl { } } +fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { + TextStyle { + color: settings.theme.editor.text_color, + font_family_id: settings.buffer_font_family, + font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(), + font_id: font_cache + .select_font(settings.buffer_font_family, &Default::default()) + .unwrap(), + font_size: settings.buffer_font_size, + font_properties: Default::default(), + underline: Default::default(), + } +} + +fn make_new_size( + constraint: SizeConstraint, + cell_width: &CellWidth, + line_height: &LineHeight, +) -> SizeInfo { + SizeInfo::new( + constraint.max.x() - cell_width.0, + constraint.max.y(), + cell_width.0, + line_height.0, + 0., + 0., + false, + ) +} + pub(crate) fn build_chunks( grid_iterator: GridIterator, theme: &TerminalStyle, -) -> (Vec<(String, Option)>, usize) { - let mut lines: Vec<(String, Option)> = vec![]; - let mut last_line = 0; - let mut line_count = 1; - let mut cur_chunk = String::new(); - - let mut cur_highlight = HighlightStyle { - color: Some(Color::white()), - ..Default::default() - }; - - for cell in grid_iterator { - let Indexed { - point: Point { line, .. }, - cell: Cell { - c, fg, flags, .. // TODO: Add bg and flags - }, //TODO: Learn what 'CellExtra does' - } = cell; - - let new_highlight = make_style_from_cell(fg, flags, theme); - - if line != last_line { +) -> (Vec<(String, Option, LineSpan)>, usize) { + let mut line_count: usize = 0; + let lines = grid_iterator.group_by(|i| i.point.line.0); + let result = lines + .into_iter() + .map(|(_, line)| { line_count += 1; - cur_chunk.push('\n'); - last_line = line.0; - } + let mut col_index = 0; - if new_highlight != cur_highlight { - lines.push((cur_chunk.clone(), Some(cur_highlight.clone()))); - cur_chunk.clear(); - cur_highlight = new_highlight; - } - cur_chunk.push(*c) - } - lines.push((cur_chunk, Some(cur_highlight))); - (lines, line_count) -} - -fn make_style_from_cell(fg: &AnsiColor, flags: &Flags, style: &TerminalStyle) -> HighlightStyle { - let fg = Some(alac_color_to_gpui_color(fg, style)); - let underline = if flags.contains(Flags::UNDERLINE) { - Some(Underline { - color: fg, - squiggly: false, - thickness: OrderedFloat(1.), + let chunks = line.group_by(|i| cell_style(&i, theme)); + chunks + .into_iter() + .map(|(style, fragment)| { + let str_fragment = fragment.map(|indexed| indexed.c).collect::(); + let start = col_index; + let end = start + str_fragment.len() as i32; + col_index = end; + ( + str_fragment, + Some(style.0), + LineSpan::new(start, end, line_count - 1, style.1), //Line count -> Line index + ) + }) + .chain(iter::once(("\n".to_string(), None, Default::default()))) + .collect::, LineSpan)>>() }) - } else { - None - }; - HighlightStyle { - color: fg, - underline, - ..Default::default() - } + .flatten() + .collect::, LineSpan)>>(); + (result, line_count) } -fn alac_color_to_gpui_color(allac_color: &AnsiColor, style: &TerminalStyle) -> Color { - match allac_color { +fn make_background_rects( + backgrounds: Vec, + shaped_lines: &Vec, + line_height: &LineHeight, +) -> Vec<(RectF, Color)> { + backgrounds + .into_iter() + .map(|line_span| { + let line = shaped_lines + .get(line_span.line) + .expect("Background line_num did not correspond to a line number"); + let x = line.x_for_index(line_span.start as usize); + let width = line.x_for_index(line_span.end as usize) - x; + ( + RectF::new( + vec2f(x, line_span.line as f32 * line_height.0), + vec2f(width, line_height.0), + ), + line_span.color, + ) + }) + .collect::>() +} + +fn make_cursor_rect( + cursor_point: Point, + shaped_lines: &Vec, + display_offset: usize, + line_height: &LineHeight, + cell_width: &CellWidth, +) -> Option { + let cursor_line = cursor_point.line.0 as usize + display_offset; + shaped_lines.get(cursor_line).map(|layout_line| { + let cursor_x = layout_line.x_for_index(cursor_point.column.0); + RectF::new( + vec2f(cursor_x, cursor_line as f32 * line_height.0), + vec2f(cell_width.0, line_height.0), + ) + }) +} + +fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle) -> (HighlightStyle, Color) { + let flags = indexed.cell.flags; + let fg = Some(alac_color_to_gpui_color(&indexed.cell.fg, style)); + let bg = alac_color_to_gpui_color(&indexed.cell.bg, style); + + let underline = flags.contains(Flags::UNDERLINE).then(|| Underline { + color: fg, + squiggly: false, + thickness: OrderedFloat(1.), + }); + + ( + HighlightStyle { + color: fg, + underline, + ..Default::default() + }, + bg, + ) +} + +fn alac_color_to_gpui_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color { + match alac_color { alacritty_terminal::ansi::Color::Named(n) => match n { alacritty_terminal::ansi::NamedColor::Black => style.black, alacritty_terminal::ansi::NamedColor::Red => style.red, @@ -341,7 +432,7 @@ fn alac_color_to_gpui_color(allac_color: &AnsiColor, style: &TerminalStyle) -> C alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground, alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground, }, //Theme defined - alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, 1), + alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX), alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style), //Color cube weirdness } } @@ -366,14 +457,14 @@ pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color { 15 => style.bright_white, 16..=231 => { let (r, g, b) = rgb_for_index(index); //Split the index into it's rgb components - let step = (u8::MAX as f32 / 5.).round() as u8; //Split the GPUI range into 5 chunks - Color::new(r * step, g * step, b * step, 1) //Map the rgb components to GPUI's range + let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the 256 channel range into 5 chunks + Color::new(r * step, g * step, b * step, u8::MAX) //Map the [0, 5] rgb components to the [0, 256] channel range } //Grayscale from black to white, 0 to 24 232..=255 => { - let i = 24 - (index - 232); //Align index to 24..0 - let step = (u8::MAX as f32 / 24.).round() as u8; //Split the 256 range grayscale into 24 chunks - Color::new(i * step, i * step, i * step, 1) //Map the rgb components to GPUI's range + let i = index - 232; //Align index to 0..24 + let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the [0,256] range grayscale values into 24 chunks + Color::new(i * step, i * step, i * step, u8::MAX) //Map the rgb components to the grayscale range } } } @@ -400,10 +491,10 @@ fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContex let width = layout.cur_size.width(); let height = layout.cur_size.height(); //Alacritty uses 'as usize', so shall we. - for col in 0..(width / layout.em_width).round() as usize { + for col in 0..(width / layout.em_width.0).round() as usize { cx.scene.push_quad(Quad { bounds: RectF::new( - bounds.origin() + vec2f((col + 1) as f32 * layout.em_width, 0.), + bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.), vec2f(1., height), ), background: Some(Color::green()), @@ -411,10 +502,10 @@ fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContex corner_radius: 0., }); } - for row in 0..((height / layout.line_height) + 1.0).round() as usize { + for row in 0..((height / layout.line_height.0) + 1.0).round() as usize { cx.scene.push_quad(Quad { bounds: RectF::new( - bounds.origin() + vec2f(layout.em_width, row as f32 * layout.line_height), + bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0), vec2f(width, 1.), ), background: Some(Color::green()), diff --git a/crates/terminal/truecolor.sh b/crates/terminal/truecolor.sh new file mode 100755 index 0000000000..14e5d81308 --- /dev/null +++ b/crates/terminal/truecolor.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Copied from: https://unix.stackexchange.com/a/696756 +# Based on: https://gist.github.com/XVilka/8346728 and https://unix.stackexchange.com/a/404415/395213 + +awk -v term_cols="${width:-$(tput cols || echo 80)}" -v term_lines="${height:-1}" 'BEGIN{ + s="/\\"; + total_cols=term_cols*term_lines; + for (colnum = 0; colnum255) g = 510-g; + printf "\033[48;2;%d;%d;%dm", r,g,b; + printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b; + printf "%s\033[0m", substr(s,colnum%2+1,1); + if (colnum%term_cols==term_cols) printf "\n"; + } + printf "\n"; +}' \ No newline at end of file From bcf5351e64cabeeafb52f9bce71613c05c04e3b7 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Jun 2022 20:02:16 -0700 Subject: [PATCH 46/60] Refactored and commented code to be my expressive --- crates/terminal/src/terminal.rs | 45 ++++++++--- crates/terminal/src/terminal_element.rs | 101 +++++++++++++++--------- 2 files changed, 94 insertions(+), 52 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 6e62ce2a9f..984f883746 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -25,7 +25,6 @@ use workspace::{Item, Workspace}; use crate::terminal_element::{get_color_at_index, TerminalEl}; //ASCII Control characters on a keyboard -//Consts -> Structs -> Impls -> Functions, Vaguely in order of importance const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c' const TAB_CHAR: char = 9_u8 as char; const CARRIAGE_RETURN_CHAR: char = 13_u8 as char; @@ -39,9 +38,11 @@ const DEFAULT_TITLE: &str = "Terminal"; pub mod terminal_element; +///Action for carrying the input to the PTY #[derive(Clone, Default, Debug, PartialEq, Eq)] pub struct Input(pub String); +///Event to transmit the scroll from the element to the view #[derive(Clone, Debug, PartialEq)] pub struct ScrollTerminal(pub i32); @@ -51,6 +52,7 @@ actions!( ); impl_internal_actions!(terminal, [Input, ScrollTerminal]); +///Initialize and register all of our action handlers pub fn init(cx: &mut MutableAppContext) { cx.add_action(Terminal::deploy); cx.add_action(Terminal::write_to_pty); @@ -68,6 +70,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Terminal::scroll_terminal); } +///A translation struct for Alacritty to communicate with us from their event loop #[derive(Clone)] pub struct ZedListener(UnboundedSender); @@ -77,7 +80,7 @@ impl EventListener for ZedListener { } } -///A terminal renderer. +///A terminal view, maintains the PTY's file handles and communicates with the terminal pub struct Terminal { pty_tx: Notifier, term: Arc>>, @@ -87,6 +90,7 @@ pub struct Terminal { cur_size: SizeInfo, } +///Upward flowing events, for changing the title and such pub enum Event { TitleChanged, CloseTerminal, @@ -128,7 +132,8 @@ impl Terminal { ..Default::default() }; - //The details here don't matter, the terminal will be resized on layout + //The details here don't matter, the terminal will be resized on the first layout + //Set to something small for easier debugging let size_info = SizeInfo::new(200., 100.0, 5., 5., 0., 0., false); //Set up the terminal... @@ -169,7 +174,6 @@ impl Terminal { match event { AlacTermEvent::Wakeup => { if !cx.is_self_focused() { - //Need to figure out how to trigger a redraw when not in focus self.has_new_content = true; //Change tab content cx.emit(Event::TitleChanged); } else { @@ -207,6 +211,7 @@ impl Terminal { let term_style = &cx.global::().theme.terminal; match index { 0..=255 => to_alac_rgb(get_color_at_index(&(index as u8), term_style)), + //These additional values are required to match the Alacritty Colors object's behavior 256 => to_alac_rgb(term_style.foreground), 257 => to_alac_rgb(term_style.background), 258 => to_alac_rgb(term_style.cursor), @@ -226,8 +231,7 @@ impl Terminal { self.write_to_pty(&Input(format(color)), cx) } AlacTermEvent::CursorBlinkingChange => { - //So, it's our job to set a timer and cause the cursor to blink here - //Which means that I'm going to put this off until someone @ Zed looks at it + //TODO: Set a timer to blink the cursor on and off } AlacTermEvent::Bell => { self.has_bell = true; @@ -237,6 +241,7 @@ impl Terminal { } } + ///Resize the terminal and the PTY. This locks the terminal. fn set_size(&mut self, new_size: SizeInfo) { if new_size != self.cur_size { self.pty_tx.0.send(Msg::Resize(new_size)).ok(); @@ -245,18 +250,20 @@ impl Terminal { } } + ///Scroll the terminal. This locks the terminal fn scroll_terminal(&mut self, scroll: &ScrollTerminal, _: &mut ViewContext) { self.term.lock().scroll_display(Scroll::Delta(scroll.0)); } - ///Create a new Terminal + ///Create a new Terminal in the current working directory or the user's home directory fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { let project = workspace.project().read(cx); let abs_path = project .active_entry() .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) - .map(|wt| wt.abs_path().to_path_buf()); + .map(|wt| wt.abs_path().to_path_buf()) + .or_else(|| Some("~".into())); workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path))), cx); } @@ -266,16 +273,19 @@ impl Terminal { self.pty_tx.0.send(Msg::Shutdown).ok(); } + ///Tell Zed to close us fn quit(&mut self, _: &Quit, cx: &mut ViewContext) { cx.emit(Event::CloseTerminal); } + ///Attempt to paste the clipboard into the terminal fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { if let Some(item) = cx.read_from_clipboard() { self.write_to_pty(&Input(item.text().to_owned()), cx); } } + ///Write the Input payload to the tty. This locks the terminal so we can scroll it. fn write_to_pty(&mut self, input: &Input, cx: &mut ViewContext) { //iTerm bell behavior, bell stays until terminal is interacted with self.has_bell = false; @@ -284,38 +294,47 @@ impl Terminal { self.pty_tx.notify(input.0.clone().into_bytes()); } + ///Send the `up` key fn up(&mut self, _: &Up, cx: &mut ViewContext) { self.write_to_pty(&Input(UP_SEQ.to_string()), cx); } + ///Send the `down` key fn down(&mut self, _: &Down, cx: &mut ViewContext) { self.write_to_pty(&Input(DOWN_SEQ.to_string()), cx); } + ///Send the `tab` key fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { self.write_to_pty(&Input(TAB_CHAR.to_string()), cx); } + ///Send `SIGINT` (`ctrl-c`) fn send_sigint(&mut self, _: &Sigint, cx: &mut ViewContext) { self.write_to_pty(&Input(ETX_CHAR.to_string()), cx); } + ///Send the `escape` key fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { self.write_to_pty(&Input(ESC_CHAR.to_string()), cx); } + ///Send the `delete` key. TODO: Difference between this and backspace? fn del(&mut self, _: &Del, cx: &mut ViewContext) { self.write_to_pty(&Input(DEL_CHAR.to_string()), cx); } + ///Send a carriage return. TODO: May need to check the terminal mode. fn carriage_return(&mut self, _: &Return, cx: &mut ViewContext) { self.write_to_pty(&Input(CARRIAGE_RETURN_CHAR.to_string()), cx); } + //Send the `left` key fn left(&mut self, _: &Left, cx: &mut ViewContext) { self.write_to_pty(&Input(LEFT_SEQ.to_string()), cx); } + //Send the `right` key fn right(&mut self, _: &Right, cx: &mut ViewContext) { self.write_to_pty(&Input(RIGHT_SEQ.to_string()), cx); } @@ -333,10 +352,7 @@ impl View for Terminal { } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - TerminalEl::new(cx.handle()) - .contained() - // .with_style(theme.terminal.container) - .boxed() + TerminalEl::new(cx.handle()).contained().boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { @@ -354,7 +370,7 @@ impl Item for Terminal { if self.has_bell { flex.add_child( - Svg::new("icons/zap.svg") + Svg::new("icons/zap.svg") //TODO: Swap out for a better icon, or at least resize this .with_color(tab_theme.label.text.color) .constrained() .with_width(search_theme.tab_icon_width) @@ -437,6 +453,7 @@ impl Item for Terminal { } } +//Convenience method for less lines fn to_alac_rgb(color: Color) -> AlacRgb { AlacRgb { r: color.r, @@ -451,6 +468,8 @@ mod tests { use crate::terminal_element::build_chunks; use gpui::TestAppContext; + ///Basic integration test, can we get the terminal to show up, execute a command, + //and produce noticable output? #[gpui::test] async fn test_terminal(cx: &mut TestAppContext) { let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None)); diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 5124a9ea71..ac964dca05 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -42,20 +42,21 @@ pub struct TerminalEl { ///Represents a span of cells in a single line in the terminal's grid. ///This is used for drawing background rectangles #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] -pub struct LineSpan { +pub struct RectSpan { start: i32, end: i32, line: usize, color: Color, } -impl LineSpan { +///A background color span +impl RectSpan { ///Creates a new LineSpan. `start` must be <= `end`. ///If `start` == `end`, then this span is considered to be over a /// single cell - fn new(start: i32, end: i32, line: usize, color: Color) -> LineSpan { + fn new(start: i32, end: i32, line: usize, color: Color) -> RectSpan { debug_assert!(start <= end); - LineSpan { + RectSpan { start, end, line, @@ -64,6 +65,7 @@ impl LineSpan { } } +///Helper types so I don't mix these two up struct CellWidth(f32); struct LineHeight(f32); @@ -117,11 +119,7 @@ impl Element for TerminalEl { //And we're off! Begin layouting let (chunks, line_count) = build_chunks(content.display_iter, &terminal_theme); - let backgrounds = chunks - .iter() - .filter(|(_, _, line_span)| line_span != &LineSpan::default()) - .map(|(_, _, line_span)| *line_span) - .collect(); + let shaped_lines = layout_highlighted_chunks( chunks .iter() @@ -133,6 +131,11 @@ impl Element for TerminalEl { line_count, ); + let backgrounds = chunks + .iter() + .filter(|(_, _, line_span)| line_span != &RectSpan::default()) + .map(|(_, _, line_span)| *line_span) + .collect(); let background_rects = make_background_rects(backgrounds, &shaped_lines, &line_height); let cursor = make_cursor_rect( @@ -165,8 +168,10 @@ impl Element for TerminalEl { layout: &mut Self::LayoutState, cx: &mut gpui::PaintContext, ) -> Self::PaintState { + //Setup element stuff cx.scene.push_layer(Some(visible_bounds)); + //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse cx.scene.push_mouse_region(MouseRegion { view_id: self.view.id(), mouse_down: Some(Rc::new(|_, cx| cx.focus_parent_view())), @@ -236,26 +241,22 @@ impl Element for TerminalEl { match event { Event::ScrollWheel { delta, position, .. - } => { - if visible_bounds.contains_point(*position) { + } => visible_bounds + .contains_point(*position) + .then(|| { let vertical_scroll = (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER; cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32)); - true - } else { - false - } - } + }) + .is_some(), Event::KeyDown { input: Some(input), .. - } => { - if cx.is_parent_view_focused() { + } => cx + .is_parent_view_focused() + .then(|| { cx.dispatch_action(Input(input.to_string())); - true - } else { - false - } - } + }) + .is_some(), _ => false, } } @@ -303,11 +304,15 @@ fn make_new_size( ) } +///In a single pass, this function generates the background and foreground color info for every item in the grid. pub(crate) fn build_chunks( grid_iterator: GridIterator, theme: &TerminalStyle, -) -> (Vec<(String, Option, LineSpan)>, usize) { +) -> (Vec<(String, Option, RectSpan)>, usize) { let mut line_count: usize = 0; + //Every `group_by()` -> `into_iter()` pair needs to be seperated by a local variable so + //rust knows where to put everything. + //Start by grouping by lines let lines = grid_iterator.group_by(|i| i.point.line.0); let result = lines .into_iter() @@ -315,10 +320,12 @@ pub(crate) fn build_chunks( line_count += 1; let mut col_index = 0; + //Then group by style let chunks = line.group_by(|i| cell_style(&i, theme)); chunks .into_iter() .map(|(style, fragment)| { + //And assemble the styled fragment into it's background and foreground information let str_fragment = fragment.map(|indexed| indexed.c).collect::(); let start = col_index; let end = start + str_fragment.len() as i32; @@ -326,25 +333,30 @@ pub(crate) fn build_chunks( ( str_fragment, Some(style.0), - LineSpan::new(start, end, line_count - 1, style.1), //Line count -> Line index + RectSpan::new(start, end, line_count - 1, style.1), //Line count -> Line index ) }) + //Add a \n to the end, as we're using text layouting rather than grid layouts .chain(iter::once(("\n".to_string(), None, Default::default()))) - .collect::, LineSpan)>>() + .collect::, RectSpan)>>() }) + //We have a Vec> (Vec of lines of styled chunks), flatten to just Vec<> (the styled chunks) .flatten() - .collect::, LineSpan)>>(); + .collect::, RectSpan)>>(); (result, line_count) } +///Convert a RectSpan in terms of character offsets, into RectFs of exact offsets fn make_background_rects( - backgrounds: Vec, + backgrounds: Vec, shaped_lines: &Vec, line_height: &LineHeight, ) -> Vec<(RectF, Color)> { backgrounds .into_iter() .map(|line_span| { + //This should always be safe, as the shaped lines and backgrounds where derived + //At the same time earlier let line = shaped_lines .get(line_span.line) .expect("Background line_num did not correspond to a line number"); @@ -361,6 +373,7 @@ fn make_background_rects( .collect::>() } +///Create the rectangle for a cursor, exactly positioned according to the text fn make_cursor_rect( cursor_point: Point, shaped_lines: &Vec, @@ -378,10 +391,11 @@ fn make_cursor_rect( }) } +///Convert the Alacritty cell styles to GPUI text styles and background color fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle) -> (HighlightStyle, Color) { let flags = indexed.cell.flags; - let fg = Some(alac_color_to_gpui_color(&indexed.cell.fg, style)); - let bg = alac_color_to_gpui_color(&indexed.cell.bg, style); + let fg = Some(convert_color(&indexed.cell.fg, style)); + let bg = convert_color(&indexed.cell.bg, style); let underline = flags.contains(Flags::UNDERLINE).then(|| Underline { color: fg, @@ -399,8 +413,10 @@ fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle) -> (HighlightStyl ) } -fn alac_color_to_gpui_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color { +///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent +fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color { match alac_color { + //Named and theme defined colors alacritty_terminal::ansi::Color::Named(n) => match n { alacritty_terminal::ansi::NamedColor::Black => style.black, alacritty_terminal::ansi::NamedColor::Red => style.red, @@ -431,14 +447,18 @@ fn alac_color_to_gpui_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Co alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white, alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground, alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground, - }, //Theme defined + }, + //'True' colors alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX), - alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style), //Color cube weirdness + //8 bit, indexed colors + alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style), } } +///Converts an 8 bit ANSI color to it's GPUI equivalent. pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color { match index { + //0-15 are the same as the named colors above 0 => style.black, 1 => style.red, 2 => style.green, @@ -455,16 +475,17 @@ pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color { 13 => style.bright_magenta, 14 => style.bright_cyan, 15 => style.bright_white, + //16-231 are mapped to their RGB colors on a 0-5 range per channel 16..=231 => { - let (r, g, b) = rgb_for_index(index); //Split the index into it's rgb components - let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the 256 channel range into 5 chunks - Color::new(r * step, g * step, b * step, u8::MAX) //Map the [0, 5] rgb components to the [0, 256] channel range + let (r, g, b) = rgb_for_index(index); //Split the index into it's ANSI-RGB components + let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow + Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color } - //Grayscale from black to white, 0 to 24 + //232-255 are a 24 step grayscale from black to white 232..=255 => { let i = index - 232; //Align index to 0..24 - let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the [0,256] range grayscale values into 24 chunks - Color::new(i * step, i * step, i * step, u8::MAX) //Map the rgb components to the grayscale range + let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks + Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale } } } @@ -486,6 +507,8 @@ fn rgb_for_index(i: &u8) -> (u8, u8, u8) { (r, g, b) } +///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between +///Display and conceptual grid. #[cfg(debug_assertions)] fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) { let width = layout.cur_size.width(); From 64d3dc32d2df76161daa691986e6d401e0482ed1 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Jun 2022 20:30:52 -0700 Subject: [PATCH 47/60] Update terminal.rs Whoopsies --- crates/terminal/src/terminal.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 984f883746..134cf5be6e 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -262,8 +262,7 @@ impl Terminal { .active_entry() .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) - .map(|wt| wt.abs_path().to_path_buf()) - .or_else(|| Some("~".into())); + .map(|wt| wt.abs_path().to_path_buf()); workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path))), cx); } From 831afb5ba777cbec15b380953699281027687d10 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Jun 2022 20:34:06 -0700 Subject: [PATCH 48/60] Fixed a major bug and now use the same cursor paint logic as the editor --- crates/editor/src/element.rs | 22 ++++++++++-- crates/terminal/src/terminal.rs | 3 +- crates/terminal/src/terminal_element.rs | 48 +++++++++++++++---------- 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 19226c6472..348ce57ef3 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1630,7 +1630,7 @@ impl Default for CursorShape { } } -struct Cursor { +pub struct Cursor { origin: Vector2F, block_width: f32, line_height: f32, @@ -1640,7 +1640,25 @@ struct Cursor { } impl Cursor { - fn paint(&self, cx: &mut PaintContext) { + pub fn new( + origin: Vector2F, + block_width: f32, + line_height: f32, + color: Color, + shape: CursorShape, + block_text: Option, + ) -> Cursor { + Cursor { + origin, + block_width, + line_height, + color, + shape, + block_text, + } + } + + pub fn paint(&self, cx: &mut PaintContext) { let bounds = match self.shape { CursorShape::Bar => RectF::new(self.origin, vec2f(2.0, self.line_height)), CursorShape::Block => { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 984f883746..134cf5be6e 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -262,8 +262,7 @@ impl Terminal { .active_entry() .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) - .map(|wt| wt.abs_path().to_path_buf()) - .or_else(|| Some("~".into())); + .map(|wt| wt.abs_path().to_path_buf()); workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path))), cx); } diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index ac964dca05..697a0abed9 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -7,11 +7,15 @@ use alacritty_terminal::{ SizeInfo, }, }; +use editor::{Cursor, CursorShape}; use gpui::{ color::Color, elements::*, fonts::{HighlightStyle, TextStyle, Underline}, - geometry::{rect::RectF, vector::vec2f}, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, json::json, text_layout::Line, Event, FontCache, MouseRegion, PaintContext, Quad, SizeConstraint, WeakViewHandle, @@ -74,7 +78,7 @@ pub struct LayoutState { lines: Vec, line_height: LineHeight, em_width: CellWidth, - cursor: Option<(RectF, Color)>, + cursor: Option<(Vector2F, Color)>, cur_size: SizeInfo, background_color: Color, background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan @@ -138,12 +142,11 @@ impl Element for TerminalEl { .collect(); let background_rects = make_background_rects(backgrounds, &shaped_lines, &line_height); - let cursor = make_cursor_rect( + let cursor = get_cursor_position( content.cursor.point, &shaped_lines, content.display_offset, &line_height, - &cell_width, ) .map(|cursor_rect| (cursor_rect, terminal_theme.cursor)); @@ -179,6 +182,16 @@ impl Element for TerminalEl { ..Default::default() }); + //TODO: Implement cursor region based styling + // cx.scene.push_cursor_region(CursorRegion { + // bounds, + // style: if !view.link_go_to_definition_state.definitions.is_empty() { + // CursorStyle::PointingHand + // } else { + // CursorStyle::IBeam + // }, + // }); + let origin = bounds.origin() + vec2f(layout.em_width.0, 0.); //Start us off with a nice simple background color @@ -212,13 +225,16 @@ impl Element for TerminalEl { //Draw cursor if let Some((c, color)) = layout.cursor { - let new_origin = origin + c.origin(); - cx.scene.push_quad(Quad { - bounds: RectF::new(new_origin, c.size()), - background: Some(color), - border: Default::default(), - corner_radius: 0., - }); + let editor_cursor = Cursor::new( + origin + c, + layout.em_width.0, + layout.line_height.0, + color, + CursorShape::Block, + None, //TODO fix this + ); + + editor_cursor.paint(cx); } #[cfg(debug_assertions)] @@ -374,20 +390,16 @@ fn make_background_rects( } ///Create the rectangle for a cursor, exactly positioned according to the text -fn make_cursor_rect( +fn get_cursor_position( cursor_point: Point, shaped_lines: &Vec, display_offset: usize, line_height: &LineHeight, - cell_width: &CellWidth, -) -> Option { +) -> Option { let cursor_line = cursor_point.line.0 as usize + display_offset; shaped_lines.get(cursor_line).map(|layout_line| { let cursor_x = layout_line.x_for_index(cursor_point.column.0); - RectF::new( - vec2f(cursor_x, cursor_line as f32 * line_height.0), - vec2f(cell_width.0, line_height.0), - ) + vec2f(cursor_x, cursor_line as f32 * line_height.0) }) } From ae836e14658b278e4c14481c446a3bbb590ac027 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Jun 2022 20:34:06 -0700 Subject: [PATCH 49/60] Fixed a major bug and now use the same cursor paint logic as the editor --- crates/editor/src/element.rs | 22 ++++++++++-- crates/terminal/src/terminal_element.rs | 48 +++++++++++++++---------- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 19226c6472..348ce57ef3 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1630,7 +1630,7 @@ impl Default for CursorShape { } } -struct Cursor { +pub struct Cursor { origin: Vector2F, block_width: f32, line_height: f32, @@ -1640,7 +1640,25 @@ struct Cursor { } impl Cursor { - fn paint(&self, cx: &mut PaintContext) { + pub fn new( + origin: Vector2F, + block_width: f32, + line_height: f32, + color: Color, + shape: CursorShape, + block_text: Option, + ) -> Cursor { + Cursor { + origin, + block_width, + line_height, + color, + shape, + block_text, + } + } + + pub fn paint(&self, cx: &mut PaintContext) { let bounds = match self.shape { CursorShape::Bar => RectF::new(self.origin, vec2f(2.0, self.line_height)), CursorShape::Block => { diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index ac964dca05..697a0abed9 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -7,11 +7,15 @@ use alacritty_terminal::{ SizeInfo, }, }; +use editor::{Cursor, CursorShape}; use gpui::{ color::Color, elements::*, fonts::{HighlightStyle, TextStyle, Underline}, - geometry::{rect::RectF, vector::vec2f}, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, json::json, text_layout::Line, Event, FontCache, MouseRegion, PaintContext, Quad, SizeConstraint, WeakViewHandle, @@ -74,7 +78,7 @@ pub struct LayoutState { lines: Vec, line_height: LineHeight, em_width: CellWidth, - cursor: Option<(RectF, Color)>, + cursor: Option<(Vector2F, Color)>, cur_size: SizeInfo, background_color: Color, background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan @@ -138,12 +142,11 @@ impl Element for TerminalEl { .collect(); let background_rects = make_background_rects(backgrounds, &shaped_lines, &line_height); - let cursor = make_cursor_rect( + let cursor = get_cursor_position( content.cursor.point, &shaped_lines, content.display_offset, &line_height, - &cell_width, ) .map(|cursor_rect| (cursor_rect, terminal_theme.cursor)); @@ -179,6 +182,16 @@ impl Element for TerminalEl { ..Default::default() }); + //TODO: Implement cursor region based styling + // cx.scene.push_cursor_region(CursorRegion { + // bounds, + // style: if !view.link_go_to_definition_state.definitions.is_empty() { + // CursorStyle::PointingHand + // } else { + // CursorStyle::IBeam + // }, + // }); + let origin = bounds.origin() + vec2f(layout.em_width.0, 0.); //Start us off with a nice simple background color @@ -212,13 +225,16 @@ impl Element for TerminalEl { //Draw cursor if let Some((c, color)) = layout.cursor { - let new_origin = origin + c.origin(); - cx.scene.push_quad(Quad { - bounds: RectF::new(new_origin, c.size()), - background: Some(color), - border: Default::default(), - corner_radius: 0., - }); + let editor_cursor = Cursor::new( + origin + c, + layout.em_width.0, + layout.line_height.0, + color, + CursorShape::Block, + None, //TODO fix this + ); + + editor_cursor.paint(cx); } #[cfg(debug_assertions)] @@ -374,20 +390,16 @@ fn make_background_rects( } ///Create the rectangle for a cursor, exactly positioned according to the text -fn make_cursor_rect( +fn get_cursor_position( cursor_point: Point, shaped_lines: &Vec, display_offset: usize, line_height: &LineHeight, - cell_width: &CellWidth, -) -> Option { +) -> Option { let cursor_line = cursor_point.line.0 as usize + display_offset; shaped_lines.get(cursor_line).map(|layout_line| { let cursor_x = layout_line.x_for_index(cursor_point.column.0); - RectF::new( - vec2f(cursor_x, cursor_line as f32 * line_height.0), - vec2f(cell_width.0, line_height.0), - ) + vec2f(cursor_x, cursor_line as f32 * line_height.0) }) } From d36a4888dbd7e49fbd21efd3227e98a7671f3345 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Jul 2022 11:45:30 +0200 Subject: [PATCH 50/60] Ensure worktrees have been sent before responding with definitions Changing the frequency at which we update worktrees highlighted a problem in the randomized tests that was causing clients to receive a definition to a worktree *before* observing the registration of the worktree itself. This was most likely caused by #1224 because the scenario that pull request enabled was the following: - Guest requests a definition pointing to a non-existant worktree - Server forwards the request to the host - Host sends an `UpdateProject` message - Host sends a response to the definition request - Server observes the `UpdateProject` message and tries to acquire the store - Given that we're waiting, the server goes ahead to process the response for the definition request, responding *before* `UpdateProject` is forwarded - Server finally forwards `UpdateProject` to the guest This commit ensures that, after forwarding a project request and getting a response, we acquire a lock to the store again to ensure the project still exists. This has the effect of ordering the forwarded request *after* any message that was received prior to the response and for which we are still waiting to acquire a lock to the store. --- crates/collab/src/integration_tests.rs | 19 ++--- crates/collab/src/rpc.rs | 106 ++++++++++--------------- 2 files changed, 49 insertions(+), 76 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index d901bd060c..4d23c00d42 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -50,7 +50,6 @@ use std::{ time::Duration, }; use theme::ThemeRegistry; -use tokio::sync::RwLockReadGuard; use workspace::{Item, SplitDirection, ToggleFollow, Workspace}; #[ctor::ctor] @@ -589,7 +588,7 @@ async fn test_offline_projects( deterministic.run_until_parked(); assert!(server .store - .read() + .lock() .await .project_metadata_for_user(user_a) .is_empty()); @@ -620,7 +619,7 @@ async fn test_offline_projects( cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); assert!(server .store - .read() + .lock() .await .project_metadata_for_user(user_a) .is_empty()); @@ -1446,7 +1445,7 @@ async fn test_collaborating_with_diagnostics( // Wait for server to see the diagnostics update. deterministic.run_until_parked(); { - let store = server.store.read().await; + let store = server.store.lock().await; let project = store.project(ProjectId::from_proto(project_id)).unwrap(); let worktree = project.worktrees.get(&worktree_id.to_proto()).unwrap(); assert!(!worktree.diagnostic_summaries.is_empty()); @@ -3172,7 +3171,7 @@ async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { assert_eq!( server - .state() + .store() .await .channel(channel_id) .unwrap() @@ -4660,7 +4659,7 @@ async fn test_random_collaboration( .unwrap(); let contacts = server .store - .read() + .lock() .await .build_initial_contacts_update(contacts) .contacts; @@ -4745,7 +4744,7 @@ async fn test_random_collaboration( let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap(); let contacts = server .store - .read() + .lock() .await .build_initial_contacts_update(contacts) .contacts; @@ -5077,10 +5076,6 @@ impl TestServer { }) } - async fn state<'a>(&'a self) -> RwLockReadGuard<'a, Store> { - self.server.store.read().await - } - async fn condition(&mut self, mut predicate: F) where F: FnMut(&Store) -> bool, @@ -5089,7 +5084,7 @@ impl TestServer { self.foreground.parking_forbidden(), "you must call forbid_parking to use server conditions so we don't block indefinitely" ); - while !(predicate)(&*self.server.store.read().await) { + while !(predicate)(&*self.server.store.lock().await) { self.foreground.start_waiting(); self.notifications.next().await; self.foreground.finish_waiting(); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index b3dc965ff3..b7b0e00f2d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -51,7 +51,7 @@ use std::{ }; use time::OffsetDateTime; use tokio::{ - sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}, + sync::{Mutex, MutexGuard}, time::Sleep, }; use tower::ServiceBuilder; @@ -97,7 +97,7 @@ impl Response { pub struct Server { peer: Arc, - pub(crate) store: RwLock, + pub(crate) store: Mutex, app_state: Arc, handlers: HashMap, notifications: Option>, @@ -115,13 +115,8 @@ pub struct RealExecutor; const MESSAGE_COUNT_PER_PAGE: usize = 100; const MAX_MESSAGE_LEN: usize = 1024; -struct StoreReadGuard<'a> { - guard: RwLockReadGuard<'a, Store>, - _not_send: PhantomData>, -} - -struct StoreWriteGuard<'a> { - guard: RwLockWriteGuard<'a, Store>, +pub(crate) struct StoreGuard<'a> { + guard: MutexGuard<'a, Store>, _not_send: PhantomData>, } @@ -129,7 +124,7 @@ struct StoreWriteGuard<'a> { pub struct ServerSnapshot<'a> { peer: &'a Peer, #[serde(serialize_with = "serialize_deref")] - store: RwLockReadGuard<'a, Store>, + store: StoreGuard<'a>, } pub fn serialize_deref(value: &T, serializer: S) -> Result @@ -384,7 +379,7 @@ impl Server { ).await?; { - let mut store = this.store_mut().await; + let mut store = this.store().await; store.add_connection(connection_id, user_id, user.admin); this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?; @@ -471,7 +466,7 @@ impl Server { let mut projects_to_unregister = Vec::new(); let removed_user_id; { - let mut store = self.store_mut().await; + let mut store = self.store().await; let removed_connection = store.remove_connection(connection_id)?; for (project_id, project) in removed_connection.hosted_projects { @@ -605,7 +600,7 @@ impl Server { .await .user_id_for_connection(request.sender_id)?; let project_id = self.app_state.db.register_project(user_id).await?; - self.store_mut() + self.store() .await .register_project(request.sender_id, project_id)?; @@ -623,7 +618,7 @@ impl Server { ) -> Result<()> { let project_id = ProjectId::from_proto(request.payload.project_id); let (user_id, project) = { - let mut state = self.store_mut().await; + let mut state = self.store().await; let project = state.unregister_project(project_id, request.sender_id)?; (state.user_id_for_connection(request.sender_id)?, project) }; @@ -725,7 +720,7 @@ impl Server { return Err(anyhow!("no such project"))?; } - self.store_mut().await.request_join_project( + self.store().await.request_join_project( guest_user_id, project_id, response.into_receipt(), @@ -747,7 +742,7 @@ impl Server { let host_user_id; { - let mut state = self.store_mut().await; + let mut state = self.store().await; let project_id = ProjectId::from_proto(request.payload.project_id); let project = state.project(project_id)?; if project.host_connection_id != request.sender_id { @@ -897,7 +892,7 @@ impl Server { let project_id = ProjectId::from_proto(request.payload.project_id); let project; { - let mut store = self.store_mut().await; + let mut store = self.store().await; project = store.leave_project(sender_id, project_id)?; tracing::info!( %project_id, @@ -948,7 +943,7 @@ impl Server { let project_id = ProjectId::from_proto(request.payload.project_id); let user_id; { - let mut state = self.store_mut().await; + let mut state = self.store().await; user_id = state.user_id_for_connection(request.sender_id)?; let guest_connection_ids = state .read_project(project_id, request.sender_id)? @@ -967,7 +962,7 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> Result<()> { - self.store_mut().await.register_project_activity( + self.store().await.register_project_activity( ProjectId::from_proto(request.payload.project_id), request.sender_id, )?; @@ -982,7 +977,7 @@ impl Server { let project_id = ProjectId::from_proto(request.payload.project_id); let worktree_id = request.payload.worktree_id; let (connection_ids, metadata_changed, extension_counts) = { - let mut store = self.store_mut().await; + let mut store = self.store().await; let (connection_ids, metadata_changed, extension_counts) = store.update_worktree( request.sender_id, project_id, @@ -1024,7 +1019,7 @@ impl Server { .summary .clone() .ok_or_else(|| anyhow!("invalid summary"))?; - let receiver_ids = self.store_mut().await.update_diagnostic_summary( + let receiver_ids = self.store().await.update_diagnostic_summary( ProjectId::from_proto(request.payload.project_id), request.payload.worktree_id, request.sender_id, @@ -1042,7 +1037,7 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> Result<()> { - let receiver_ids = self.store_mut().await.start_language_server( + let receiver_ids = self.store().await.start_language_server( ProjectId::from_proto(request.payload.project_id), request.sender_id, request @@ -1081,20 +1076,23 @@ impl Server { where T: EntityMessage + RequestMessage, { + let project_id = ProjectId::from_proto(request.payload.remote_entity_id()); let host_connection_id = self .store() .await - .read_project( - ProjectId::from_proto(request.payload.remote_entity_id()), - request.sender_id, - )? + .read_project(project_id, request.sender_id)? .host_connection_id; + let payload = self + .peer + .forward_request(request.sender_id, host_connection_id, request.payload) + .await?; - response.send( - self.peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?, - )?; + // Ensure project still exists by the time we get the response from the host. + self.store() + .await + .read_project(project_id, request.sender_id)?; + + response.send(payload)?; Ok(()) } @@ -1135,7 +1133,7 @@ impl Server { ) -> Result<()> { let project_id = ProjectId::from_proto(request.payload.project_id); let receiver_ids = { - let mut store = self.store_mut().await; + let mut store = self.store().await; store.register_project_activity(project_id, request.sender_id)?; store.project_connection_ids(project_id, request.sender_id)? }; @@ -1202,7 +1200,7 @@ impl Server { let leader_id = ConnectionId(request.payload.leader_id); let follower_id = request.sender_id; { - let mut store = self.store_mut().await; + let mut store = self.store().await; if !store .project_connection_ids(project_id, follower_id)? .contains(&leader_id) @@ -1227,7 +1225,7 @@ impl Server { async fn unfollow(self: Arc, request: TypedEnvelope) -> Result<()> { let project_id = ProjectId::from_proto(request.payload.project_id); let leader_id = ConnectionId(request.payload.leader_id); - let mut store = self.store_mut().await; + let mut store = self.store().await; if !store .project_connection_ids(project_id, request.sender_id)? .contains(&leader_id) @@ -1245,7 +1243,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let project_id = ProjectId::from_proto(request.payload.project_id); - let mut store = self.store_mut().await; + let mut store = self.store().await; store.register_project_activity(project_id, request.sender_id)?; let connection_ids = store.project_connection_ids(project_id, request.sender_id)?; let leader_id = request @@ -1503,7 +1501,7 @@ impl Server { Err(anyhow!("access denied"))?; } - self.store_mut() + self.store() .await .join_channel(request.sender_id, channel_id); let messages = self @@ -1545,7 +1543,7 @@ impl Server { Err(anyhow!("access denied"))?; } - self.store_mut() + self.store() .await .leave_channel(request.sender_id, channel_id); @@ -1653,25 +1651,13 @@ impl Server { Ok(()) } - async fn store<'a>(self: &'a Arc) -> StoreReadGuard<'a> { + pub(crate) async fn store<'a>(&'a self) -> StoreGuard<'a> { #[cfg(test)] tokio::task::yield_now().await; - let guard = self.store.read().await; + let guard = self.store.lock().await; #[cfg(test)] tokio::task::yield_now().await; - StoreReadGuard { - guard, - _not_send: PhantomData, - } - } - - async fn store_mut<'a>(self: &'a Arc) -> StoreWriteGuard<'a> { - #[cfg(test)] - tokio::task::yield_now().await; - let guard = self.store.write().await; - #[cfg(test)] - tokio::task::yield_now().await; - StoreWriteGuard { + StoreGuard { guard, _not_send: PhantomData, } @@ -1679,13 +1665,13 @@ impl Server { pub async fn snapshot<'a>(self: &'a Arc) -> ServerSnapshot<'a> { ServerSnapshot { - store: self.store.read().await, + store: self.store().await, peer: &self.peer, } } } -impl<'a> Deref for StoreReadGuard<'a> { +impl<'a> Deref for StoreGuard<'a> { type Target = Store; fn deref(&self) -> &Self::Target { @@ -1693,21 +1679,13 @@ impl<'a> Deref for StoreReadGuard<'a> { } } -impl<'a> Deref for StoreWriteGuard<'a> { - type Target = Store; - - fn deref(&self) -> &Self::Target { - &*self.guard - } -} - -impl<'a> DerefMut for StoreWriteGuard<'a> { +impl<'a> DerefMut for StoreGuard<'a> { fn deref_mut(&mut self) -> &mut Self::Target { &mut *self.guard } } -impl<'a> Drop for StoreWriteGuard<'a> { +impl<'a> Drop for StoreGuard<'a> { fn drop(&mut self) { #[cfg(test)] self.check_invariants(); From 4d3c7083870a215ff19cae36c4a276ac76f2ea0e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Jul 2022 12:05:21 +0200 Subject: [PATCH 51/60] Add `simulate_random_delay` to every implemented method in `FakeDb` --- crates/collab/src/db.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 6c40996ae4..96c93eb934 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -2181,6 +2181,7 @@ pub mod tests { } async fn get_user_by_id(&self, id: UserId) -> Result> { + self.background.simulate_random_delay().await; Ok(self.get_users_by_ids(vec![id]).await?.into_iter().next()) } @@ -2195,6 +2196,7 @@ pub mod tests { } async fn get_user_by_github_login(&self, github_login: &str) -> Result> { + self.background.simulate_random_delay().await; Ok(self .users .lock() @@ -2228,6 +2230,7 @@ pub mod tests { } async fn get_invite_code_for_user(&self, _id: UserId) -> Result> { + self.background.simulate_random_delay().await; Ok(None) } @@ -2265,6 +2268,7 @@ pub mod tests { } async fn unregister_project(&self, project_id: ProjectId) -> Result<()> { + self.background.simulate_random_delay().await; self.projects .lock() .get_mut(&project_id) @@ -2370,6 +2374,7 @@ pub mod tests { requester_id: UserId, responder_id: UserId, ) -> Result<()> { + self.background.simulate_random_delay().await; let mut contacts = self.contacts.lock(); for contact in contacts.iter_mut() { if contact.requester_id == requester_id && contact.responder_id == responder_id { @@ -2399,6 +2404,7 @@ pub mod tests { } async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> { + self.background.simulate_random_delay().await; self.contacts.lock().retain(|contact| { !(contact.requester_id == requester_id && contact.responder_id == responder_id) }); @@ -2410,6 +2416,7 @@ pub mod tests { user_id: UserId, contact_user_id: UserId, ) -> Result<()> { + self.background.simulate_random_delay().await; let mut contacts = self.contacts.lock(); for contact in contacts.iter_mut() { if contact.requester_id == contact_user_id @@ -2436,6 +2443,7 @@ pub mod tests { requester_id: UserId, accept: bool, ) -> Result<()> { + self.background.simulate_random_delay().await; let mut contacts = self.contacts.lock(); for (ix, contact) in contacts.iter_mut().enumerate() { if contact.requester_id == requester_id && contact.responder_id == responder_id { @@ -2631,6 +2639,7 @@ pub mod tests { count: usize, before_id: Option, ) -> Result> { + self.background.simulate_random_delay().await; let mut messages = self .channel_messages .lock() From 833aa726d54a67f1b38117e4e92f4ef11dd43c49 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Jul 2022 14:11:21 +0200 Subject: [PATCH 52/60] :art: --- crates/project/src/worktree.rs | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index af7cf02cb8..97ba52a763 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -410,10 +410,7 @@ impl LocalWorktree { while let Some(scan_state) = scan_states_rx.next().await { if let Some(this) = this.upgrade(&cx) { last_scan_state_tx.blocking_send(scan_state).ok(); - this.update(&mut cx, |this, cx| { - this.poll_snapshot(cx); - this.as_local_mut().unwrap().broadcast_snapshot() - }); + this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); } else { break; } @@ -500,8 +497,11 @@ impl LocalWorktree { fn poll_snapshot(&mut self, cx: &mut ModelContext) { match self.scan_state() { ScanState::Idle => { - self.snapshot = self.background_snapshot.lock().clone(); self.poll_task.take(); + self.snapshot = self.background_snapshot.lock().clone(); + if let Some(share) = self.share.as_mut() { + *share.snapshots_tx.borrow_mut() = self.snapshot.clone(); + } cx.emit(Event::UpdatedEntries); } ScanState::Initializing => { @@ -677,7 +677,6 @@ impl LocalWorktree { snapshot.delete_entry(entry_id); } this.poll_snapshot(cx); - this.broadcast_snapshot(); }); Ok(()) })) @@ -717,7 +716,6 @@ impl LocalWorktree { this.update(&mut cx, |this, cx| { let this = this.as_local_mut().unwrap(); this.poll_snapshot(cx); - this.broadcast_snapshot(); }); Ok(entry) })) @@ -757,7 +755,6 @@ impl LocalWorktree { this.update(&mut cx, |this, cx| { let this = this.as_local_mut().unwrap(); this.poll_snapshot(cx); - this.broadcast_snapshot() }); Ok(entry) })) @@ -795,7 +792,6 @@ impl LocalWorktree { this.update(&mut cx, |this, cx| { let this = this.as_local_mut().unwrap(); this.poll_snapshot(cx); - this.broadcast_snapshot(); }); Ok(entry) }) @@ -844,7 +840,6 @@ impl LocalWorktree { snapshot.scan_id += 1; } this.poll_snapshot(cx); - this.broadcast_snapshot(); Ok(inserted_entry) }) }) @@ -936,15 +931,6 @@ impl LocalWorktree { pub fn is_shared(&self) -> bool { self.share.is_some() } - - fn broadcast_snapshot(&mut self) { - if matches!(self.scan_state(), ScanState::Idle) { - let snapshot = self.snapshot(); - if let Some(share) = self.share.as_mut() { - *share.snapshots_tx.borrow_mut() = snapshot; - } - } - } } impl RemoteWorktree { From a42399bcf33b9f06fcc163aeae1aedcde7600416 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Jul 2022 14:36:51 +0200 Subject: [PATCH 53/60] Grab latest snapshot when invoking `LocalWorktree::poll_snapshot` --- crates/project/src/worktree.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 97ba52a763..04c8cc4ad7 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -495,10 +495,10 @@ impl LocalWorktree { } fn poll_snapshot(&mut self, cx: &mut ModelContext) { + self.poll_task.take(); + self.snapshot = self.background_snapshot.lock().clone(); match self.scan_state() { ScanState::Idle => { - self.poll_task.take(); - self.snapshot = self.background_snapshot.lock().clone(); if let Some(share) = self.share.as_mut() { *share.snapshots_tx.borrow_mut() = self.snapshot.clone(); } @@ -516,10 +516,7 @@ impl LocalWorktree { smol::Timer::after(Duration::from_millis(100)).await; } if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.as_local_mut().unwrap().poll_task = None; - this.poll_snapshot(cx); - }); + this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); } })); } From cf05738f68cdb1fbdc19526e80d166e6afb12554 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Jul 2022 14:40:39 +0200 Subject: [PATCH 54/60] Remove redundant calls to `poll_snapshot` --- crates/project/src/worktree.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 04c8cc4ad7..949bc1bdc4 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -569,7 +569,6 @@ impl LocalWorktree { .refresh_entry(path, abs_path, None, cx) }) .await?; - this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); Ok(( File { entry_id: Some(entry.id), @@ -710,10 +709,6 @@ impl LocalWorktree { ) }) .await?; - this.update(&mut cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - this.poll_snapshot(cx); - }); Ok(entry) })) } @@ -749,10 +744,6 @@ impl LocalWorktree { ) }) .await?; - this.update(&mut cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - this.poll_snapshot(cx); - }); Ok(entry) })) } @@ -786,10 +777,6 @@ impl LocalWorktree { .refresh_entry(path, abs_path, None, cx) }) .await?; - this.update(&mut cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - this.poll_snapshot(cx); - }); Ok(entry) }) } From 8e4c54ab61cc763d19aa0cc71066da98bf27f22e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 1 Jul 2022 11:38:12 -0700 Subject: [PATCH 55/60] Checkpointing after some debugging --- crates/terminal/src/terminal.rs | 1 + crates/terminal/src/terminal_element.rs | 29 +++++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 134cf5be6e..2c0edb1d31 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -484,6 +484,7 @@ mod tests { let (chunks, _) = build_chunks( term.lock().renderable_content().display_iter, &Default::default(), + 0., ); let content = chunks.iter().map(|e| e.0.trim()).collect::(); content.contains("7") diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 697a0abed9..082296e751 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -1,6 +1,6 @@ use alacritty_terminal::{ ansi::Color as AnsiColor, - grid::{GridIterator, Indexed}, + grid::{Dimensions, GridIterator, Indexed}, index::Point, term::{ cell::{Cell, Flags}, @@ -121,8 +121,13 @@ impl Element for TerminalEl { let term = view_handle.read(cx).term.lock(); let content = term.renderable_content(); + //TODO: Remove + // dbg!("*******"); + // dbg!(cur_size.columns()); + //And we're off! Begin layouting - let (chunks, line_count) = build_chunks(content.display_iter, &terminal_theme); + let (chunks, line_count) = + build_chunks(content.display_iter, &terminal_theme, cell_width.0); let shaped_lines = layout_highlighted_chunks( chunks @@ -135,6 +140,11 @@ impl Element for TerminalEl { line_count, ); + //TODO: Remove + // for shaped_line in &shaped_lines { + // dbg!(shaped_line.width()); + // } + let backgrounds = chunks .iter() .filter(|(_, _, line_span)| line_span != &RectSpan::default()) @@ -290,6 +300,7 @@ impl Element for TerminalEl { } } +///Configures a text style from the current settings. fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { TextStyle { color: settings.theme.editor.text_color, @@ -304,6 +315,7 @@ fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { } } +///Configures a size info object from the given information. fn make_new_size( constraint: SizeConstraint, cell_width: &CellWidth, @@ -324,6 +336,7 @@ fn make_new_size( pub(crate) fn build_chunks( grid_iterator: GridIterator, theme: &TerminalStyle, + em_width: f32, ) -> (Vec<(String, Option, RectSpan)>, usize) { let mut line_count: usize = 0; //Every `group_by()` -> `into_iter()` pair needs to be seperated by a local variable so @@ -356,8 +369,16 @@ pub(crate) fn build_chunks( .chain(iter::once(("\n".to_string(), None, Default::default()))) .collect::, RectSpan)>>() }) - //We have a Vec> (Vec of lines of styled chunks), flatten to just Vec<> (the styled chunks) + //TODO: Remove + // .inspect(|line_chunks| { + // let mut line_len = 0; + // for chunk in line_chunks { + // line_len += chunk.0.len(); + // } + // dbg!((line_len, line_len as f32 * em_width)); + // }) .flatten() + //We have a Vec> (Vec of lines of styled chunks), flatten to just Vec<> (the styled chunks) .collect::, RectSpan)>>(); (result, line_count) } @@ -398,7 +419,7 @@ fn get_cursor_position( ) -> Option { let cursor_line = cursor_point.line.0 as usize + display_offset; shaped_lines.get(cursor_line).map(|layout_line| { - let cursor_x = layout_line.x_for_index(cursor_point.column.0); + let cursor_x = layout_line.x_for_index(cursor_point.column.0 + 3); vec2f(cursor_x, cursor_line as f32 * line_height.0) }) } From ce60a9a50a6b321a6bef97e859e56fa39bd2f4de Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 1 Jul 2022 11:39:43 -0700 Subject: [PATCH 56/60] Cleaned up debugging code --- crates/terminal/src/terminal.rs | 1 - crates/terminal/src/terminal_element.rs | 33 ++----------------------- 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 2c0edb1d31..134cf5be6e 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -484,7 +484,6 @@ mod tests { let (chunks, _) = build_chunks( term.lock().renderable_content().display_iter, &Default::default(), - 0., ); let content = chunks.iter().map(|e| e.0.trim()).collect::(); content.contains("7") diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 082296e751..1df95c3b96 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -1,6 +1,6 @@ use alacritty_terminal::{ ansi::Color as AnsiColor, - grid::{Dimensions, GridIterator, Indexed}, + grid::{GridIterator, Indexed}, index::Point, term::{ cell::{Cell, Flags}, @@ -121,13 +121,8 @@ impl Element for TerminalEl { let term = view_handle.read(cx).term.lock(); let content = term.renderable_content(); - //TODO: Remove - // dbg!("*******"); - // dbg!(cur_size.columns()); - //And we're off! Begin layouting - let (chunks, line_count) = - build_chunks(content.display_iter, &terminal_theme, cell_width.0); + let (chunks, line_count) = build_chunks(content.display_iter, &terminal_theme); let shaped_lines = layout_highlighted_chunks( chunks @@ -140,11 +135,6 @@ impl Element for TerminalEl { line_count, ); - //TODO: Remove - // for shaped_line in &shaped_lines { - // dbg!(shaped_line.width()); - // } - let backgrounds = chunks .iter() .filter(|(_, _, line_span)| line_span != &RectSpan::default()) @@ -192,16 +182,6 @@ impl Element for TerminalEl { ..Default::default() }); - //TODO: Implement cursor region based styling - // cx.scene.push_cursor_region(CursorRegion { - // bounds, - // style: if !view.link_go_to_definition_state.definitions.is_empty() { - // CursorStyle::PointingHand - // } else { - // CursorStyle::IBeam - // }, - // }); - let origin = bounds.origin() + vec2f(layout.em_width.0, 0.); //Start us off with a nice simple background color @@ -336,7 +316,6 @@ fn make_new_size( pub(crate) fn build_chunks( grid_iterator: GridIterator, theme: &TerminalStyle, - em_width: f32, ) -> (Vec<(String, Option, RectSpan)>, usize) { let mut line_count: usize = 0; //Every `group_by()` -> `into_iter()` pair needs to be seperated by a local variable so @@ -369,14 +348,6 @@ pub(crate) fn build_chunks( .chain(iter::once(("\n".to_string(), None, Default::default()))) .collect::, RectSpan)>>() }) - //TODO: Remove - // .inspect(|line_chunks| { - // let mut line_len = 0; - // for chunk in line_chunks { - // line_len += chunk.0.len(); - // } - // dbg!((line_len, line_len as f32 * em_width)); - // }) .flatten() //We have a Vec> (Vec of lines of styled chunks), flatten to just Vec<> (the styled chunks) .collect::, RectSpan)>>(); From f4ac694ad8366243f26169c28738ec433bf80a92 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 1 Jul 2022 11:48:50 -0700 Subject: [PATCH 57/60] Fixed debug offset I added to terminal --- crates/terminal/src/terminal_element.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 1df95c3b96..c3918c21a7 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -119,6 +119,9 @@ impl Element for TerminalEl { //Now that we're done with the mutable portion, grab the immutable settings and view again let terminal_theme = &(cx.global::()).theme.terminal; let term = view_handle.read(cx).term.lock(); + + dbg!(term.grid()); + let content = term.renderable_content(); //And we're off! Begin layouting @@ -390,7 +393,7 @@ fn get_cursor_position( ) -> Option { let cursor_line = cursor_point.line.0 as usize + display_offset; shaped_lines.get(cursor_line).map(|layout_line| { - let cursor_x = layout_line.x_for_index(cursor_point.column.0 + 3); + let cursor_x = layout_line.x_for_index(cursor_point.column.0); vec2f(cursor_x, cursor_line as f32 * line_height.0) }) } From 62939322d377ac7df77fc675d2fe766cde2fe5cd Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 1 Jul 2022 13:03:26 -0700 Subject: [PATCH 58/60] rendering cursor correctly --- crates/editor/src/element.rs | 2 +- crates/gpui/src/text_layout.rs | 2 +- crates/terminal/src/terminal_element.rs | 40 ++++++++++++++++++++----- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 348ce57ef3..778f42f094 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -490,7 +490,7 @@ impl EditorElement { } let block_text = - if matches!(self.cursor_shape, CursorShape::Block) { + if let CursorShape::Block = self.cursor_shape { layout.snapshot.chars_at(cursor_position).next().and_then( |character| { let font_id = diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index 2d8672aab3..50f16cb995 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -164,7 +164,7 @@ impl<'a> Hash for CacheKeyRef<'a> { } } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub struct Line { layout: Arc, style_runs: SmallVec<[(u32, Color, Underline); 32]>, diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index c3918c21a7..e389261a63 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -17,7 +17,7 @@ use gpui::{ vector::{vec2f, Vector2F}, }, json::json, - text_layout::Line, + text_layout::{Line, RunStyle}, Event, FontCache, MouseRegion, PaintContext, Quad, SizeConstraint, WeakViewHandle, }; use itertools::Itertools; @@ -78,7 +78,7 @@ pub struct LayoutState { lines: Vec, line_height: LineHeight, em_width: CellWidth, - cursor: Option<(Vector2F, Color)>, + cursor: Option<(Vector2F, Color, Option)>, cur_size: SizeInfo, background_color: Color, background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan @@ -120,7 +120,13 @@ impl Element for TerminalEl { let terminal_theme = &(cx.global::()).theme.terminal; let term = view_handle.read(cx).term.lock(); - dbg!(term.grid()); + // let cursor_char = term.grid().cursor_cell().c.to_string(); + + let cursor_text = { + let grid = term.grid(); + let cursor_point = grid.cursor.point; + grid[cursor_point.line][cursor_point.column].c.to_string() + }; let content = term.renderable_content(); @@ -145,13 +151,25 @@ impl Element for TerminalEl { .collect(); let background_rects = make_background_rects(backgrounds, &shaped_lines, &line_height); + let block_text = cx.text_layout_cache.layout_str( + &cursor_text, + text_style.font_size, + &[( + cursor_text.len(), + RunStyle { + font_id: text_style.font_id, + color: terminal_theme.background, + underline: Default::default(), + }, + )], + ); let cursor = get_cursor_position( content.cursor.point, &shaped_lines, content.display_offset, &line_height, ) - .map(|cursor_rect| (cursor_rect, terminal_theme.cursor)); + .map(|cursor_rect| (cursor_rect, terminal_theme.cursor, Some(block_text))); ( constraint.max, @@ -188,6 +206,7 @@ impl Element for TerminalEl { let origin = bounds.origin() + vec2f(layout.em_width.0, 0.); //Start us off with a nice simple background color + cx.scene.push_layer(Some(visible_bounds)); cx.scene.push_quad(Quad { bounds: RectF::new(bounds.origin(), bounds.size()), background: Some(layout.background_color), @@ -205,8 +224,10 @@ impl Element for TerminalEl { corner_radius: 0., }) } + cx.scene.pop_layer(); //Draw text + cx.scene.push_layer(Some(visible_bounds)); let mut line_origin = origin.clone(); for line in &layout.lines { let boundaries = RectF::new(line_origin, vec2f(bounds.width(), layout.line_height.0)); @@ -215,20 +236,23 @@ impl Element for TerminalEl { } line_origin.set_y(boundaries.max_y()); } + cx.scene.pop_layer(); //Draw cursor - if let Some((c, color)) = layout.cursor { + cx.scene.push_layer(Some(visible_bounds)); + if let Some((c, color, block_text)) = &layout.cursor { let editor_cursor = Cursor::new( - origin + c, + origin + *c, layout.em_width.0, layout.line_height.0, - color, + *color, CursorShape::Block, - None, //TODO fix this + block_text.clone(), //TODO fix this ); editor_cursor.paint(cx); } + cx.scene.pop_layer(); #[cfg(debug_assertions)] if DEBUG_GRID { From 6ac5cc0d2a14b2f532e68f547b2db55df864722b Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 1 Jul 2022 14:53:19 -0700 Subject: [PATCH 59/60] Fixed cursor positioning bugs in multi-byte charcters. Still have at least one though :/ --- crates/editor/src/element.rs | 20 ++--- crates/terminal/src/terminal.rs | 5 +- crates/terminal/src/terminal_element.rs | 107 ++++++++++++++++-------- 3 files changed, 86 insertions(+), 46 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 778f42f094..faa821f20d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -520,7 +520,7 @@ impl EditorElement { cursors.push(Cursor { color: selection_style.cursor, block_width, - origin: content_origin + vec2f(x, y), + origin: vec2f(x, y), line_height: layout.line_height, shape: self.cursor_shape, block_text, @@ -546,13 +546,12 @@ impl EditorElement { cx.scene.push_layer(Some(bounds)); for cursor in cursors { - cursor.paint(cx); + cursor.paint(content_origin, cx); } cx.scene.pop_layer(); if let Some((position, context_menu)) = layout.context_menu.as_mut() { cx.scene.push_stacking_context(None); - let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize]; let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left; let y = (position.row() + 1) as f32 * layout.line_height - scroll_top; @@ -1658,14 +1657,15 @@ impl Cursor { } } - pub fn paint(&self, cx: &mut PaintContext) { + pub fn paint(&self, origin: Vector2F, cx: &mut PaintContext) { let bounds = match self.shape { - CursorShape::Bar => RectF::new(self.origin, vec2f(2.0, self.line_height)), - CursorShape::Block => { - RectF::new(self.origin, vec2f(self.block_width, self.line_height)) - } + CursorShape::Bar => RectF::new(self.origin + origin, vec2f(2.0, self.line_height)), + CursorShape::Block => RectF::new( + self.origin + origin, + vec2f(self.block_width, self.line_height), + ), CursorShape::Underscore => RectF::new( - self.origin + Vector2F::new(0.0, self.line_height - 2.0), + self.origin + origin + Vector2F::new(0.0, self.line_height - 2.0), vec2f(self.block_width, 2.0), ), }; @@ -1678,7 +1678,7 @@ impl Cursor { }); if let Some(block_text) = &self.block_text { - block_text.paint(self.origin, bounds, self.line_height, cx); + block_text.paint(self.origin + origin, bounds, self.line_height, cx); } } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 134cf5be6e..6ea4ca5f73 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -464,7 +464,7 @@ fn to_alac_rgb(color: Color) -> AlacRgb { #[cfg(test)] mod tests { use super::*; - use crate::terminal_element::build_chunks; + use crate::terminal_element::{build_chunks, BuiltChunks}; use gpui::TestAppContext; ///Basic integration test, can we get the terminal to show up, execute a command, @@ -481,9 +481,10 @@ mod tests { terminal .condition(cx, |terminal, _cx| { let term = terminal.term.clone(); - let (chunks, _) = build_chunks( + let BuiltChunks { chunks, .. } = build_chunks( term.lock().renderable_content().display_iter, &Default::default(), + Default::default(), ); let content = chunks.iter().map(|e| e.0.trim()).collect::(); content.contains("7") diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index e389261a63..42d4386fa6 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -78,7 +78,7 @@ pub struct LayoutState { lines: Vec, line_height: LineHeight, em_width: CellWidth, - cursor: Option<(Vector2F, Color, Option)>, + cursor: Option, cur_size: SizeInfo, background_color: Color, background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan @@ -120,18 +120,18 @@ impl Element for TerminalEl { let terminal_theme = &(cx.global::()).theme.terminal; let term = view_handle.read(cx).term.lock(); - // let cursor_char = term.grid().cursor_cell().c.to_string(); - - let cursor_text = { - let grid = term.grid(); - let cursor_point = grid.cursor.point; - grid[cursor_point.line][cursor_point.column].c.to_string() - }; + let grid = term.grid(); + let cursor_point = grid.cursor.point; + let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string(); let content = term.renderable_content(); //And we're off! Begin layouting - let (chunks, line_count) = build_chunks(content.display_iter, &terminal_theme); + let BuiltChunks { + chunks, + line_count, + cursor_index, + } = build_chunks(content.display_iter, &terminal_theme, cursor_point); let shaped_lines = layout_highlighted_chunks( chunks @@ -163,13 +163,30 @@ impl Element for TerminalEl { }, )], ); + let cursor = get_cursor_position( - content.cursor.point, + content.cursor.point.line.0 as usize, + cursor_index, &shaped_lines, content.display_offset, &line_height, ) - .map(|cursor_rect| (cursor_rect, terminal_theme.cursor, Some(block_text))); + .map(move |(cursor_position, block_width)| { + let block_width = if block_width != 0.0 { + block_width + } else { + cell_width.0 + }; + + Cursor::new( + cursor_position, + block_width, + line_height.0, + terminal_theme.cursor, + CursorShape::Block, + Some(block_text.clone()), + ) + }); ( constraint.max, @@ -239,20 +256,11 @@ impl Element for TerminalEl { cx.scene.pop_layer(); //Draw cursor - cx.scene.push_layer(Some(visible_bounds)); - if let Some((c, color, block_text)) = &layout.cursor { - let editor_cursor = Cursor::new( - origin + *c, - layout.em_width.0, - layout.line_height.0, - *color, - CursorShape::Block, - block_text.clone(), //TODO fix this - ); - - editor_cursor.paint(cx); + if let Some(cursor) = &layout.cursor { + cx.scene.push_layer(Some(visible_bounds)); + cursor.paint(origin, cx); + cx.scene.pop_layer(); } - cx.scene.pop_layer(); #[cfg(debug_assertions)] if DEBUG_GRID { @@ -339,21 +347,30 @@ fn make_new_size( ) } +pub struct BuiltChunks { + pub chunks: Vec<(String, Option, RectSpan)>, + pub line_count: usize, + pub cursor_index: usize, +} + ///In a single pass, this function generates the background and foreground color info for every item in the grid. pub(crate) fn build_chunks( grid_iterator: GridIterator, theme: &TerminalStyle, -) -> (Vec<(String, Option, RectSpan)>, usize) { + cursor_point: Point, +) -> BuiltChunks { let mut line_count: usize = 0; + let mut cursor_index: usize = 0; //Every `group_by()` -> `into_iter()` pair needs to be seperated by a local variable so //rust knows where to put everything. //Start by grouping by lines let lines = grid_iterator.group_by(|i| i.point.line.0); let result = lines .into_iter() - .map(|(_, line)| { + .map(|(_line_grid_index, line)| { line_count += 1; let mut col_index = 0; + //Setup a variable //Then group by style let chunks = line.group_by(|i| cell_style(&i, theme)); @@ -361,9 +378,20 @@ pub(crate) fn build_chunks( .into_iter() .map(|(style, fragment)| { //And assemble the styled fragment into it's background and foreground information - let str_fragment = fragment.map(|indexed| indexed.c).collect::(); + let mut str_fragment = String::new(); + for indexed_cell in fragment { + if cursor_point.line.0 == indexed_cell.point.line.0 + && indexed_cell.point.column < cursor_point.column.0 + { + cursor_index += indexed_cell.c.to_string().len(); + } + str_fragment.push(indexed_cell.c); + } + let start = col_index; let end = start + str_fragment.len() as i32; + + //munge it here col_index = end; ( str_fragment, @@ -378,7 +406,12 @@ pub(crate) fn build_chunks( .flatten() //We have a Vec> (Vec of lines of styled chunks), flatten to just Vec<> (the styled chunks) .collect::, RectSpan)>>(); - (result, line_count) + + BuiltChunks { + chunks: result, + line_count, + cursor_index, + } } ///Convert a RectSpan in terms of character offsets, into RectFs of exact offsets @@ -408,17 +441,23 @@ fn make_background_rects( .collect::>() } -///Create the rectangle for a cursor, exactly positioned according to the text +// Compute the cursor position and expected block width, may return a zero width if x_for_index returns +// the same position for sequential indexes. Use em_width instead fn get_cursor_position( - cursor_point: Point, + line: usize, + line_index: usize, shaped_lines: &Vec, display_offset: usize, line_height: &LineHeight, -) -> Option { - let cursor_line = cursor_point.line.0 as usize + display_offset; +) -> Option<(Vector2F, f32)> { + let cursor_line = line + display_offset; shaped_lines.get(cursor_line).map(|layout_line| { - let cursor_x = layout_line.x_for_index(cursor_point.column.0); - vec2f(cursor_x, cursor_line as f32 * line_height.0) + let cursor_x = layout_line.x_for_index(line_index); + let next_char_x = layout_line.x_for_index(line_index + 1); + ( + vec2f(cursor_x, cursor_line as f32 * line_height.0), + next_char_x - cursor_x, + ) }) } From aca757a02d46d7721124f89abfbd0ab3a8203600 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 4 Jul 2022 08:10:31 +0200 Subject: [PATCH 60/60] Don't poll snapshot if processing events unless user manually changed fs --- crates/project/src/worktree.rs | 43 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 949bc1bdc4..435cf59057 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -334,7 +334,7 @@ impl Worktree { fn poll_snapshot(&mut self, cx: &mut ModelContext) { match self { - Self::Local(worktree) => worktree.poll_snapshot(cx), + Self::Local(worktree) => worktree.poll_snapshot(false, cx), Self::Remote(worktree) => worktree.poll_snapshot(cx), }; } @@ -494,36 +494,37 @@ impl LocalWorktree { Ok(updated) } - fn poll_snapshot(&mut self, cx: &mut ModelContext) { + fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext) { self.poll_task.take(); - self.snapshot = self.background_snapshot.lock().clone(); match self.scan_state() { ScanState::Idle => { + self.snapshot = self.background_snapshot.lock().clone(); if let Some(share) = self.share.as_mut() { *share.snapshots_tx.borrow_mut() = self.snapshot.clone(); } cx.emit(Event::UpdatedEntries); } ScanState::Initializing => { + let is_fake_fs = self.fs.is_fake(); self.snapshot = self.background_snapshot.lock().clone(); - if self.poll_task.is_none() { - let is_fake_fs = self.fs.is_fake(); - self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move { - if is_fake_fs { - #[cfg(any(test, feature = "test-support"))] - cx.background().simulate_random_delay().await; - } else { - smol::Timer::after(Duration::from_millis(100)).await; - } - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); - } - })); - } + self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move { + if is_fake_fs { + #[cfg(any(test, feature = "test-support"))] + cx.background().simulate_random_delay().await; + } else { + smol::Timer::after(Duration::from_millis(100)).await; + } + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); + } + })); cx.emit(Event::UpdatedEntries); } - ScanState::Updating => {} - ScanState::Err(_) => {} + _ => { + if force { + self.snapshot = self.background_snapshot.lock().clone(); + } + } } cx.notify(); } @@ -672,7 +673,7 @@ impl LocalWorktree { let mut snapshot = this.background_snapshot.lock(); snapshot.delete_entry(entry_id); } - this.poll_snapshot(cx); + this.poll_snapshot(true, cx); }); Ok(()) })) @@ -823,7 +824,7 @@ impl LocalWorktree { inserted_entry = snapshot.insert_entry(entry, fs.as_ref()); snapshot.scan_id += 1; } - this.poll_snapshot(cx); + this.poll_snapshot(true, cx); Ok(inserted_entry) }) })