diff --git a/Cargo.lock b/Cargo.lock index 17ace3f47b3b1e846b766b91a9606ad64b716efd..363ee93c14adde698e2d2b578c275d6c517ce7ef 100644 --- a/Cargo.lock +++ b/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", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 7a25dc19d3bcd0ff491f31d9ae1609b1ba267f3e..a0bc8c39e6b2d29089292daa2863705a32e05ea4 100644 --- a/assets/keymaps/default.json +++ b/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" + ] } } ] \ No newline at end of file diff --git a/assets/settings/default.json b/assets/settings/default.json index d8efdc41ff02851e28a8536c4bb63acc8c393ae0..a12cf44d94ae29c45851d3b39a3c4caa32008f96 100644 --- a/assets/settings/default.json +++ b/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: // diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 2b9dd25a901b02915746c6779841b19ad4056223..3c9886dc16d9135fb51d3d38fd5c6388f853c691 100644 --- a/crates/collab/src/integration_tests.rs +++ b/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(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 07a9fc011fc28bcca82fa4ab206bf0345bc411ce..c6cfd887db87d5c0ed6db3f80aa18b42edc89c1f 100644 --- a/crates/editor/src/editor.rs +++ b/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>> { + 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, + cx: &mut ViewContext<'_, Self>, + ) -> Task> { + 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) { 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::(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::(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( diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 6c004f20078ef8964210b689385ecafa48d08092..f63ffc3d7cce0eb920818b36747961290d2865b2 100644 --- a/crates/editor/src/items.rs +++ b/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, cx: &mut ViewContext, ) -> Task> { + 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 => { diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index e68810fb360527dd2e047d12c98f75632bac6105..227f946ac6529e9d4e33f8acb5c535cc1805699d 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/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, - scroll_position: f32, + scroll_to: Cell>, + scroll_position: Cell, } pub struct Flex { axis: Axis, children: Vec, - scroll_state: Option>, + scroll_state: Option<(ElementStateHandle>, usize)>, } impl Flex { @@ -52,9 +51,9 @@ impl Flex { Tag: 'static, V: View, { - let scroll_state = cx.default_element_state::(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::>(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::(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 } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 57436c256bd9ee241dc78d07206ba8c3655e2c54..d752a52a1666110a7fc1cc2160988aac86429212 100644 --- a/crates/gpui/src/elements/list.rs +++ b/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::(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 { diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index bb958ce99c2728eed3e0908ee040db5b0ccfaa29..5b3b9b13f6c77ae2f3fe2b517a3136e83729e4e0 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/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 MouseEventHandler { 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 diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index 20b6c75c8fd6906713a5cf2e2c0afc101eefe992..d47a39e958b8cc18ac12e5b32fa52c602ad0d714 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -14,6 +14,7 @@ pub struct Overlay { anchor_position: Option, 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 => { diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 55ab7e44d25e0fb8edfdcdb210a6630ce62bbad6..c86230a5e158f30c5cef1716b67113a664b3a4df 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/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>, diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 103cb00d8cc1f46d77d85928e95aea666c589bed..6bc35c06922638c8221805d7a945b0c5f747ec65 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/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, + 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.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 } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 73b5e3595d6ea718a817df3897cd1bd9d754a9f3..6841c561d01bc0cfb21b3e6301934ef80829da50 100644 --- a/crates/project/src/project.rs +++ b/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.add_model_message_handler(Self::handle_request_join_project); @@ -3046,6 +3062,7 @@ impl Project { &self, buffers: HashSet>, push_to_history: bool, + trigger: FormatTrigger, cx: &mut ModelContext, ) -> Task> { 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::(); 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?; diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 4cf7e38a139037ccd294ebc4cc377724779023cb..7840829b4461bc7ca497f7bf1bcc7b34e397f0f8 100644 --- a/crates/rpc/proto/zed.proto +++ b/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 { diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 308e5b0f4349c281616c56884279e4aae622a93e..b9f6e6a7390a759b4317ed53bd7309d47a9e37b3 100644 --- a/crates/rpc/src/rpc.rs +++ b/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; diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index d2b076b0126e6b4ebe56f3ea340e33954cd4bd98..e346ff60e6ba89a304e43b7e8696c90d09ac88cb 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -59,6 +59,7 @@ pub struct EditorSettings { pub soft_wrap: Option, pub preferred_line_length: Option, pub format_on_save: Option, + pub formatter: Option, pub enable_language_server: Option, } @@ -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, + }, +} + #[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(value: Option) -> Option { 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(), diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 7831be1c5af47f1dd9e80bac7f5be5d070e21e51..a0b5231501228d699631e4146b2acb3051656903 100644 --- a/crates/terminal/Cargo.toml +++ b/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" diff --git a/crates/terminal/src/mappings/mouse.rs b/crates/terminal/src/mappings/mouse.rs index 7d92036b714abc7085134fced7f00798dc8050e9..1616540cff65aaa245551ef7a2da37f7da2b4613 100644 --- a/crates/terminal/src/mappings/mouse.rs +++ b/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( diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index b05750b84157ca0c1211fc757740273c16c78de0..473bbd4f52aa2ebbf4885830cded6d44252fa75c 100644 --- a/crates/terminal/src/terminal.rs +++ b/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, shell: Option, env: Option>, - initial_size: TerminalSize, blink_settings: Option, 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, cursor: RenderableCursor, cursor_char: char, + size: TerminalSize, + last_hovered_hyperlink: Option<(String, RangeInclusive, 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>>, events: VecDeque, + /// 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, pub matches: Vec>, - cur_size: TerminalSize, last_content: TerminalContent, last_synced: Instant, sync_task: Option>, @@ -485,6 +517,8 @@ pub struct Terminal { shell_fd: u32, foreground_process_info: Option, 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, usize)>, + url: String, + url_match: RangeInclusive, + ) { + 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) { @@ -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) -> TerminalContent { + fn make_content(term: &Term, 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) { + 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 { //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(term: &Term, point: Point, regex: &RegexSearch) -> Option { + 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, + regex: &'a RegexSearch, +) -> impl Iterator + '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) -> 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, -// regex: &'a RegexSearch, -// ) -> impl Iterator + '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, 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] + ); + } } diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal/src/terminal_container_view.rs index 1aebd1f5e7a06200594df31bd4bc2dd6fc42b356..e0fe6ef6cbf275d768970da9d926b1adda7b509f 100644 --- a/crates/terminal/src/terminal_container_view.rs +++ b/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 { - //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::(); 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(), diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 00b6c7e39aa6cc60ea80aeba6a7e22eb57f1ee12..8cb193555d9a6974edc2514a8553705289931ae3 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/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, } ///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)>, ) -> (Vec, Vec) { 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)>, ) -> 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( @@ -366,7 +392,7 @@ impl TerminalElement { ) { let connection = self.terminal; - let mut region = MouseRegion::new::(view_id, view_id, visible_bounds); + let mut region = MouseRegion::new::(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::(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, layout: &LayoutState, diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs index fc8bf20ca7e994e41641c027f89fdf28f54fbade..274207604507d36029b799bad08d19dbbd57cc29 100644 --- a/crates/terminal/src/terminal_view.rs +++ b/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.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.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.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) { + fn send_text(&mut self, text: &SendText, cx: &mut ViewContext) { 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.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) { + 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::() + .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.terminal.read(cx).focus_out(); + self.terminal.update(cx, |terminal, _| { + terminal.focus_out(); + }); cx.notify(); } diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index f9ee6e808266092f5767faaf4401c3392ca5cc31..3e3d1243d5fbed3c3bb407a32684b6d85857deeb 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/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>) { + 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> { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 446139eaafbd25c8f4c4374b0f6538ee44586b58..dc2b0abd03e59e753f13eb323a372e4c817ad079 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.53.1" +version = "0.55.0" [lib] name = "zed" diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 7b6569d3362fdb1e551f194f45a1201ac8f4e7fe..d7f87bee6c0d64cf936eedac874581e33690e25d 100644 --- a/crates/zed/src/languages/json.rs +++ b/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 { @@ -29,28 +25,46 @@ impl LspAdapter for JsonLspAdapter { async fn fetch_latest_server_version( &self, - _: Arc, - ) -> Result> { - Ok(Box::new(npm_package_latest_version("vscode-json-languageserver").await?) as Box<_>) + http: Arc, + ) -> Result> { + 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, - _: Arc, + http: Arc, container_dir: PathBuf, ) -> Result { - let version = version.downcast::().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::().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, + ::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 { (|| 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() diff --git a/script/bundle b/script/bundle index 42ed7d9244a1b6486569bfebb8354f90fc2b84fd..f3fc4e74341f24118647b1d04e796012ff84d6b5 100755 --- a/script/bundle +++ b/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