Merge branch 'main' into new-signup-flow

Max Brunsfeld created

Change summary

Cargo.lock                                         |   5 
assets/keymaps/default.json                        |  45 +
assets/settings/default.json                       |  13 
crates/collab/src/integration_tests.rs             |  20 
crates/editor/src/editor.rs                        | 144 ++++
crates/editor/src/items.rs                         |  13 
crates/gpui/src/elements/flex.rs                   | 122 ++--
crates/gpui/src/elements/list.rs                   |  39 
crates/gpui/src/elements/mouse_event_handler.rs    |  11 
crates/gpui/src/elements/overlay.rs                |  29 
crates/gpui/src/elements/tooltip.rs                |   8 
crates/gpui/src/elements/uniform_list.rs           |  47 +
crates/project/src/project.rs                      |  41 +
crates/rpc/proto/zed.proto                         |   8 
crates/rpc/src/rpc.rs                              |   2 
crates/settings/src/settings.rs                    |  22 
crates/terminal/Cargo.toml                         |   5 
crates/terminal/src/mappings/mouse.rs              |   6 
crates/terminal/src/terminal.rs                    | 456 ++++++++++++---
crates/terminal/src/terminal_container_view.rs     |   5 
crates/terminal/src/terminal_element.rs            | 155 ++++-
crates/terminal/src/terminal_view.rs               |  73 +-
crates/terminal/src/tests/terminal_test_context.rs |  40 +
crates/zed/Cargo.toml                              |   2 
crates/zed/src/languages/json.rs                   |  88 +-
script/bundle                                      |   1 
26 files changed, 1,032 insertions(+), 368 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5500,11 +5500,14 @@ dependencies = [
  "futures",
  "gpui",
  "itertools",
+ "lazy_static",
  "libc",
  "mio-extras",
  "ordered-float",
  "procinfo",
  "project",
+ "rand 0.8.5",
+ "serde",
  "settings",
  "shellexpand",
  "smallvec",
@@ -7148,7 +7151,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.53.1"
+version = "0.55.0"
 dependencies = [
  "activity_indicator",
  "anyhow",

assets/keymaps/default.json 🔗

@@ -93,6 +93,7 @@
             "cmd-shift-down": "editor::SelectToEnd",
             "cmd-a": "editor::SelectAll",
             "cmd-l": "editor::SelectLine",
+            "cmd-shift-i": "editor::Format",
             "cmd-shift-left": [
                 "editor::SelectToBeginningOfLine",
                 {
@@ -427,17 +428,45 @@
     {
         "context": "Terminal",
         "bindings": {
-            // Overrides for global bindings, remove at your own risk:
-            "up": "terminal::Up",
-            "down": "terminal::Down",
-            "escape": "terminal::Escape",
-            "enter": "terminal::Enter",
-            "ctrl-c": "terminal::CtrlC",
-            // Useful terminal actions:
             "ctrl-cmd-space": "terminal::ShowCharacterPalette",
             "cmd-c": "terminal::Copy",
             "cmd-v": "terminal::Paste",
-            "cmd-k": "terminal::Clear"
+            "cmd-k": "terminal::Clear",
+            // Some nice conveniences
+            "cmd-backspace": [
+                "terminal::SendText",
+                "\u0015"
+            ],
+            "cmd-right": [
+                "terminal::SendText",
+                "\u0005"
+            ],
+            "cmd-left": [
+                "terminal::SendText",
+                "\u0001"
+            ],
+            // There are conflicting bindings for these keys in the global context.
+            // these bindings override them, remove at your own risk:
+            "up": [
+                "terminal::SendKeystroke",
+                "up"
+            ],
+            "down": [
+                "terminal::SendKeystroke",
+                "down"
+            ],
+            "escape": [
+                "terminal::SendKeystroke",
+                "escape"
+            ],
+            "enter": [
+                "terminal::SendKeystroke",
+                "enter"
+            ],
+            "ctrl-c": [
+                "terminal::SendKeystroke",
+                "ctrl-c"
+            ]
         }
     }
 ]

assets/settings/default.json 🔗

@@ -42,21 +42,20 @@
     // 3. Position the dock full screen over the entire workspace"
     //     "default_dock_anchor": "expanded"
     "default_dock_anchor": "right",
-    // How to auto-format modified buffers when saving them. This
-    // setting can take three values:
+    // Whether or not to perform a buffer format before saving
+    "format_on_save": "on",
+    // How to perform a buffer format. This setting can take two values:
     //
-    // 1. Don't format code
-    //     "format_on_save": "off"
-    // 2. Format code using the current language server:
+    // 1. Format code using the current language server:
     //     "format_on_save": "language_server"
-    // 3. Format code using an external command:
+    // 2. Format code using an external command:
     //     "format_on_save": {
     //       "external": {
     //         "command": "prettier",
     //         "arguments": ["--stdin-filepath", "{buffer_path}"]
     //       }
     //     }
-    "format_on_save": "language_server",
+    "formatter": "language_server",
     // How to soft-wrap long lines of text. This setting can take
     // three values:
     //

crates/collab/src/integration_tests.rs 🔗

@@ -36,7 +36,7 @@ use project::{
 use rand::prelude::*;
 use rpc::PeerId;
 use serde_json::json;
-use settings::{FormatOnSave, Settings};
+use settings::{Formatter, Settings};
 use sqlx::types::time::OffsetDateTime;
 use std::{
     cell::RefCell,
@@ -1990,6 +1990,8 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te
 
 #[gpui::test(iterations = 10)]
 async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    use project::FormatTrigger;
+
     let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
@@ -2042,7 +2044,12 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
 
     project_b
         .update(cx_b, |project, cx| {
-            project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
+            project.format(
+                HashSet::from_iter([buffer_b.clone()]),
+                true,
+                FormatTrigger::Save,
+                cx,
+            )
         })
         .await
         .unwrap();
@@ -2055,7 +2062,7 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
     // host's configuration is honored as opposed to using the guest's settings.
     cx_a.update(|cx| {
         cx.update_global(|settings: &mut Settings, _| {
-            settings.editor_defaults.format_on_save = Some(FormatOnSave::External {
+            settings.editor_defaults.formatter = Some(Formatter::External {
                 command: "awk".to_string(),
                 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
             });
@@ -2063,7 +2070,12 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
     });
     project_b
         .update(cx_b, |project, cx| {
-            project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
+            project.format(
+                HashSet::from_iter([buffer_b.clone()]),
+                true,
+                FormatTrigger::Save,
+                cx,
+            )
         })
         .await
         .unwrap();

crates/editor/src/editor.rs 🔗

@@ -19,6 +19,7 @@ use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
 pub use display_map::DisplayPoint;
 use display_map::*;
 pub use element::*;
+use futures::FutureExt;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     actions,
@@ -50,7 +51,7 @@ pub use multi_buffer::{
 };
 use multi_buffer::{MultiBufferChunks, ToOffsetUtf16};
 use ordered_float::OrderedFloat;
-use project::{LocationLink, Project, ProjectPath, ProjectTransaction};
+use project::{FormatTrigger, LocationLink, Project, ProjectPath, ProjectTransaction};
 use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
 use serde::{Deserialize, Serialize};
 use settings::Settings;
@@ -77,6 +78,8 @@ const MAX_LINE_LEN: usize = 1024;
 const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
 const MAX_SELECTION_HISTORY_LEN: usize = 1024;
 
+pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
+
 #[derive(Clone, Deserialize, PartialEq, Default)]
 pub struct SelectNext {
     #[serde(default)]
@@ -205,6 +208,7 @@ actions!(
         OpenExcerpts,
         RestartLanguageServer,
         Hover,
+        Format,
     ]
 );
 
@@ -311,6 +315,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::toggle_code_actions);
     cx.add_action(Editor::open_excerpts);
     cx.add_action(Editor::jump);
+    cx.add_async_action(Editor::format);
     cx.add_action(Editor::restart_language_server);
     cx.add_action(Editor::show_character_palette);
     cx.add_async_action(Editor::confirm_completion);
@@ -1577,17 +1582,20 @@ impl Editor {
         let start;
         let end;
         let mode;
+        let auto_scroll;
         match click_count {
             1 => {
                 start = buffer.anchor_before(position.to_point(&display_map));
                 end = start.clone();
                 mode = SelectMode::Character;
+                auto_scroll = true;
             }
             2 => {
                 let range = movement::surrounding_word(&display_map, position);
                 start = buffer.anchor_before(range.start.to_point(&display_map));
                 end = buffer.anchor_before(range.end.to_point(&display_map));
                 mode = SelectMode::Word(start.clone()..end.clone());
+                auto_scroll = true;
             }
             3 => {
                 let position = display_map
@@ -1601,15 +1609,17 @@ impl Editor {
                 start = buffer.anchor_before(line_start);
                 end = buffer.anchor_before(next_line_start);
                 mode = SelectMode::Line(start.clone()..end.clone());
+                auto_scroll = true;
             }
             _ => {
                 start = buffer.anchor_before(0);
                 end = buffer.anchor_before(buffer.len());
                 mode = SelectMode::All;
+                auto_scroll = false;
             }
         }
 
-        self.change_selections(Some(Autoscroll::Newest), cx, |s| {
+        self.change_selections(auto_scroll.then(|| Autoscroll::Newest), cx, |s| {
             if !add {
                 s.clear_disjoint();
             } else if click_count > 1 {
@@ -5170,6 +5180,51 @@ impl Editor {
         self.pending_rename.as_ref()
     }
 
+    fn format(&mut self, _: &Format, cx: &mut ViewContext<'_, Self>) -> Option<Task<Result<()>>> {
+        let project = match &self.project {
+            Some(project) => project.clone(),
+            None => return None,
+        };
+
+        Some(self.perform_format(project, cx))
+    }
+
+    fn perform_format(
+        &mut self,
+        project: ModelHandle<Project>,
+        cx: &mut ViewContext<'_, Self>,
+    ) -> Task<Result<()>> {
+        let buffer = self.buffer().clone();
+        let buffers = buffer.read(cx).all_buffers();
+
+        let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
+        let format = project.update(cx, |project, cx| {
+            project.format(buffers, true, FormatTrigger::Manual, cx)
+        });
+
+        cx.spawn(|_, mut cx| async move {
+            let transaction = futures::select_biased! {
+                _ = timeout => {
+                    log::warn!("timed out waiting for formatting");
+                    None
+                }
+                transaction = format.log_err().fuse() => transaction,
+            };
+
+            buffer.update(&mut cx, |buffer, cx| {
+                if let Some(transaction) = transaction {
+                    if !buffer.is_singleton() {
+                        buffer.push_transaction(&transaction.0);
+                    }
+                }
+
+                cx.notify();
+            });
+
+            Ok(())
+        })
+    }
+
     fn restart_language_server(&mut self, _: &RestartLanguageServer, cx: &mut ViewContext<Self>) {
         if let Some(project) = self.project.clone() {
             self.buffer.update(cx, |multi_buffer, cx| {
@@ -10103,7 +10158,7 @@ mod tests {
             unreachable!()
         });
         let save = cx.update(|cx| editor.save(project.clone(), cx));
-        cx.foreground().advance_clock(items::FORMAT_TIMEOUT);
+        cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
         cx.foreground().start_waiting();
         save.await.unwrap();
         assert_eq!(
@@ -10219,7 +10274,7 @@ mod tests {
             },
         );
         let save = cx.update(|cx| editor.save(project.clone(), cx));
-        cx.foreground().advance_clock(items::FORMAT_TIMEOUT);
+        cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
         cx.foreground().start_waiting();
         save.await.unwrap();
         assert_eq!(
@@ -10257,6 +10312,87 @@ mod tests {
         save.await.unwrap();
     }
 
+    #[gpui::test]
+    async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
+        cx.foreground().forbid_parking();
+
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    document_formatting_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_file("/file.rs", Default::default()).await;
+
+        let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+        let buffer = project
+            .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
+            .await
+            .unwrap();
+
+        cx.foreground().start_waiting();
+        let fake_server = fake_servers.next().await.unwrap();
+
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+        let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
+        editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+
+        let format = editor.update(cx, |editor, cx| editor.perform_format(project.clone(), cx));
+        fake_server
+            .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+                assert_eq!(
+                    params.text_document.uri,
+                    lsp::Url::from_file_path("/file.rs").unwrap()
+                );
+                assert_eq!(params.options.tab_size, 4);
+                Ok(Some(vec![lsp::TextEdit::new(
+                    lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
+                    ", ".to_string(),
+                )]))
+            })
+            .next()
+            .await;
+        cx.foreground().start_waiting();
+        format.await.unwrap();
+        assert_eq!(
+            editor.read_with(cx, |editor, cx| editor.text(cx)),
+            "one, two\nthree\n"
+        );
+
+        editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+        // Ensure we don't lock if formatting hangs.
+        fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/file.rs").unwrap()
+            );
+            futures::future::pending::<()>().await;
+            unreachable!()
+        });
+        let format = editor.update(cx, |editor, cx| editor.perform_format(project, cx));
+        cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
+        cx.foreground().start_waiting();
+        format.await.unwrap();
+        assert_eq!(
+            editor.read_with(cx, |editor, cx| editor.text(cx)),
+            "one\ntwo\nthree\n"
+        );
+    }
+
     #[gpui::test]
     async fn test_completion(cx: &mut gpui::TestAppContext) {
         let mut cx = EditorLspTestContext::new_rust(

crates/editor/src/items.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
     movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer,
-    MultiBufferSnapshot, NavigationData, ToPoint as _,
+    MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT,
 };
 use anyhow::{anyhow, Result};
 use futures::FutureExt;
@@ -10,7 +10,7 @@ use gpui::{
     RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
 };
 use language::{Bias, Buffer, File as _, OffsetRangeExt, SelectionGoal};
-use project::{File, Project, ProjectEntryId, ProjectPath};
+use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
 use rpc::proto::{self, update_view};
 use settings::Settings;
 use smallvec::SmallVec;
@@ -20,7 +20,6 @@ use std::{
     fmt::Write,
     ops::Range,
     path::{Path, PathBuf},
-    time::Duration,
 };
 use text::{Point, Selection};
 use util::TryFutureExt;
@@ -30,7 +29,6 @@ use workspace::{
     ToolbarItemLocation,
 };
 
-pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 pub const MAX_TAB_TITLE_LEN: usize = 24;
 
 impl FollowableItem for Editor {
@@ -406,11 +404,14 @@ impl Item for Editor {
         project: ModelHandle<Project>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
+        self.report_event("save editor", cx);
+
         let buffer = self.buffer().clone();
         let buffers = buffer.read(cx).all_buffers();
         let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
-        let format = project.update(cx, |project, cx| project.format(buffers, true, cx));
-        self.report_event("save editor", cx);
+        let format = project.update(cx, |project, cx| {
+            project.format(buffers, true, FormatTrigger::Save, cx)
+        });
         cx.spawn(|_, mut cx| async move {
             let transaction = futures::select_biased! {
                 _ = timeout => {

crates/gpui/src/elements/flex.rs 🔗

@@ -1,11 +1,10 @@
-use std::{any::Any, f32::INFINITY, ops::Range};
+use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
 
 use crate::{
     json::{self, ToJson, Value},
     presenter::MeasurementContext,
     Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
-    LayoutContext, MouseMovedEvent, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint,
-    Vector2FExt, View,
+    LayoutContext, PaintContext, RenderContext, SizeConstraint, Vector2FExt, View,
 };
 use pathfinder_geometry::{
     rect::RectF,
@@ -15,14 +14,14 @@ use serde_json::json;
 
 #[derive(Default)]
 struct ScrollState {
-    scroll_to: Option<usize>,
-    scroll_position: f32,
+    scroll_to: Cell<Option<usize>>,
+    scroll_position: Cell<f32>,
 }
 
 pub struct Flex {
     axis: Axis,
     children: Vec<ElementBox>,
-    scroll_state: Option<ElementStateHandle<ScrollState>>,
+    scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
 }
 
 impl Flex {
@@ -52,9 +51,9 @@ impl Flex {
         Tag: 'static,
         V: View,
     {
-        let scroll_state = cx.default_element_state::<Tag, ScrollState>(element_id);
-        scroll_state.update(cx, |scroll_state, _| scroll_state.scroll_to = scroll_to);
-        self.scroll_state = Some(scroll_state);
+        let scroll_state = cx.default_element_state::<Tag, Rc<ScrollState>>(element_id);
+        scroll_state.read(cx).scroll_to.set(scroll_to);
+        self.scroll_state = Some((scroll_state, cx.handle().id()));
         self
     }
 
@@ -202,9 +201,9 @@ impl Element for Flex {
         }
 
         if let Some(scroll_state) = self.scroll_state.as_ref() {
-            scroll_state.update(cx, |scroll_state, _| {
+            scroll_state.0.update(cx, |scroll_state, _| {
                 if let Some(scroll_to) = scroll_state.scroll_to.take() {
-                    let visible_start = scroll_state.scroll_position;
+                    let visible_start = scroll_state.scroll_position.get();
                     let visible_end = visible_start + size.along(self.axis);
                     if let Some(child) = self.children.get(scroll_to) {
                         let child_start: f32 = self.children[..scroll_to]
@@ -213,15 +212,22 @@ impl Element for Flex {
                             .sum();
                         let child_end = child_start + child.size().along(self.axis);
                         if child_start < visible_start {
-                            scroll_state.scroll_position = child_start;
+                            scroll_state.scroll_position.set(child_start);
                         } else if child_end > visible_end {
-                            scroll_state.scroll_position = child_end - size.along(self.axis);
+                            scroll_state
+                                .scroll_position
+                                .set(child_end - size.along(self.axis));
                         }
                     }
                 }
 
-                scroll_state.scroll_position =
-                    scroll_state.scroll_position.min(-remaining_space).max(0.);
+                scroll_state.scroll_position.set(
+                    scroll_state
+                        .scroll_position
+                        .get()
+                        .min(-remaining_space)
+                        .max(0.),
+                );
             });
         }
 
@@ -242,9 +248,45 @@ impl Element for Flex {
             cx.scene.push_layer(Some(bounds));
         }
 
+        if let Some(scroll_state) = &self.scroll_state {
+            cx.scene.push_mouse_region(
+                crate::MouseRegion::new::<Self>(scroll_state.1, 0, bounds)
+                    .on_scroll({
+                        let scroll_state = scroll_state.0.read(cx).clone();
+                        let axis = self.axis;
+                        move |e, cx| {
+                            if remaining_space < 0. {
+                                let mut delta = match axis {
+                                    Axis::Horizontal => {
+                                        if e.delta.x() != 0. {
+                                            e.delta.x()
+                                        } else {
+                                            e.delta.y()
+                                        }
+                                    }
+                                    Axis::Vertical => e.delta.y(),
+                                };
+                                if !e.precise {
+                                    delta *= 20.;
+                                }
+
+                                scroll_state
+                                    .scroll_position
+                                    .set(scroll_state.scroll_position.get() - delta);
+
+                                cx.notify();
+                            } else {
+                                cx.propogate_event();
+                            }
+                        }
+                    })
+                    .on_move(|_, _| { /* Capture move events */ }),
+            )
+        }
+
         let mut child_origin = bounds.origin();
         if let Some(scroll_state) = self.scroll_state.as_ref() {
-            let scroll_position = scroll_state.read(cx).scroll_position;
+            let scroll_position = scroll_state.0.read(cx).scroll_position.get();
             match self.axis {
                 Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position),
                 Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position),
@@ -278,9 +320,9 @@ impl Element for Flex {
     fn dispatch_event(
         &mut self,
         event: &Event,
-        bounds: RectF,
         _: RectF,
-        remaining_space: &mut Self::LayoutState,
+        _: RectF,
+        _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,
     ) -> bool {
@@ -288,50 +330,6 @@ impl Element for Flex {
         for child in &mut self.children {
             handled = child.dispatch_event(event, cx) || handled;
         }
-        if !handled {
-            if let &Event::ScrollWheel(ScrollWheelEvent {
-                position,
-                delta,
-                precise,
-                ..
-            }) = event
-            {
-                if *remaining_space < 0. && bounds.contains_point(position) {
-                    if let Some(scroll_state) = self.scroll_state.as_ref() {
-                        scroll_state.update(cx, |scroll_state, cx| {
-                            let mut delta = match self.axis {
-                                Axis::Horizontal => {
-                                    if delta.x() != 0. {
-                                        delta.x()
-                                    } else {
-                                        delta.y()
-                                    }
-                                }
-                                Axis::Vertical => delta.y(),
-                            };
-                            if !precise {
-                                delta *= 20.;
-                            }
-
-                            scroll_state.scroll_position -= delta;
-
-                            handled = true;
-                            cx.notify();
-                        });
-                    }
-                }
-            }
-        }
-
-        if !handled {
-            if let &Event::MouseMoved(MouseMovedEvent { position, .. }) = event {
-                // If this is a scrollable flex, and the mouse is over it, eat the scroll event to prevent
-                // propogating it to the element below.
-                if self.scroll_state.is_some() && bounds.contains_point(position) {
-                    handled = true;
-                }
-            }
-        }
 
         handled
     }

crates/gpui/src/elements/list.rs 🔗

@@ -5,8 +5,8 @@ use crate::{
     },
     json::json,
     presenter::MeasurementContext,
-    DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
-    RenderContext, ScrollWheelEvent, SizeConstraint, View, ViewContext,
+    DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, MouseRegion,
+    PaintContext, RenderContext, SizeConstraint, View, ViewContext,
 };
 use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
 use sum_tree::{Bias, SumTree};
@@ -263,6 +263,22 @@ impl Element for List {
     ) {
         cx.scene.push_layer(Some(bounds));
 
+        cx.scene
+            .push_mouse_region(MouseRegion::new::<Self>(10, 0, bounds).on_scroll({
+                let state = self.state.clone();
+                let height = bounds.height();
+                let scroll_top = scroll_top.clone();
+                move |e, cx| {
+                    state.0.borrow_mut().scroll(
+                        &scroll_top,
+                        height,
+                        e.platform_event.delta,
+                        e.platform_event.precise,
+                        cx,
+                    )
+                }
+            }));
+
         let state = &mut *self.state.0.borrow_mut();
         for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
             element.paint(origin, visible_bounds, cx);
@@ -312,20 +328,6 @@ impl Element for List {
         drop(cursor);
         state.items = new_items;
 
-        if let Event::ScrollWheel(ScrollWheelEvent {
-            position,
-            delta,
-            precise,
-            ..
-        }) = event
-        {
-            if bounds.contains_point(*position)
-                && state.scroll(scroll_top, bounds.height(), *delta, *precise, cx)
-            {
-                handled = true;
-            }
-        }
-
         handled
     }
 
@@ -527,7 +529,7 @@ impl StateInner {
         mut delta: Vector2F,
         precise: bool,
         cx: &mut EventContext,
-    ) -> bool {
+    ) {
         if !precise {
             delta *= 20.;
         }
@@ -554,9 +556,6 @@ impl StateInner {
             let visible_range = self.visible_range(height, scroll_top);
             self.scroll_handler.as_mut().unwrap()(visible_range, cx);
         }
-        cx.notify();
-
-        true
     }
 
     fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {

crates/gpui/src/elements/mouse_event_handler.rs 🔗

@@ -7,7 +7,8 @@ use crate::{
     platform::CursorStyle,
     scene::{
         ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
-        HandlerSet, HoverRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
+        HandlerSet, HoverRegionEvent, MoveRegionEvent, ScrollWheelRegionEvent, UpOutRegionEvent,
+        UpRegionEvent,
     },
     DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MeasurementContext,
     MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
@@ -122,6 +123,14 @@ impl<Tag> MouseEventHandler<Tag> {
         self
     }
 
+    pub fn on_scroll(
+        mut self,
+        handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static,
+    ) -> Self {
+        self.handlers = self.handlers.on_scroll(handler);
+        self
+    }
+
     pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
         self.hoverable = is_hoverable;
         self

crates/gpui/src/elements/overlay.rs 🔗

@@ -14,6 +14,7 @@ pub struct Overlay {
     anchor_position: Option<Vector2F>,
     anchor_corner: AnchorCorner,
     fit_mode: OverlayFitMode,
+    position_mode: OverlayPositionMode,
     hoverable: bool,
 }
 
@@ -24,6 +25,12 @@ pub enum OverlayFitMode {
     None,
 }
 
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum OverlayPositionMode {
+    Window,
+    Local,
+}
+
 #[derive(Clone, Copy, PartialEq, Eq)]
 pub enum AnchorCorner {
     TopLeft,
@@ -73,6 +80,7 @@ impl Overlay {
             anchor_position: None,
             anchor_corner: AnchorCorner::TopLeft,
             fit_mode: OverlayFitMode::None,
+            position_mode: OverlayPositionMode::Window,
             hoverable: false,
         }
     }
@@ -92,6 +100,11 @@ impl Overlay {
         self
     }
 
+    pub fn with_position_mode(mut self, position_mode: OverlayPositionMode) -> Self {
+        self.position_mode = position_mode;
+        self
+    }
+
     pub fn with_hoverable(mut self, hoverable: bool) -> Self {
         self.hoverable = hoverable;
         self
@@ -123,8 +136,20 @@ impl Element for Overlay {
         size: &mut Self::LayoutState,
         cx: &mut PaintContext,
     ) {
-        let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
-        let mut bounds = self.anchor_corner.get_bounds(anchor_position, *size);
+        let (anchor_position, mut bounds) = match self.position_mode {
+            OverlayPositionMode::Window => {
+                let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
+                let bounds = self.anchor_corner.get_bounds(anchor_position, *size);
+                (anchor_position, bounds)
+            }
+            OverlayPositionMode::Local => {
+                let anchor_position = self.anchor_position.unwrap_or_default();
+                let bounds = self
+                    .anchor_corner
+                    .get_bounds(bounds.origin() + anchor_position, *size);
+                (anchor_position, bounds)
+            }
+        };
 
         match self.fit_mode {
             OverlayFitMode::SnapToWindow => {

crates/gpui/src/elements/tooltip.rs 🔗

@@ -36,10 +36,10 @@ struct TooltipState {
 #[derive(Clone, Deserialize, Default)]
 pub struct TooltipStyle {
     #[serde(flatten)]
-    container: ContainerStyle,
-    text: TextStyle,
+    pub container: ContainerStyle,
+    pub text: TextStyle,
     keystroke: KeystrokeStyle,
-    max_text_width: f32,
+    pub max_text_width: f32,
 }
 
 #[derive(Clone, Deserialize, Default)]
@@ -126,7 +126,7 @@ impl Tooltip {
         }
     }
 
-    fn render_tooltip(
+    pub fn render_tooltip(
         text: String,
         style: TooltipStyle,
         action: Option<Box<dyn Action>>,

crates/gpui/src/elements/uniform_list.rs 🔗

@@ -6,7 +6,8 @@ use crate::{
     },
     json::{self, json},
     presenter::MeasurementContext,
-    ElementBox, RenderContext, ScrollWheelEvent, View,
+    scene::ScrollWheelRegionEvent,
+    ElementBox, MouseRegion, RenderContext, ScrollWheelEvent, View,
 };
 use json::ToJson;
 use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -50,6 +51,7 @@ pub struct UniformList {
     padding_top: f32,
     padding_bottom: f32,
     get_width_from_item: Option<usize>,
+    view_id: usize,
 }
 
 impl UniformList {
@@ -77,6 +79,7 @@ impl UniformList {
             padding_top: 0.,
             padding_bottom: 0.,
             get_width_from_item: None,
+            view_id: cx.handle().id(),
         }
     }
 
@@ -96,7 +99,7 @@ impl UniformList {
     }
 
     fn scroll(
-        &self,
+        state: UniformListState,
         _: Vector2F,
         mut delta: Vector2F,
         precise: bool,
@@ -107,7 +110,7 @@ impl UniformList {
             delta *= 20.;
         }
 
-        let mut state = self.state.0.borrow_mut();
+        let mut state = state.0.borrow_mut();
         state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
         cx.notify();
 
@@ -283,6 +286,28 @@ impl Element for UniformList {
     ) -> Self::PaintState {
         cx.scene.push_layer(Some(bounds));
 
+        cx.scene.push_mouse_region(
+            MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({
+                let scroll_max = layout.scroll_max;
+                let state = self.state.clone();
+                move |ScrollWheelRegionEvent {
+                          platform_event:
+                              ScrollWheelEvent {
+                                  position,
+                                  delta,
+                                  precise,
+                                  ..
+                              },
+                          ..
+                      },
+                      cx| {
+                    if !Self::scroll(state.clone(), position, delta, precise, scroll_max, cx) {
+                        cx.propogate_event();
+                    }
+                }
+            }),
+        );
+
         let mut item_origin = bounds.origin()
             - vec2f(
                 0.,
@@ -300,7 +325,7 @@ impl Element for UniformList {
     fn dispatch_event(
         &mut self,
         event: &Event,
-        bounds: RectF,
+        _: RectF,
         _: RectF,
         layout: &mut Self::LayoutState,
         _: &mut Self::PaintState,
@@ -311,20 +336,6 @@ impl Element for UniformList {
             handled = item.dispatch_event(event, cx) || handled;
         }
 
-        if let Event::ScrollWheel(ScrollWheelEvent {
-            position,
-            delta,
-            precise,
-            ..
-        }) = event
-        {
-            if bounds.contains_point(*position)
-                && self.scroll(*position, *delta, *precise, layout.scroll_max, cx)
-            {
-                handled = true;
-            }
-        }
-
         handled
     }
 

crates/project/src/project.rs 🔗

@@ -39,7 +39,7 @@ use postage::watch;
 use rand::prelude::*;
 use search::SearchQuery;
 use serde::Serialize;
-use settings::Settings;
+use settings::{FormatOnSave, Formatter, Settings};
 use sha2::{Digest, Sha256};
 use similar::{ChangeTag, TextDiff};
 use std::{
@@ -363,6 +363,22 @@ impl ProjectEntryId {
     }
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum FormatTrigger {
+    Save,
+    Manual,
+}
+
+impl FormatTrigger {
+    fn from_proto(value: i32) -> FormatTrigger {
+        match value {
+            0 => FormatTrigger::Save,
+            1 => FormatTrigger::Manual,
+            _ => FormatTrigger::Save,
+        }
+    }
+}
+
 impl Project {
     pub fn init(client: &Arc<Client>) {
         client.add_model_message_handler(Self::handle_request_join_project);
@@ -3046,6 +3062,7 @@ impl Project {
         &self,
         buffers: HashSet<ModelHandle<Buffer>>,
         push_to_history: bool,
+        trigger: FormatTrigger,
         cx: &mut ModelContext<Project>,
     ) -> Task<Result<ProjectTransaction>> {
         let mut local_buffers = Vec::new();
@@ -3075,6 +3092,7 @@ impl Project {
                 let response = client
                     .request(proto::FormatBuffers {
                         project_id,
+                        trigger: trigger as i32,
                         buffer_ids: remote_buffers
                             .iter()
                             .map(|buffer| buffer.read_with(&cx, |buffer, _| buffer.remote_id()))
@@ -3091,18 +3109,21 @@ impl Project {
             }
 
             for (buffer, buffer_abs_path, language_server) in local_buffers {
-                let (format_on_save, tab_size) = buffer.read_with(&cx, |buffer, cx| {
+                let (format_on_save, formatter, tab_size) = buffer.read_with(&cx, |buffer, cx| {
                     let settings = cx.global::<Settings>();
                     let language_name = buffer.language().map(|language| language.name());
                     (
                         settings.format_on_save(language_name.as_deref()),
+                        settings.formatter(language_name.as_deref()),
                         settings.tab_size(language_name.as_deref()),
                     )
                 });
 
-                let transaction = match format_on_save {
-                    settings::FormatOnSave::Off => continue,
-                    settings::FormatOnSave::LanguageServer => Self::format_via_lsp(
+                let transaction = match (formatter, format_on_save) {
+                    (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => continue,
+
+                    (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
+                    | (_, FormatOnSave::LanguageServer) => Self::format_via_lsp(
                         &this,
                         &buffer,
                         &buffer_abs_path,
@@ -3112,7 +3133,12 @@ impl Project {
                     )
                     .await
                     .context("failed to format via language server")?,
-                    settings::FormatOnSave::External { command, arguments } => {
+
+                    (
+                        Formatter::External { command, arguments },
+                        FormatOnSave::On | FormatOnSave::Off,
+                    )
+                    | (_, FormatOnSave::External { command, arguments }) => {
                         Self::format_via_external_command(
                             &buffer,
                             &buffer_abs_path,
@@ -5295,7 +5321,8 @@ impl Project {
                         .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?,
                 );
             }
-            Ok::<_, anyhow::Error>(this.format(buffers, false, cx))
+            let trigger = FormatTrigger::from_proto(envelope.payload.trigger);
+            Ok::<_, anyhow::Error>(this.format(buffers, false, trigger, cx))
         })?;
 
         let project_transaction = format.await?;

crates/rpc/proto/zed.proto 🔗

@@ -420,9 +420,15 @@ message ReloadBuffersResponse {
     ProjectTransaction transaction = 1;
 }
 
+enum FormatTrigger {
+    Save = 0;
+    Manual = 1;
+}
+
 message FormatBuffers {
     uint64 project_id = 1;
-    repeated uint64 buffer_ids = 2;
+    FormatTrigger trigger = 2;
+    repeated uint64 buffer_ids = 3;
 }
 
 message FormatBuffersResponse {

crates/rpc/src/rpc.rs 🔗

@@ -6,4 +6,4 @@ pub use conn::Connection;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 31;
+pub const PROTOCOL_VERSION: u32 = 32;

crates/settings/src/settings.rs 🔗

@@ -59,6 +59,7 @@ pub struct EditorSettings {
     pub soft_wrap: Option<SoftWrap>,
     pub preferred_line_length: Option<u32>,
     pub format_on_save: Option<FormatOnSave>,
+    pub formatter: Option<Formatter>,
     pub enable_language_server: Option<bool>,
 }
 
@@ -69,10 +70,10 @@ pub enum SoftWrap {
     EditorWidth,
     PreferredLineLength,
 }
-
 #[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum FormatOnSave {
+    On,
     Off,
     LanguageServer,
     External {
@@ -81,6 +82,16 @@ pub enum FormatOnSave {
     },
 }
 
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum Formatter {
+    LanguageServer,
+    External {
+        command: String,
+        arguments: Vec<String>,
+    },
+}
+
 #[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum Autosave {
@@ -207,6 +218,7 @@ impl Settings {
         font_cache: &FontCache,
         themes: &ThemeRegistry,
     ) -> Self {
+        #[track_caller]
         fn required<T>(value: Option<T>) -> Option<T> {
             assert!(value.is_some(), "missing default setting value");
             value
@@ -236,6 +248,7 @@ impl Settings {
                 soft_wrap: required(defaults.editor.soft_wrap),
                 preferred_line_length: required(defaults.editor.preferred_line_length),
                 format_on_save: required(defaults.editor.format_on_save),
+                formatter: required(defaults.editor.formatter),
                 enable_language_server: required(defaults.editor.enable_language_server),
             },
             editor_overrides: Default::default(),
@@ -326,6 +339,10 @@ impl Settings {
         self.language_setting(language, |settings| settings.format_on_save.clone())
     }
 
+    pub fn formatter(&self, language: Option<&str>) -> Formatter {
+        self.language_setting(language, |settings| settings.formatter.clone())
+    }
+
     pub fn enable_language_server(&self, language: Option<&str>) -> bool {
         self.language_setting(language, |settings| settings.enable_language_server)
     }
@@ -358,7 +375,8 @@ impl Settings {
                 hard_tabs: Some(false),
                 soft_wrap: Some(SoftWrap::None),
                 preferred_line_length: Some(80),
-                format_on_save: Some(FormatOnSave::LanguageServer),
+                format_on_save: Some(FormatOnSave::On),
+                formatter: Some(Formatter::LanguageServer),
                 enable_language_server: Some(true),
             },
             editor_overrides: Default::default(),

crates/terminal/Cargo.toml 🔗

@@ -29,6 +29,10 @@ shellexpand = "2.1.0"
 libc = "0.2"
 anyhow = "1"
 thiserror = "1.0"
+lazy_static = "1.4.0"
+serde = { version = "1.0", features = ["derive"] }
+
+
 
 
 [dev-dependencies]
@@ -36,3 +40,4 @@ gpui = { path = "../gpui", features = ["test-support"] }
 client = { path = "../client", features = ["test-support"]}
 project = { path = "../project", features = ["test-support"]}
 workspace = { path = "../workspace", features = ["test-support"] }
+rand = "0.8.5"

crates/terminal/src/mappings/mouse.rs 🔗

@@ -202,7 +202,7 @@ pub fn mouse_side(pos: Vector2F, cur_size: TerminalSize) -> alacritty_terminal::
     }
 }
 
-pub fn mouse_point(pos: Vector2F, cur_size: TerminalSize, display_offset: usize) -> Point {
+pub fn grid_point(pos: Vector2F, cur_size: TerminalSize, display_offset: usize) -> Point {
     let col = pos.x() / cur_size.cell_width;
     let col = min(GridCol(col as usize), cur_size.last_column());
     let line = pos.y() / cur_size.line_height;
@@ -295,7 +295,7 @@ fn sgr_mouse_report(point: Point, button: u8, pressed: bool) -> String {
 
 #[cfg(test)]
 mod test {
-    use crate::mappings::mouse::mouse_point;
+    use crate::mappings::mouse::grid_point;
 
     #[test]
     fn test_mouse_to_selection() {
@@ -317,7 +317,7 @@ mod test {
         let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
         let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
         let mouse_pos = mouse_pos - origin;
-        let point = mouse_point(mouse_pos, cur_size, 0);
+        let point = grid_point(mouse_pos, cur_size, 0);
         assert_eq!(
             point,
             alacritty_terminal::index::Point::new(

crates/terminal/src/terminal.rs 🔗

@@ -29,18 +29,22 @@ use futures::{
 };
 
 use mappings::mouse::{
-    alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
+    alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report,
 };
 
 use procinfo::LocalProcessInfo;
 use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
+use util::ResultExt;
 
 use std::{
+    cmp::min,
     collections::{HashMap, VecDeque},
     fmt::Display,
-    ops::{Deref, RangeInclusive, Sub},
-    os::unix::prelude::AsRawFd,
+    io,
+    ops::{Deref, Index, RangeInclusive, Sub},
+    os::unix::{prelude::AsRawFd, process::CommandExt},
     path::PathBuf,
+    process::Command,
     sync::Arc,
     time::{Duration, Instant},
 };
@@ -49,9 +53,7 @@ use thiserror::Error;
 use gpui::{
     geometry::vector::{vec2f, Vector2F},
     keymap::Keystroke,
-    scene::{
-        ClickRegionEvent, DownRegionEvent, DragRegionEvent, ScrollWheelRegionEvent, UpRegionEvent,
-    },
+    scene::{DownRegionEvent, DragRegionEvent, ScrollWheelRegionEvent, UpRegionEvent},
     ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext, Task,
 };
 
@@ -59,6 +61,7 @@ use crate::mappings::{
     colors::{get_color_at_index, to_alac_rgb},
     keys::to_esc_str,
 };
+use lazy_static::lazy_static;
 
 ///Initialize and register all of our action handlers
 pub fn init(cx: &mut MutableAppContext) {
@@ -70,12 +73,18 @@ pub fn init(cx: &mut MutableAppContext) {
 ///Scroll multiplier that is set to 3 by default. This will be removed when I
 ///Implement scroll bars.
 const SCROLL_MULTIPLIER: f32 = 4.;
-// const MAX_SEARCH_LINES: usize = 100;
+const MAX_SEARCH_LINES: usize = 100;
 const DEBUG_TERMINAL_WIDTH: f32 = 500.;
 const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
 const DEBUG_CELL_WIDTH: f32 = 5.;
 const DEBUG_LINE_HEIGHT: f32 = 5.;
 
+// Regex Copied from alacritty's ui_config.rs
+
+lazy_static! {
+    static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap();
+}
+
 ///Upward flowing events, for changing the title and such
 #[derive(Clone, Copy, Debug)]
 pub enum Event {
@@ -98,6 +107,8 @@ enum InternalEvent {
     ScrollToPoint(Point),
     SetSelection(Option<(Selection, Point)>),
     UpdateSelection(Vector2F),
+    // Adjusted mouse position, should open
+    FindHyperlink(Vector2F, bool),
     Copy,
 }
 
@@ -267,7 +278,6 @@ impl TerminalBuilder {
         working_directory: Option<PathBuf>,
         shell: Option<Shell>,
         env: Option<HashMap<String, String>>,
-        initial_size: TerminalSize,
         blink_settings: Option<TerminalBlink>,
         alternate_scroll: &AlternateScroll,
         window_id: usize,
@@ -307,7 +317,11 @@ impl TerminalBuilder {
         //TODO: Remove with a bounded sender which can be dispatched on &self
         let (events_tx, events_rx) = unbounded();
         //Set up the terminal...
-        let mut term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
+        let mut term = Term::new(
+            &config,
+            &TerminalSize::default(),
+            ZedListener(events_tx.clone()),
+        );
 
         //Start off blinking if we need to
         if let Some(TerminalBlink::On) = blink_settings {
@@ -322,7 +336,11 @@ impl TerminalBuilder {
         let term = Arc::new(FairMutex::new(term));
 
         //Setup the pty...
-        let pty = match tty::new(&pty_config, initial_size.into(), window_id as u64) {
+        let pty = match tty::new(
+            &pty_config,
+            TerminalSize::default().into(),
+            window_id as u64,
+        ) {
             Ok(pty) => pty,
             Err(error) => {
                 bail!(TerminalError {
@@ -354,7 +372,6 @@ impl TerminalBuilder {
             term,
             events: VecDeque::with_capacity(10), //Should never get this high.
             last_content: Default::default(),
-            cur_size: initial_size,
             last_mouse: None,
             matches: Vec::new(),
             last_synced: Instant::now(),
@@ -365,6 +382,9 @@ impl TerminalBuilder {
             foreground_process_info: None,
             breadcrumb_text: String::new(),
             scroll_px: 0.,
+            last_mouse_position: None,
+            next_link_id: 0,
+            selection_phase: SelectionPhase::Ended,
         };
 
         Ok(TerminalBuilder {
@@ -450,6 +470,8 @@ pub struct TerminalContent {
     selection: Option<SelectionRange>,
     cursor: RenderableCursor,
     cursor_char: char,
+    size: TerminalSize,
+    last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
 }
 
 impl Default for TerminalContent {
@@ -465,17 +487,27 @@ impl Default for TerminalContent {
                 point: Point::new(Line(0), Column(0)),
             },
             cursor_char: Default::default(),
+            size: Default::default(),
+            last_hovered_hyperlink: None,
         }
     }
 }
 
+#[derive(PartialEq, Eq)]
+pub enum SelectionPhase {
+    Selecting,
+    Ended,
+}
+
 pub struct Terminal {
     pty_tx: Notifier,
     term: Arc<FairMutex<Term<ZedListener>>>,
     events: VecDeque<InternalEvent>,
+    /// This is only used for mouse mode cell change detection
     last_mouse: Option<(Point, AlacDirection)>,
+    /// This is only used for terminal hyperlink checking
+    last_mouse_position: Option<Vector2F>,
     pub matches: Vec<RangeInclusive<Point>>,
-    cur_size: TerminalSize,
     last_content: TerminalContent,
     last_synced: Instant,
     sync_task: Option<Task<()>>,
@@ -485,6 +517,8 @@ pub struct Terminal {
     shell_fd: u32,
     foreground_process_info: Option<LocalProcessInfo>,
     scroll_px: f32,
+    next_link_id: usize,
+    selection_phase: SelectionPhase,
 }
 
 impl Terminal {
@@ -508,7 +542,7 @@ impl Terminal {
             )),
             AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
             AlacTermEvent::TextAreaSizeRequest(format) => {
-                self.write_to_pty(format(self.cur_size.into()))
+                self.write_to_pty(format(self.last_content.size.into()))
             }
             AlacTermEvent::CursorBlinkingChange => {
                 cx.emit(Event::BlinkChanged);
@@ -577,16 +611,10 @@ impl Terminal {
                 new_size.height = f32::max(new_size.line_height, new_size.height);
                 new_size.width = f32::max(new_size.cell_width, new_size.width);
 
-                self.cur_size = new_size.clone();
+                self.last_content.size = new_size.clone();
 
                 self.pty_tx.0.send(Msg::Resize((new_size).into())).ok();
 
-                // When this resize happens
-                // We go from 737px -> 703px height
-                // This means there is 1 less line
-                // that means the delta is 1
-                // That means the selection is rotated by -1
-
                 term.resize(new_size);
             }
             InternalEvent::Clear => {
@@ -595,6 +623,7 @@ impl Terminal {
             }
             InternalEvent::Scroll(scroll) => {
                 term.scroll_display(*scroll);
+                self.refresh_hyperlink();
             }
             InternalEvent::SetSelection(selection) => {
                 term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
@@ -606,8 +635,12 @@ impl Terminal {
             }
             InternalEvent::UpdateSelection(position) => {
                 if let Some(mut selection) = term.selection.take() {
-                    let point = mouse_point(*position, self.cur_size, term.grid().display_offset());
-                    let side = mouse_side(*position, self.cur_size);
+                    let point = grid_point(
+                        *position,
+                        self.last_content.size,
+                        term.grid().display_offset(),
+                    );
+                    let side = mouse_side(*position, self.last_content.size);
 
                     selection.update(point, side);
                     term.selection = Some(selection);
@@ -622,10 +655,95 @@ impl Terminal {
                     cx.write_to_clipboard(ClipboardItem::new(txt))
                 }
             }
-            InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point),
+            InternalEvent::ScrollToPoint(point) => {
+                term.scroll_to_point(*point);
+                self.refresh_hyperlink();
+            }
+            InternalEvent::FindHyperlink(position, open) => {
+                let prev_hyperlink = self.last_content.last_hovered_hyperlink.take();
+
+                let point = grid_point(
+                    *position,
+                    self.last_content.size,
+                    term.grid().display_offset(),
+                )
+                .grid_clamp(term, alacritty_terminal::index::Boundary::Cursor);
+
+                let link = term.grid().index(point).hyperlink();
+                let found_url = if link.is_some() {
+                    let mut min_index = point;
+                    loop {
+                        let new_min_index =
+                            min_index.sub(term, alacritty_terminal::index::Boundary::Cursor, 1);
+                        if new_min_index == min_index {
+                            break;
+                        } else if term.grid().index(new_min_index).hyperlink() != link {
+                            break;
+                        } else {
+                            min_index = new_min_index
+                        }
+                    }
+
+                    let mut max_index = point;
+                    loop {
+                        let new_max_index =
+                            max_index.add(term, alacritty_terminal::index::Boundary::Cursor, 1);
+                        if new_max_index == max_index {
+                            break;
+                        } else if term.grid().index(new_max_index).hyperlink() != link {
+                            break;
+                        } else {
+                            max_index = new_max_index
+                        }
+                    }
+
+                    let url = link.unwrap().uri().to_owned();
+                    let url_match = min_index..=max_index;
+
+                    Some((url, url_match))
+                } else if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) {
+                    let url = term.bounds_to_string(*url_match.start(), *url_match.end());
+
+                    Some((url, url_match))
+                } else {
+                    None
+                };
+
+                if let Some((url, url_match)) = found_url {
+                    if *open {
+                        open_uri(&url).log_err();
+                    } else {
+                        self.update_hyperlink(prev_hyperlink, url, url_match);
+                    }
+                }
+            }
         }
     }
 
+    fn update_hyperlink(
+        &mut self,
+        prev_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
+        url: String,
+        url_match: RangeInclusive<Point>,
+    ) {
+        if let Some(prev_hyperlink) = prev_hyperlink {
+            if prev_hyperlink.0 == url && prev_hyperlink.1 == url_match {
+                self.last_content.last_hovered_hyperlink = Some((url, url_match, prev_hyperlink.2));
+            } else {
+                self.last_content.last_hovered_hyperlink =
+                    Some((url, url_match, self.next_link_id()));
+            }
+        } else {
+            self.last_content.last_hovered_hyperlink = Some((url, url_match, self.next_link_id()));
+        }
+    }
+
+    fn next_link_id(&mut self) -> usize {
+        let res = self.next_link_id;
+        self.next_link_id = self.next_link_id.wrapping_add(1);
+        res
+    }
+
     pub fn last_content(&self) -> &TerminalContent {
         &self.last_content
     }
@@ -691,7 +809,8 @@ impl Terminal {
         } else {
             text.replace("\r\n", "\r").replace('\n', "\r")
         };
-        self.input(paste_text)
+
+        self.input(paste_text);
     }
 
     pub fn try_sync(&mut self, cx: &mut ModelContext<Self>) {
@@ -730,11 +849,11 @@ impl Terminal {
             self.process_terminal_event(&e, &mut terminal, cx)
         }
 
-        self.last_content = Self::make_content(&terminal);
+        self.last_content = Self::make_content(&terminal, &self.last_content);
         self.last_synced = Instant::now();
     }
 
-    fn make_content(term: &Term<ZedListener>) -> TerminalContent {
+    fn make_content(term: &Term<ZedListener>, last_content: &TerminalContent) -> TerminalContent {
         let content = term.renderable_content();
         TerminalContent {
             cells: content
@@ -757,6 +876,8 @@ impl Terminal {
             selection: content.selection,
             cursor: content.cursor,
             cursor_char: term.grid()[content.cursor.point].c,
+            size: last_content.size,
+            last_hovered_hyperlink: last_content.last_hovered_hyperlink.clone(),
         }
     }
 
@@ -766,7 +887,8 @@ impl Terminal {
         }
     }
 
-    pub fn focus_out(&self) {
+    pub fn focus_out(&mut self) {
+        self.last_mouse_position = None;
         if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
             self.write_to_pty("\x1b[O".to_string());
         }
@@ -795,21 +917,40 @@ impl Terminal {
 
     pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
-
-        let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
-        let side = mouse_side(position, self.cur_size);
-
-        if self.mouse_changed(point, side) && self.mouse_mode(e.shift) {
-            if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
-                self.pty_tx.notify(bytes);
+        self.last_mouse_position = Some(position);
+        if self.mouse_mode(e.shift) {
+            let point = grid_point(
+                position,
+                self.last_content.size,
+                self.last_content.display_offset,
+            );
+            let side = mouse_side(position, self.last_content.size);
+
+            if self.mouse_changed(point, side) {
+                if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
+                    self.pty_tx.notify(bytes);
+                }
             }
+        } else {
+            self.hyperlink_from_position(Some(position));
+        }
+    }
+
+    fn hyperlink_from_position(&mut self, position: Option<Vector2F>) {
+        if self.selection_phase == SelectionPhase::Selecting {
+            self.last_content.last_hovered_hyperlink = None;
+        } else if let Some(position) = position {
+            self.events
+                .push_back(InternalEvent::FindHyperlink(position, false));
         }
     }
 
     pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
+        self.last_mouse_position = Some(position);
 
         if !self.mouse_mode(e.shift) {
+            self.selection_phase = SelectionPhase::Selecting;
             // Alacritty has the same ordering, of first updating the selection
             // then scrolling 15ms later
             self.events
@@ -822,20 +963,18 @@ impl Terminal {
                     None => return,
                 };
 
-                let scroll_lines = (scroll_delta / self.cur_size.line_height) as i32;
+                let scroll_lines = (scroll_delta / self.last_content.size.line_height) as i32;
 
                 self.events
                     .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
-                self.events
-                    .push_back(InternalEvent::UpdateSelection(position))
             }
         }
     }
 
     fn drag_line_delta(&mut self, e: DragRegionEvent) -> Option<f32> {
         //TODO: Why do these need to be doubled? Probably the same problem that the IME has
-        let top = e.region.origin_y() + (self.cur_size.line_height * 2.);
-        let bottom = e.region.lower_left().y() - (self.cur_size.line_height * 2.);
+        let top = e.region.origin_y() + (self.last_content.size.line_height * 2.);
+        let bottom = e.region.lower_left().y() - (self.last_content.size.line_height * 2.);
         let scroll_delta = if e.position.y() < top {
             (top - e.position.y()).powf(1.1)
         } else if e.position.y() > bottom {
@@ -848,42 +987,60 @@ impl Terminal {
 
     pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
-        let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
-        let side = mouse_side(position, self.cur_size);
+        let point = grid_point(
+            position,
+            self.last_content.size,
+            self.last_content.display_offset,
+        );
+        // let side = mouse_side(position, self.last_content.size);
 
         if self.mouse_mode(e.shift) {
             if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
                 self.pty_tx.notify(bytes);
             }
         } else if e.button == MouseButton::Left {
-            self.events.push_back(InternalEvent::SetSelection(Some((
-                Selection::new(SelectionType::Simple, point, side),
-                point,
-            ))));
+            self.left_click(e, origin)
         }
     }
 
-    pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) {
+    pub fn left_click(&mut self, e: &DownRegionEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
-
         if !self.mouse_mode(e.shift) {
-            let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
-            let side = mouse_side(position, self.cur_size);
-
-            let selection_type = match e.click_count {
-                0 => return, //This is a release
-                1 => Some(SelectionType::Simple),
-                2 => Some(SelectionType::Semantic),
-                3 => Some(SelectionType::Lines),
-                _ => None,
-            };
+            //Hyperlinks
+            {
+                let mouse_cell_index = content_index_for_mouse(position, &self.last_content);
+                if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
+                    open_uri(link.uri()).log_err();
+                } else {
+                    self.events
+                        .push_back(InternalEvent::FindHyperlink(position, true));
+                }
+            }
 
-            let selection =
-                selection_type.map(|selection_type| Selection::new(selection_type, point, side));
+            // Selections
+            {
+                let point = grid_point(
+                    position,
+                    self.last_content.size,
+                    self.last_content.display_offset,
+                );
+                let side = mouse_side(position, self.last_content.size);
+
+                let selection_type = match e.click_count {
+                    0 => return, //This is a release
+                    1 => Some(SelectionType::Simple),
+                    2 => Some(SelectionType::Semantic),
+                    3 => Some(SelectionType::Lines),
+                    _ => None,
+                };
 
-            if let Some(sel) = selection {
-                self.events
-                    .push_back(InternalEvent::SetSelection(Some((sel, point))));
+                let selection = selection_type
+                    .map(|selection_type| Selection::new(selection_type, point, side));
+
+                if let Some(sel) = selection {
+                    self.events
+                        .push_back(InternalEvent::SetSelection(Some((sel, point))));
+                }
             }
         }
     }
@@ -891,7 +1048,11 @@ impl Terminal {
     pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
         if self.mouse_mode(e.shift) {
-            let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
+            let point = grid_point(
+                position,
+                self.last_content.size,
+                self.last_content.display_offset,
+            );
 
             if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
                 self.pty_tx.notify(bytes);
@@ -901,6 +1062,7 @@ impl Terminal {
             // so let's do that here
             self.copy();
         }
+        self.selection_phase = SelectionPhase::Ended;
         self.last_mouse = None;
     }
 
@@ -910,9 +1072,9 @@ impl Terminal {
 
         if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
             if mouse_mode {
-                let point = mouse_point(
+                let point = grid_point(
                     e.position.sub(origin),
-                    self.cur_size,
+                    self.last_content.size,
                     self.last_content.display_offset,
                 );
 
@@ -940,6 +1102,10 @@ impl Terminal {
         }
     }
 
+    pub fn refresh_hyperlink(&mut self) {
+        self.hyperlink_from_position(self.last_mouse_position);
+    }
+
     fn determine_scroll_lines(
         &mut self,
         e: &ScrollWheelRegionEvent,
@@ -955,20 +1121,22 @@ impl Terminal {
             }
             /* Calculate the appropriate scroll lines */
             Some(gpui::TouchPhase::Moved) => {
-                let old_offset = (self.scroll_px / self.cur_size.line_height) as i32;
+                let old_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
 
                 self.scroll_px += e.delta.y() * scroll_multiplier;
 
-                let new_offset = (self.scroll_px / self.cur_size.line_height) as i32;
+                let new_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
 
                 // Whenever we hit the edges, reset our stored scroll to 0
                 // so we can respond to changes in direction quickly
-                self.scroll_px %= self.cur_size.height;
+                self.scroll_px %= self.last_content.size.height;
 
                 Some(new_offset - old_offset)
             }
             /* Fall back to delta / line_height */
-            None => Some(((e.delta.y() * scroll_multiplier) / self.cur_size.line_height) as i32),
+            None => Some(
+                ((e.delta.y() * scroll_multiplier) / self.last_content.size.line_height) as i32,
+            ),
             _ => None,
         }
     }
@@ -1011,30 +1179,36 @@ impl Entity for Terminal {
     type Event = Event;
 }
 
+/// Based on alacritty/src/display/hint.rs > regex_match_at
+/// Retrieve the match, if the specified point is inside the content matching the regex.
+fn regex_match_at<T>(term: &Term<T>, point: Point, regex: &RegexSearch) -> Option<Match> {
+    visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))
+}
+
+/// Copied from alacritty/src/display/hint.rs:
+/// Iterate over all visible regex matches.
+pub fn visible_regex_match_iter<'a, T>(
+    term: &'a Term<T>,
+    regex: &'a RegexSearch,
+) -> impl Iterator<Item = Match> + 'a {
+    let viewport_start = Line(-(term.grid().display_offset() as i32));
+    let viewport_end = viewport_start + term.bottommost_line();
+    let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
+    let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
+    start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
+    end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
+
+    RegexIter::new(start, end, AlacDirection::Right, term, regex)
+        .skip_while(move |rm| rm.end().line < viewport_start)
+        .take_while(move |rm| rm.start().line <= viewport_end)
+}
+
 fn make_selection(range: &RangeInclusive<Point>) -> Selection {
     let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
     selection.update(*range.end(), AlacDirection::Right);
     selection
 }
 
-/// Copied from alacritty/src/display/hint.rs HintMatches::visible_regex_matches()
-/// Iterate over all visible regex matches.
-// fn visible_search_matches<'a, T>(
-//     term: &'a Term<T>,
-//     regex: &'a RegexSearch,
-// ) -> impl Iterator<Item = Match> + 'a {
-//     let viewport_start = Line(-(term.grid().display_offset() as i32));
-//     let viewport_end = viewport_start + term.bottommost_line();
-//     let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
-//     let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
-//     start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
-//     end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
-
-//     RegexIter::new(start, end, AlacDirection::Right, term, regex)
-//         .skip_while(move |rm| rm.end().line < viewport_start)
-//         .take_while(move |rm| rm.start().line <= viewport_end)
-// }
-
 fn all_search_matches<'a, T>(
     term: &'a Term<T>,
     regex: &'a RegexSearch,
@@ -1044,7 +1218,115 @@ fn all_search_matches<'a, T>(
     RegexIter::new(start, end, AlacDirection::Right, term, regex)
 }
 
+fn content_index_for_mouse<'a>(pos: Vector2F, content: &'a TerminalContent) -> usize {
+    let col = min(
+        (pos.x() / content.size.cell_width()) as usize,
+        content.size.columns() - 1,
+    ) as usize;
+    let line = min(
+        (pos.y() / content.size.line_height()) as usize,
+        content.size.screen_lines() - 1,
+    ) as usize;
+
+    line * content.size.columns() + col
+}
+
+fn open_uri(uri: &str) -> Result<(), std::io::Error> {
+    let mut command = Command::new("open");
+    command.arg(uri);
+
+    unsafe {
+        command
+            .pre_exec(|| {
+                match libc::fork() {
+                    -1 => return Err(io::Error::last_os_error()),
+                    0 => (),
+                    _ => libc::_exit(0),
+                }
+
+                if libc::setsid() == -1 {
+                    return Err(io::Error::last_os_error());
+                }
+
+                Ok(())
+            })
+            .spawn()?
+            .wait()
+            .map(|_| ())
+    }
+}
+
 #[cfg(test)]
 mod tests {
+    use gpui::geometry::vector::vec2f;
+    use rand::{thread_rng, Rng};
+
+    use crate::content_index_for_mouse;
+
+    use self::terminal_test_context::TerminalTestContext;
+
     pub mod terminal_test_context;
+
+    #[test]
+    fn test_mouse_to_cell() {
+        let mut rng = thread_rng();
+
+        for _ in 0..10 {
+            let viewport_cells = rng.gen_range(5..50);
+            let cell_size = rng.gen_range(5.0..20.0);
+
+            let size = crate::TerminalSize {
+                cell_width: cell_size,
+                line_height: cell_size,
+                height: cell_size * (viewport_cells as f32),
+                width: cell_size * (viewport_cells as f32),
+            };
+
+            let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng);
+
+            for i in 0..(viewport_cells - 1) {
+                let i = i as usize;
+                for j in 0..(viewport_cells - 1) {
+                    let j = j as usize;
+                    let min_row = i as f32 * cell_size;
+                    let max_row = (i + 1) as f32 * cell_size;
+                    let min_col = j as f32 * cell_size;
+                    let max_col = (j + 1) as f32 * cell_size;
+
+                    let mouse_pos = vec2f(
+                        rng.gen_range(min_row..max_row),
+                        rng.gen_range(min_col..max_col),
+                    );
+
+                    assert_eq!(
+                        content.cells[content_index_for_mouse(mouse_pos, &content)].c,
+                        cells[j][i]
+                    );
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn test_mouse_to_cell_clamp() {
+        let mut rng = thread_rng();
+
+        let size = crate::TerminalSize {
+            cell_width: 10.,
+            line_height: 10.,
+            height: 100.,
+            width: 100.,
+        };
+
+        let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng);
+
+        assert_eq!(
+            content.cells[content_index_for_mouse(vec2f(-10., -10.), &content)].c,
+            cells[0][0]
+        );
+        assert_eq!(
+            content.cells[content_index_for_mouse(vec2f(1000., 1000.), &content)].c,
+            cells[9][9]
+        );
+    }
 }

crates/terminal/src/terminal_container_view.rs 🔗

@@ -11,7 +11,6 @@ use util::truncate_and_trailoff;
 use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
 use workspace::{Item, ItemEvent, ToolbarItemLocation, Workspace};
 
-use crate::TerminalSize;
 use project::{LocalWorktree, Project, ProjectPath};
 use settings::{AlternateScroll, Settings, WorkingDirectory};
 use smallvec::SmallVec;
@@ -87,9 +86,6 @@ impl TerminalContainer {
         modal: bool,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        //The exact size here doesn't matter, the terminal will be resized on the first layout
-        let size_info = TerminalSize::default();
-
         let settings = cx.global::<Settings>();
         let shell = settings.terminal_overrides.shell.clone();
         let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
@@ -111,7 +107,6 @@ impl TerminalContainer {
             working_directory.clone(),
             shell,
             envs,
-            size_info,
             settings.terminal_overrides.blinking.clone(),
             scroll,
             cx.window_id(),

crates/terminal/src/terminal_element.rs 🔗

@@ -7,15 +7,17 @@ use alacritty_terminal::{
 use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
 use gpui::{
     color::Color,
-    fonts::{Properties, Style::Italic, TextStyle, Underline, Weight},
+    elements::{Empty, Overlay},
+    fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight},
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
     serde_json::json,
     text_layout::{Line, RunStyle},
-    Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, MouseRegion,
-    PaintContext, Quad, TextLayoutCache, WeakModelHandle, WeakViewHandle,
+    Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton,
+    MouseRegion, PaintContext, Quad, SizeConstraint, TextLayoutCache, WeakModelHandle,
+    WeakViewHandle,
 };
 use itertools::Itertools;
 use ordered_float::OrderedFloat;
@@ -42,6 +44,7 @@ pub struct LayoutState {
     size: TerminalSize,
     mode: TermMode,
     display_offset: usize,
+    hyperlink_tooltip: Option<ElementBox>,
 }
 
 ///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
@@ -180,6 +183,7 @@ impl TerminalElement {
         text_layout_cache: &TextLayoutCache,
         font_cache: &FontCache,
         modal: bool,
+        hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
     ) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
         let mut cells = vec![];
         let mut rects = vec![];
@@ -237,7 +241,7 @@ impl TerminalElement {
                 //Layout current cell text
                 {
                     let cell_text = &cell.c.to_string();
-                    if cell_text != " " {
+                    if !is_blank(&cell) {
                         let cell_style = TerminalElement::cell_style(
                             &cell,
                             fg,
@@ -245,6 +249,7 @@ impl TerminalElement {
                             text_style,
                             font_cache,
                             modal,
+                            hyperlink,
                         );
 
                         let layout_cell = text_layout_cache.layout_str(
@@ -257,8 +262,8 @@ impl TerminalElement {
                             Point::new(line_index as i32, cell.point.column.0 as i32),
                             layout_cell,
                         ))
-                    }
-                };
+                    };
+                }
             }
 
             if cur_rect.is_some() {
@@ -304,11 +309,12 @@ impl TerminalElement {
         text_style: &TextStyle,
         font_cache: &FontCache,
         modal: bool,
+        hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
     ) -> RunStyle {
         let flags = indexed.cell.flags;
         let fg = convert_color(&fg, &style.colors, modal);
 
-        let underline = flags
+        let mut underline = flags
             .intersects(Flags::ALL_UNDERLINES)
             .then(|| Underline {
                 color: Some(fg),
@@ -317,6 +323,12 @@ impl TerminalElement {
             })
             .unwrap_or_default();
 
+        if indexed.cell.hyperlink().is_some() {
+            if underline.thickness == OrderedFloat(0.) {
+                underline.thickness = OrderedFloat(1.);
+            }
+        }
+
         let mut properties = Properties::new();
         if indexed
             .flags
@@ -332,11 +344,25 @@ impl TerminalElement {
             .select_font(text_style.font_family_id, &properties)
             .unwrap_or(text_style.font_id);
 
-        RunStyle {
+        let mut result = RunStyle {
             color: fg,
             font_id,
             underline,
+        };
+
+        if let Some((style, range)) = hyperlink {
+            if range.contains(&indexed.point) {
+                if let Some(underline) = style.underline {
+                    result.underline = underline;
+                }
+
+                if let Some(color) = style.color {
+                    result.color = color;
+                }
+            }
         }
+
+        result
     }
 
     fn generic_button_handler<E>(
@@ -366,7 +392,7 @@ impl TerminalElement {
     ) {
         let connection = self.terminal;
 
-        let mut region = MouseRegion::new::<Self>(view_id, view_id, visible_bounds);
+        let mut region = MouseRegion::new::<Self>(view_id, 0, visible_bounds);
 
         // Terminal Emulator controlled behavior:
         region = region
@@ -403,17 +429,6 @@ impl TerminalElement {
                     },
                 ),
             )
-            // Handle click based selections
-            .on_click(
-                MouseButton::Left,
-                TerminalElement::generic_button_handler(
-                    connection,
-                    origin,
-                    move |terminal, origin, e, _cx| {
-                        terminal.left_click(&e, origin);
-                    },
-                ),
-            )
             // Context menu
             .on_click(MouseButton::Right, move |e, cx| {
                 let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx.app) {
@@ -428,6 +443,16 @@ impl TerminalElement {
                     });
                 }
             })
+            .on_move(move |event, cx| {
+                if cx.is_parent_view_focused() {
+                    if let Some(conn_handle) = connection.upgrade(cx.app) {
+                        conn_handle.update(cx.app, |terminal, cx| {
+                            terminal.mouse_move(&event, origin);
+                            cx.notify();
+                        })
+                    }
+                }
+            })
             .on_scroll(TerminalElement::generic_button_handler(
                 connection,
                 origin,
@@ -481,21 +506,6 @@ impl TerminalElement {
                     ),
                 )
         }
-        //Mouse move manages both dragging and motion events
-        if mode.intersects(TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION) {
-            region = region
-                //TODO: This does not fire on right-mouse-down-move events.
-                .on_move(move |event, cx| {
-                    if cx.is_parent_view_focused() {
-                        if let Some(conn_handle) = connection.upgrade(cx.app) {
-                            conn_handle.update(cx.app, |terminal, cx| {
-                                terminal.mouse_move(&event, origin);
-                                cx.notify();
-                            })
-                        }
-                    }
-                })
-        }
 
         cx.scene.push_mouse_region(region);
     }
@@ -547,6 +557,9 @@ impl Element for TerminalElement {
 
         //Setup layout information
         let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
+        let link_style = settings.theme.editor.link_definition;
+        let tooltip_style = settings.theme.tooltip.clone();
+
         let text_style = TerminalElement::make_text_style(font_cache, settings);
         let selection_color = settings.theme.editor.selection.selection;
         let match_color = settings.theme.search.match_background;
@@ -569,9 +582,34 @@ impl Element for TerminalElement {
         };
         let terminal_handle = self.terminal.upgrade(cx).unwrap();
 
-        terminal_handle.update(cx.app, |terminal, cx| {
+        let last_hovered_hyperlink = terminal_handle.update(cx.app, |terminal, cx| {
             terminal.set_size(dimensions);
-            terminal.try_sync(cx)
+            terminal.try_sync(cx);
+            terminal.last_content.last_hovered_hyperlink.clone()
+        });
+
+        let view_handle = self.view.clone();
+        let hyperlink_tooltip = last_hovered_hyperlink.and_then(|(uri, _, id)| {
+            // last_mouse.and_then(|_last_mouse| {
+            view_handle.upgrade(cx).map(|handle| {
+                let mut tooltip = cx.render(&handle, |_, cx| {
+                    Overlay::new(
+                        Empty::new()
+                            .contained()
+                            .constrained()
+                            .with_width(dimensions.width())
+                            .with_height(dimensions.height())
+                            .with_tooltip::<TerminalElement, _>(id, uri, None, tooltip_style, cx)
+                            .boxed(),
+                    )
+                    .with_position_mode(gpui::elements::OverlayPositionMode::Local)
+                    .boxed()
+                });
+
+                tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
+                tooltip
+            })
+            // })
         });
 
         let TerminalContent {
@@ -581,8 +619,9 @@ impl Element for TerminalElement {
             cursor_char,
             selection,
             cursor,
+            last_hovered_hyperlink,
             ..
-        } = &terminal_handle.read(cx).last_content;
+        } = { &terminal_handle.read(cx).last_content };
 
         // searches, highlights to a single range representations
         let mut relative_highlighted_ranges = Vec::new();
@@ -602,6 +641,9 @@ impl Element for TerminalElement {
             cx.text_layout_cache,
             cx.font_cache(),
             self.modal,
+            last_hovered_hyperlink
+                .as_ref()
+                .map(|(_, range, _)| (link_style, range)),
         );
 
         //Layout cursor. Rectangle is used for IME, so we should lay it out even
@@ -633,10 +675,11 @@ impl Element for TerminalElement {
                 )
             };
 
+            let focused = self.focused;
             TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
                 move |(cursor_position, block_width)| {
                     let shape = match cursor.shape {
-                        AlacCursorShape::Block if !self.focused => CursorShape::Hollow,
+                        AlacCursorShape::Block if !focused => CursorShape::Hollow,
                         AlacCursorShape::Block => CursorShape::Block,
                         AlacCursorShape::Underline => CursorShape::Underscore,
                         AlacCursorShape::Beam => CursorShape::Bar,
@@ -669,6 +712,7 @@ impl Element for TerminalElement {
                 relative_highlighted_ranges,
                 mode: *mode,
                 display_offset: *display_offset,
+                hyperlink_tooltip,
             },
         )
     }
@@ -691,7 +735,11 @@ impl Element for TerminalElement {
 
             cx.scene.push_cursor_region(gpui::CursorRegion {
                 bounds,
-                style: gpui::CursorStyle::IBeam,
+                style: if layout.hyperlink_tooltip.is_some() {
+                    gpui::CursorStyle::PointingHand
+                } else {
+                    gpui::CursorStyle::IBeam
+                },
             });
 
             cx.paint_layer(clip_bounds, |cx| {
@@ -743,6 +791,10 @@ impl Element for TerminalElement {
                     })
                 }
             }
+
+            if let Some(element) = &mut layout.hyperlink_tooltip {
+                element.paint(origin, visible_bounds, cx)
+            }
         });
     }
 
@@ -824,6 +876,29 @@ impl Element for TerminalElement {
     }
 }
 
+fn is_blank(cell: &IndexedCell) -> bool {
+    if cell.c != ' ' {
+        return false;
+    }
+
+    if cell.bg != AnsiColor::Named(NamedColor::Background) {
+        return false;
+    }
+
+    if cell.hyperlink().is_some() {
+        return false;
+    }
+
+    if cell
+        .flags
+        .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT)
+    {
+        return false;
+    }
+
+    return true;
+}
+
 fn to_highlighted_range_lines(
     range: &RangeInclusive<Point>,
     layout: &LayoutState,

crates/terminal/src/terminal_view.rs 🔗

@@ -6,13 +6,15 @@ use gpui::{
     actions,
     elements::{AnchorCorner, ChildView, ParentElement, Stack},
     geometry::vector::Vector2F,
-    impl_internal_actions,
+    impl_actions, impl_internal_actions,
     keymap::Keystroke,
     AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task,
     View, ViewContext, ViewHandle,
 };
+use serde::Deserialize;
 use settings::{Settings, TerminalBlink};
 use smol::Timer;
+use util::ResultExt;
 use workspace::pane;
 
 use crate::{terminal_element::TerminalElement, Event, Terminal};
@@ -28,6 +30,12 @@ pub struct DeployContextMenu {
     pub position: Vector2F,
 }
 
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct SendText(String);
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct SendKeystroke(String);
+
 actions!(
     terminal,
     [
@@ -43,16 +51,15 @@ actions!(
         SearchTest
     ]
 );
+
+impl_actions!(terminal, [SendText, SendKeystroke]);
+
 impl_internal_actions!(project_panel, [DeployContextMenu]);
 
 pub fn init(cx: &mut MutableAppContext) {
-    //Global binding overrrides
-    cx.add_action(TerminalView::ctrl_c);
-    cx.add_action(TerminalView::up);
-    cx.add_action(TerminalView::down);
-    cx.add_action(TerminalView::escape);
-    cx.add_action(TerminalView::enter);
     //Useful terminal views
+    cx.add_action(TerminalView::send_text);
+    cx.add_action(TerminalView::send_keystroke);
     cx.add_action(TerminalView::deploy_context_menu);
     cx.add_action(TerminalView::copy);
     cx.add_action(TerminalView::paste);
@@ -283,44 +290,26 @@ impl TerminalView {
         }
     }
 
-    ///Synthesize the keyboard event corresponding to 'up'
-    fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
-        self.clear_bel(cx);
-        self.terminal.update(cx, |term, _| {
-            term.try_keystroke(&Keystroke::parse("up").unwrap(), false)
-        });
-    }
-
-    ///Synthesize the keyboard event corresponding to 'down'
-    fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
-        self.clear_bel(cx);
-        self.terminal.update(cx, |term, _| {
-            term.try_keystroke(&Keystroke::parse("down").unwrap(), false)
-        });
-    }
-
-    ///Synthesize the keyboard event corresponding to 'ctrl-c'
-    fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
-        self.clear_bel(cx);
-        self.terminal.update(cx, |term, _| {
-            term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap(), false)
-        });
-    }
-
-    ///Synthesize the keyboard event corresponding to 'escape'
-    fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
+    fn send_text(&mut self, text: &SendText, cx: &mut ViewContext<Self>) {
         self.clear_bel(cx);
         self.terminal.update(cx, |term, _| {
-            term.try_keystroke(&Keystroke::parse("escape").unwrap(), false)
+            term.input(text.0.to_string());
         });
     }
 
-    ///Synthesize the keyboard event corresponding to 'enter'
-    fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
-        self.clear_bel(cx);
-        self.terminal.update(cx, |term, _| {
-            term.try_keystroke(&Keystroke::parse("enter").unwrap(), false)
-        });
+    fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext<Self>) {
+        if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
+            self.clear_bel(cx);
+            self.terminal.update(cx, |term, cx| {
+                term.try_keystroke(
+                    &keystroke,
+                    cx.global::<Settings>()
+                        .terminal_overrides
+                        .option_as_meta
+                        .unwrap_or(false),
+                );
+            });
+        }
     }
 }
 
@@ -362,7 +351,9 @@ impl View for TerminalView {
     }
 
     fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        self.terminal.read(cx).focus_out();
+        self.terminal.update(cx, |terminal, _| {
+            terminal.focus_out();
+        });
         cx.notify();
     }
 

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

@@ -1,10 +1,17 @@
 use std::{path::Path, time::Duration};
 
+use alacritty_terminal::{
+    index::{Column, Line, Point},
+    term::cell::Cell,
+};
 use gpui::{ModelHandle, TestAppContext, ViewHandle};
 
 use project::{Entry, Project, ProjectPath, Worktree};
+use rand::{rngs::ThreadRng, Rng};
 use workspace::{AppState, Workspace};
 
+use crate::{IndexedCell, TerminalContent, TerminalSize};
+
 pub struct TerminalTestContext<'a> {
     pub cx: &'a mut TestAppContext,
 }
@@ -88,6 +95,39 @@ impl<'a> TerminalTestContext<'a> {
             project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
         });
     }
+
+    pub fn create_terminal_content(
+        size: TerminalSize,
+        rng: &mut ThreadRng,
+    ) -> (TerminalContent, Vec<Vec<char>>) {
+        let mut ic = Vec::new();
+        let mut cells = Vec::new();
+
+        for row in 0..((size.height() / size.line_height()) as usize) {
+            let mut row_vec = Vec::new();
+            for col in 0..((size.width() / size.cell_width()) as usize) {
+                let cell_char = rng.gen();
+                ic.push(IndexedCell {
+                    point: Point::new(Line(row as i32), Column(col)),
+                    cell: Cell {
+                        c: cell_char,
+                        ..Default::default()
+                    },
+                });
+                row_vec.push(cell_char)
+            }
+            cells.push(row_vec)
+        }
+
+        (
+            TerminalContent {
+                cells: ic,
+                size,
+                ..Default::default()
+            },
+            cells,
+        )
+    }
 }
 
 impl<'a> Drop for TerminalTestContext<'a> {

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.53.1"
+version = "0.55.0"
 
 [lib]
 name = "zed"

crates/zed/src/languages/json.rs 🔗

@@ -1,26 +1,22 @@
-use super::installation::{npm_install_packages, npm_package_latest_version};
-use anyhow::{anyhow, Context, Result};
+use super::installation::{latest_github_release, GitHubLspBinaryVersion};
+use anyhow::{anyhow, Result};
+use async_compression::futures::bufread::GzipDecoder;
 use async_trait::async_trait;
 use client::http::HttpClient;
 use collections::HashMap;
-use futures::StreamExt;
+use futures::{io::BufReader, StreamExt};
 use language::{LanguageServerName, LspAdapter};
 use serde_json::json;
-use smol::fs;
-use std::{any::Any, path::PathBuf, sync::Arc};
+use smol::fs::{self, File};
+use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
 use util::ResultExt;
 
 pub struct JsonLspAdapter;
 
-impl JsonLspAdapter {
-    const BIN_PATH: &'static str =
-        "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
-}
-
 #[async_trait]
 impl LspAdapter for JsonLspAdapter {
     async fn name(&self) -> LanguageServerName {
-        LanguageServerName("vscode-json-languageserver".into())
+        LanguageServerName("json-language-server".into())
     }
 
     async fn server_args(&self) -> Vec<String> {
@@ -29,28 +25,46 @@ impl LspAdapter for JsonLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
-    ) -> Result<Box<dyn 'static + Any + Send>> {
-        Ok(Box::new(npm_package_latest_version("vscode-json-languageserver").await?) as Box<_>)
+        http: Arc<dyn HttpClient>,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release = latest_github_release("zed-industries/json-language-server", http).await?;
+        let asset_name = format!("json-language-server-darwin-{}.gz", consts::ARCH);
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+        let version = GitHubLspBinaryVersion {
+            name: release.name,
+            url: asset.browser_download_url.clone(),
+        };
+        Ok(Box::new(version) as Box<_>)
     }
 
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
+        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> Result<PathBuf> {
-        let version = version.downcast::<String>().unwrap();
-        let version_dir = container_dir.join(version.as_str());
-        fs::create_dir_all(&version_dir)
-            .await
-            .context("failed to create version directory")?;
-        let binary_path = version_dir.join(Self::BIN_PATH);
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+        let destination_path = container_dir.join(format!(
+            "json-language-server-{}-{}",
+            version.name,
+            consts::ARCH
+        ));
 
-        if fs::metadata(&binary_path).await.is_err() {
-            npm_install_packages(
-                [("vscode-json-languageserver", version.as_str())],
-                &version_dir,
+        if fs::metadata(&destination_path).await.is_err() {
+            let mut response = http
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let mut file = File::create(&destination_path).await?;
+            futures::io::copy(decompressed_bytes, &mut file).await?;
+            fs::set_permissions(
+                &destination_path,
+                <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
             )
             .await?;
 
@@ -58,37 +72,25 @@ impl LspAdapter for JsonLspAdapter {
                 while let Some(entry) = entries.next().await {
                     if let Some(entry) = entry.log_err() {
                         let entry_path = entry.path();
-                        if entry_path.as_path() != version_dir {
-                            fs::remove_dir_all(&entry_path).await.log_err();
+                        if entry_path.as_path() != destination_path {
+                            fs::remove_file(&entry_path).await.log_err();
                         }
                     }
                 }
             }
         }
 
-        Ok(binary_path)
+        Ok(destination_path)
     }
 
     async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
         (|| async move {
-            let mut last_version_dir = None;
+            let mut last = None;
             let mut entries = fs::read_dir(&container_dir).await?;
             while let Some(entry) = entries.next().await {
-                let entry = entry?;
-                if entry.file_type().await?.is_dir() {
-                    last_version_dir = Some(entry.path());
-                }
-            }
-            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let bin_path = last_version_dir.join(Self::BIN_PATH);
-            if bin_path.exists() {
-                Ok(bin_path)
-            } else {
-                Err(anyhow!(
-                    "missing executable in directory {:?}",
-                    last_version_dir
-                ))
+                last = Some(entry?.path());
             }
+            last.ok_or_else(|| anyhow!("no cached binary"))
         })()
         .await
         .log_err()

script/bundle 🔗

@@ -3,6 +3,7 @@
 set -e
 
 export ZED_BUNDLE=true
+export MACOSX_DEPLOYMENT_TARGET=10.14
 
 echo "Installing cargo bundle"
 cargo install cargo-bundle --version 0.5.0