Merge branch 'main' into new-signup-flow

This commit is contained in:
Max Brunsfeld 2022-09-27 15:35:05 -07:00
commit 5d0b6a3da7
26 changed files with 1030 additions and 366 deletions

5
Cargo.lock generated
View file

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

View file

@ -93,6 +93,7 @@
"cmd-shift-down": "editor::SelectToEnd", "cmd-shift-down": "editor::SelectToEnd",
"cmd-a": "editor::SelectAll", "cmd-a": "editor::SelectAll",
"cmd-l": "editor::SelectLine", "cmd-l": "editor::SelectLine",
"cmd-shift-i": "editor::Format",
"cmd-shift-left": [ "cmd-shift-left": [
"editor::SelectToBeginningOfLine", "editor::SelectToBeginningOfLine",
{ {
@ -427,17 +428,45 @@
{ {
"context": "Terminal", "context": "Terminal",
"bindings": { "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", "ctrl-cmd-space": "terminal::ShowCharacterPalette",
"cmd-c": "terminal::Copy", "cmd-c": "terminal::Copy",
"cmd-v": "terminal::Paste", "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"
]
} }
} }
] ]

View file

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

View file

@ -36,7 +36,7 @@ use project::{
use rand::prelude::*; use rand::prelude::*;
use rpc::PeerId; use rpc::PeerId;
use serde_json::json; use serde_json::json;
use settings::{FormatOnSave, Settings}; use settings::{Formatter, Settings};
use sqlx::types::time::OffsetDateTime; use sqlx::types::time::OffsetDateTime;
use std::{ use std::{
cell::RefCell, cell::RefCell,
@ -1990,6 +1990,8 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { 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 mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").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 project_b
.update(cx_b, |project, cx| { .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 .await
.unwrap(); .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. // host's configuration is honored as opposed to using the guest's settings.
cx_a.update(|cx| { cx_a.update(|cx| {
cx.update_global(|settings: &mut Settings, _| { 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(), command: "awk".to_string(),
arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".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 project_b
.update(cx_b, |project, cx| { .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 .await
.unwrap(); .unwrap();

View file

@ -19,6 +19,7 @@ use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
pub use display_map::DisplayPoint; pub use display_map::DisplayPoint;
use display_map::*; use display_map::*;
pub use element::*; pub use element::*;
use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
actions, actions,
@ -50,7 +51,7 @@ pub use multi_buffer::{
}; };
use multi_buffer::{MultiBufferChunks, ToOffsetUtf16}; use multi_buffer::{MultiBufferChunks, ToOffsetUtf16};
use ordered_float::OrderedFloat; 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 selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Settings; use settings::Settings;
@ -77,6 +78,8 @@ const MAX_LINE_LEN: usize = 1024;
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
const MAX_SELECTION_HISTORY_LEN: usize = 1024; const MAX_SELECTION_HISTORY_LEN: usize = 1024;
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
#[derive(Clone, Deserialize, PartialEq, Default)] #[derive(Clone, Deserialize, PartialEq, Default)]
pub struct SelectNext { pub struct SelectNext {
#[serde(default)] #[serde(default)]
@ -205,6 +208,7 @@ actions!(
OpenExcerpts, OpenExcerpts,
RestartLanguageServer, RestartLanguageServer,
Hover, Hover,
Format,
] ]
); );
@ -311,6 +315,7 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Editor::toggle_code_actions); cx.add_action(Editor::toggle_code_actions);
cx.add_action(Editor::open_excerpts); cx.add_action(Editor::open_excerpts);
cx.add_action(Editor::jump); cx.add_action(Editor::jump);
cx.add_async_action(Editor::format);
cx.add_action(Editor::restart_language_server); cx.add_action(Editor::restart_language_server);
cx.add_action(Editor::show_character_palette); cx.add_action(Editor::show_character_palette);
cx.add_async_action(Editor::confirm_completion); cx.add_async_action(Editor::confirm_completion);
@ -1577,17 +1582,20 @@ impl Editor {
let start; let start;
let end; let end;
let mode; let mode;
let auto_scroll;
match click_count { match click_count {
1 => { 1 => {
start = buffer.anchor_before(position.to_point(&display_map)); start = buffer.anchor_before(position.to_point(&display_map));
end = start.clone(); end = start.clone();
mode = SelectMode::Character; mode = SelectMode::Character;
auto_scroll = true;
} }
2 => { 2 => {
let range = movement::surrounding_word(&display_map, position); let range = movement::surrounding_word(&display_map, position);
start = buffer.anchor_before(range.start.to_point(&display_map)); start = buffer.anchor_before(range.start.to_point(&display_map));
end = buffer.anchor_before(range.end.to_point(&display_map)); end = buffer.anchor_before(range.end.to_point(&display_map));
mode = SelectMode::Word(start.clone()..end.clone()); mode = SelectMode::Word(start.clone()..end.clone());
auto_scroll = true;
} }
3 => { 3 => {
let position = display_map let position = display_map
@ -1601,15 +1609,17 @@ impl Editor {
start = buffer.anchor_before(line_start); start = buffer.anchor_before(line_start);
end = buffer.anchor_before(next_line_start); end = buffer.anchor_before(next_line_start);
mode = SelectMode::Line(start.clone()..end.clone()); mode = SelectMode::Line(start.clone()..end.clone());
auto_scroll = true;
} }
_ => { _ => {
start = buffer.anchor_before(0); start = buffer.anchor_before(0);
end = buffer.anchor_before(buffer.len()); end = buffer.anchor_before(buffer.len());
mode = SelectMode::All; 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 { if !add {
s.clear_disjoint(); s.clear_disjoint();
} else if click_count > 1 { } else if click_count > 1 {
@ -5170,6 +5180,51 @@ impl Editor {
self.pending_rename.as_ref() 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>) { fn restart_language_server(&mut self, _: &RestartLanguageServer, cx: &mut ViewContext<Self>) {
if let Some(project) = self.project.clone() { if let Some(project) = self.project.clone() {
self.buffer.update(cx, |multi_buffer, cx| { self.buffer.update(cx, |multi_buffer, cx| {
@ -10103,7 +10158,7 @@ mod tests {
unreachable!() unreachable!()
}); });
let save = cx.update(|cx| editor.save(project.clone(), cx)); 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(); cx.foreground().start_waiting();
save.await.unwrap(); save.await.unwrap();
assert_eq!( assert_eq!(
@ -10219,7 +10274,7 @@ mod tests {
}, },
); );
let save = cx.update(|cx| editor.save(project.clone(), cx)); 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(); cx.foreground().start_waiting();
save.await.unwrap(); save.await.unwrap();
assert_eq!( assert_eq!(
@ -10257,6 +10312,87 @@ mod tests {
save.await.unwrap(); 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] #[gpui::test]
async fn test_completion(cx: &mut gpui::TestAppContext) { async fn test_completion(cx: &mut gpui::TestAppContext) {
let mut cx = EditorLspTestContext::new_rust( let mut cx = EditorLspTestContext::new_rust(

View file

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

View file

@ -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::{ use crate::{
json::{self, ToJson, Value}, json::{self, ToJson, Value},
presenter::MeasurementContext, presenter::MeasurementContext,
Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
LayoutContext, MouseMovedEvent, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint, LayoutContext, PaintContext, RenderContext, SizeConstraint, Vector2FExt, View,
Vector2FExt, View,
}; };
use pathfinder_geometry::{ use pathfinder_geometry::{
rect::RectF, rect::RectF,
@ -15,14 +14,14 @@ use serde_json::json;
#[derive(Default)] #[derive(Default)]
struct ScrollState { struct ScrollState {
scroll_to: Option<usize>, scroll_to: Cell<Option<usize>>,
scroll_position: f32, scroll_position: Cell<f32>,
} }
pub struct Flex { pub struct Flex {
axis: Axis, axis: Axis,
children: Vec<ElementBox>, children: Vec<ElementBox>,
scroll_state: Option<ElementStateHandle<ScrollState>>, scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
} }
impl Flex { impl Flex {
@ -52,9 +51,9 @@ impl Flex {
Tag: 'static, Tag: 'static,
V: View, V: View,
{ {
let scroll_state = cx.default_element_state::<Tag, ScrollState>(element_id); let scroll_state = cx.default_element_state::<Tag, Rc<ScrollState>>(element_id);
scroll_state.update(cx, |scroll_state, _| scroll_state.scroll_to = scroll_to); scroll_state.read(cx).scroll_to.set(scroll_to);
self.scroll_state = Some(scroll_state); self.scroll_state = Some((scroll_state, cx.handle().id()));
self self
} }
@ -202,9 +201,9 @@ impl Element for Flex {
} }
if let Some(scroll_state) = self.scroll_state.as_ref() { 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() { 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); let visible_end = visible_start + size.along(self.axis);
if let Some(child) = self.children.get(scroll_to) { if let Some(child) = self.children.get(scroll_to) {
let child_start: f32 = self.children[..scroll_to] let child_start: f32 = self.children[..scroll_to]
@ -213,15 +212,22 @@ impl Element for Flex {
.sum(); .sum();
let child_end = child_start + child.size().along(self.axis); let child_end = child_start + child.size().along(self.axis);
if child_start < visible_start { if child_start < visible_start {
scroll_state.scroll_position = child_start; scroll_state.scroll_position.set(child_start);
} else if child_end > visible_end { } 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.set(
scroll_state.scroll_position.min(-remaining_space).max(0.); scroll_state
.scroll_position
.get()
.min(-remaining_space)
.max(0.),
);
}); });
} }
@ -242,9 +248,45 @@ impl Element for Flex {
cx.scene.push_layer(Some(bounds)); 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(); let mut child_origin = bounds.origin();
if let Some(scroll_state) = self.scroll_state.as_ref() { 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 { match self.axis {
Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position), Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position),
Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position), Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position),
@ -278,9 +320,9 @@ impl Element for Flex {
fn dispatch_event( fn dispatch_event(
&mut self, &mut self,
event: &Event, event: &Event,
bounds: RectF,
_: RectF, _: RectF,
remaining_space: &mut Self::LayoutState, _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState, _: &mut Self::PaintState,
cx: &mut EventContext, cx: &mut EventContext,
) -> bool { ) -> bool {
@ -288,50 +330,6 @@ impl Element for Flex {
for child in &mut self.children { for child in &mut self.children {
handled = child.dispatch_event(event, cx) || handled; 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 handled
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -39,7 +39,7 @@ use postage::watch;
use rand::prelude::*; use rand::prelude::*;
use search::SearchQuery; use search::SearchQuery;
use serde::Serialize; use serde::Serialize;
use settings::Settings; use settings::{FormatOnSave, Formatter, Settings};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use similar::{ChangeTag, TextDiff}; use similar::{ChangeTag, TextDiff};
use std::{ 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 { impl Project {
pub fn init(client: &Arc<Client>) { pub fn init(client: &Arc<Client>) {
client.add_model_message_handler(Self::handle_request_join_project); client.add_model_message_handler(Self::handle_request_join_project);
@ -3046,6 +3062,7 @@ impl Project {
&self, &self,
buffers: HashSet<ModelHandle<Buffer>>, buffers: HashSet<ModelHandle<Buffer>>,
push_to_history: bool, push_to_history: bool,
trigger: FormatTrigger,
cx: &mut ModelContext<Project>, cx: &mut ModelContext<Project>,
) -> Task<Result<ProjectTransaction>> { ) -> Task<Result<ProjectTransaction>> {
let mut local_buffers = Vec::new(); let mut local_buffers = Vec::new();
@ -3075,6 +3092,7 @@ impl Project {
let response = client let response = client
.request(proto::FormatBuffers { .request(proto::FormatBuffers {
project_id, project_id,
trigger: trigger as i32,
buffer_ids: remote_buffers buffer_ids: remote_buffers
.iter() .iter()
.map(|buffer| buffer.read_with(&cx, |buffer, _| buffer.remote_id())) .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 { 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 settings = cx.global::<Settings>();
let language_name = buffer.language().map(|language| language.name()); let language_name = buffer.language().map(|language| language.name());
( (
settings.format_on_save(language_name.as_deref()), settings.format_on_save(language_name.as_deref()),
settings.formatter(language_name.as_deref()),
settings.tab_size(language_name.as_deref()), settings.tab_size(language_name.as_deref()),
) )
}); });
let transaction = match format_on_save { let transaction = match (formatter, format_on_save) {
settings::FormatOnSave::Off => continue, (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => continue,
settings::FormatOnSave::LanguageServer => Self::format_via_lsp(
(Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
| (_, FormatOnSave::LanguageServer) => Self::format_via_lsp(
&this, &this,
&buffer, &buffer,
&buffer_abs_path, &buffer_abs_path,
@ -3112,7 +3133,12 @@ impl Project {
) )
.await .await
.context("failed to format via language server")?, .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( Self::format_via_external_command(
&buffer, &buffer,
&buffer_abs_path, &buffer_abs_path,
@ -5295,7 +5321,8 @@ impl Project {
.ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?, .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?; let project_transaction = format.await?;

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = pos.x() / cur_size.cell_width;
let col = min(GridCol(col as usize), cur_size.last_column()); let col = min(GridCol(col as usize), cur_size.last_column());
let line = pos.y() / cur_size.line_height; 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)] #[cfg(test)]
mod test { mod test {
use crate::mappings::mouse::mouse_point; use crate::mappings::mouse::grid_point;
#[test] #[test]
fn test_mouse_to_selection() { 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 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 origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
let mouse_pos = mouse_pos - origin; 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!( assert_eq!(
point, point,
alacritty_terminal::index::Point::new( alacritty_terminal::index::Point::new(

View file

@ -29,18 +29,22 @@ use futures::{
}; };
use mappings::mouse::{ 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 procinfo::LocalProcessInfo;
use settings::{AlternateScroll, Settings, Shell, TerminalBlink}; use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
use util::ResultExt;
use std::{ use std::{
cmp::min,
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
fmt::Display, fmt::Display,
ops::{Deref, RangeInclusive, Sub}, io,
os::unix::prelude::AsRawFd, ops::{Deref, Index, RangeInclusive, Sub},
os::unix::{prelude::AsRawFd, process::CommandExt},
path::PathBuf, path::PathBuf,
process::Command,
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@ -49,9 +53,7 @@ use thiserror::Error;
use gpui::{ use gpui::{
geometry::vector::{vec2f, Vector2F}, geometry::vector::{vec2f, Vector2F},
keymap::Keystroke, keymap::Keystroke,
scene::{ scene::{DownRegionEvent, DragRegionEvent, ScrollWheelRegionEvent, UpRegionEvent},
ClickRegionEvent, DownRegionEvent, DragRegionEvent, ScrollWheelRegionEvent, UpRegionEvent,
},
ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext, Task, ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext, Task,
}; };
@ -59,6 +61,7 @@ use crate::mappings::{
colors::{get_color_at_index, to_alac_rgb}, colors::{get_color_at_index, to_alac_rgb},
keys::to_esc_str, keys::to_esc_str,
}; };
use lazy_static::lazy_static;
///Initialize and register all of our action handlers ///Initialize and register all of our action handlers
pub fn init(cx: &mut MutableAppContext) { 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 ///Scroll multiplier that is set to 3 by default. This will be removed when I
///Implement scroll bars. ///Implement scroll bars.
const SCROLL_MULTIPLIER: f32 = 4.; 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_WIDTH: f32 = 500.;
const DEBUG_TERMINAL_HEIGHT: f32 = 30.; const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_CELL_WIDTH: f32 = 5.;
const DEBUG_LINE_HEIGHT: 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 ///Upward flowing events, for changing the title and such
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum Event { pub enum Event {
@ -98,6 +107,8 @@ enum InternalEvent {
ScrollToPoint(Point), ScrollToPoint(Point),
SetSelection(Option<(Selection, Point)>), SetSelection(Option<(Selection, Point)>),
UpdateSelection(Vector2F), UpdateSelection(Vector2F),
// Adjusted mouse position, should open
FindHyperlink(Vector2F, bool),
Copy, Copy,
} }
@ -267,7 +278,6 @@ impl TerminalBuilder {
working_directory: Option<PathBuf>, working_directory: Option<PathBuf>,
shell: Option<Shell>, shell: Option<Shell>,
env: Option<HashMap<String, String>>, env: Option<HashMap<String, String>>,
initial_size: TerminalSize,
blink_settings: Option<TerminalBlink>, blink_settings: Option<TerminalBlink>,
alternate_scroll: &AlternateScroll, alternate_scroll: &AlternateScroll,
window_id: usize, window_id: usize,
@ -307,7 +317,11 @@ impl TerminalBuilder {
//TODO: Remove with a bounded sender which can be dispatched on &self //TODO: Remove with a bounded sender which can be dispatched on &self
let (events_tx, events_rx) = unbounded(); let (events_tx, events_rx) = unbounded();
//Set up the terminal... //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 //Start off blinking if we need to
if let Some(TerminalBlink::On) = blink_settings { if let Some(TerminalBlink::On) = blink_settings {
@ -322,7 +336,11 @@ impl TerminalBuilder {
let term = Arc::new(FairMutex::new(term)); let term = Arc::new(FairMutex::new(term));
//Setup the pty... //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, Ok(pty) => pty,
Err(error) => { Err(error) => {
bail!(TerminalError { bail!(TerminalError {
@ -354,7 +372,6 @@ impl TerminalBuilder {
term, term,
events: VecDeque::with_capacity(10), //Should never get this high. events: VecDeque::with_capacity(10), //Should never get this high.
last_content: Default::default(), last_content: Default::default(),
cur_size: initial_size,
last_mouse: None, last_mouse: None,
matches: Vec::new(), matches: Vec::new(),
last_synced: Instant::now(), last_synced: Instant::now(),
@ -365,6 +382,9 @@ impl TerminalBuilder {
foreground_process_info: None, foreground_process_info: None,
breadcrumb_text: String::new(), breadcrumb_text: String::new(),
scroll_px: 0., scroll_px: 0.,
last_mouse_position: None,
next_link_id: 0,
selection_phase: SelectionPhase::Ended,
}; };
Ok(TerminalBuilder { Ok(TerminalBuilder {
@ -450,6 +470,8 @@ pub struct TerminalContent {
selection: Option<SelectionRange>, selection: Option<SelectionRange>,
cursor: RenderableCursor, cursor: RenderableCursor,
cursor_char: char, cursor_char: char,
size: TerminalSize,
last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
} }
impl Default for TerminalContent { impl Default for TerminalContent {
@ -465,17 +487,27 @@ impl Default for TerminalContent {
point: Point::new(Line(0), Column(0)), point: Point::new(Line(0), Column(0)),
}, },
cursor_char: Default::default(), cursor_char: Default::default(),
size: Default::default(),
last_hovered_hyperlink: None,
} }
} }
} }
#[derive(PartialEq, Eq)]
pub enum SelectionPhase {
Selecting,
Ended,
}
pub struct Terminal { pub struct Terminal {
pty_tx: Notifier, pty_tx: Notifier,
term: Arc<FairMutex<Term<ZedListener>>>, term: Arc<FairMutex<Term<ZedListener>>>,
events: VecDeque<InternalEvent>, events: VecDeque<InternalEvent>,
/// This is only used for mouse mode cell change detection
last_mouse: Option<(Point, AlacDirection)>, last_mouse: Option<(Point, AlacDirection)>,
/// This is only used for terminal hyperlink checking
last_mouse_position: Option<Vector2F>,
pub matches: Vec<RangeInclusive<Point>>, pub matches: Vec<RangeInclusive<Point>>,
cur_size: TerminalSize,
last_content: TerminalContent, last_content: TerminalContent,
last_synced: Instant, last_synced: Instant,
sync_task: Option<Task<()>>, sync_task: Option<Task<()>>,
@ -485,6 +517,8 @@ pub struct Terminal {
shell_fd: u32, shell_fd: u32,
foreground_process_info: Option<LocalProcessInfo>, foreground_process_info: Option<LocalProcessInfo>,
scroll_px: f32, scroll_px: f32,
next_link_id: usize,
selection_phase: SelectionPhase,
} }
impl Terminal { impl Terminal {
@ -508,7 +542,7 @@ impl Terminal {
)), )),
AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()), AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
AlacTermEvent::TextAreaSizeRequest(format) => { AlacTermEvent::TextAreaSizeRequest(format) => {
self.write_to_pty(format(self.cur_size.into())) self.write_to_pty(format(self.last_content.size.into()))
} }
AlacTermEvent::CursorBlinkingChange => { AlacTermEvent::CursorBlinkingChange => {
cx.emit(Event::BlinkChanged); cx.emit(Event::BlinkChanged);
@ -577,16 +611,10 @@ impl Terminal {
new_size.height = f32::max(new_size.line_height, new_size.height); new_size.height = f32::max(new_size.line_height, new_size.height);
new_size.width = f32::max(new_size.cell_width, new_size.width); 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(); 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); term.resize(new_size);
} }
InternalEvent::Clear => { InternalEvent::Clear => {
@ -595,6 +623,7 @@ impl Terminal {
} }
InternalEvent::Scroll(scroll) => { InternalEvent::Scroll(scroll) => {
term.scroll_display(*scroll); term.scroll_display(*scroll);
self.refresh_hyperlink();
} }
InternalEvent::SetSelection(selection) => { InternalEvent::SetSelection(selection) => {
term.selection = selection.as_ref().map(|(sel, _)| sel.clone()); term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
@ -606,8 +635,12 @@ impl Terminal {
} }
InternalEvent::UpdateSelection(position) => { InternalEvent::UpdateSelection(position) => {
if let Some(mut selection) = term.selection.take() { if let Some(mut selection) = term.selection.take() {
let point = mouse_point(*position, self.cur_size, term.grid().display_offset()); let point = grid_point(
let side = mouse_side(*position, self.cur_size); *position,
self.last_content.size,
term.grid().display_offset(),
);
let side = mouse_side(*position, self.last_content.size);
selection.update(point, side); selection.update(point, side);
term.selection = Some(selection); term.selection = Some(selection);
@ -622,10 +655,95 @@ impl Terminal {
cx.write_to_clipboard(ClipboardItem::new(txt)) 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 { pub fn last_content(&self) -> &TerminalContent {
&self.last_content &self.last_content
} }
@ -691,7 +809,8 @@ impl Terminal {
} else { } else {
text.replace("\r\n", "\r").replace('\n', "\r") 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>) { 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.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(); 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(); let content = term.renderable_content();
TerminalContent { TerminalContent {
cells: content cells: content
@ -757,6 +876,8 @@ impl Terminal {
selection: content.selection, selection: content.selection,
cursor: content.cursor, cursor: content.cursor,
cursor_char: term.grid()[content.cursor.point].c, 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) { if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
self.write_to_pty("\x1b[O".to_string()); self.write_to_pty("\x1b[O".to_string());
} }
@ -795,21 +917,40 @@ impl Terminal {
pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
let position = e.position.sub(origin); let position = e.position.sub(origin);
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);
let point = mouse_point(position, self.cur_size, self.last_content.display_offset); if self.mouse_changed(point, side) {
let side = mouse_side(position, self.cur_size); if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
self.pty_tx.notify(bytes);
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);
} }
} 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) { pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin); let position = e.position.sub(origin);
self.last_mouse_position = Some(position);
if !self.mouse_mode(e.shift) { if !self.mouse_mode(e.shift) {
self.selection_phase = SelectionPhase::Selecting;
// Alacritty has the same ordering, of first updating the selection // Alacritty has the same ordering, of first updating the selection
// then scrolling 15ms later // then scrolling 15ms later
self.events self.events
@ -822,20 +963,18 @@ impl Terminal {
None => return, 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 self.events
.push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines))); .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> { 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 //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 top = e.region.origin_y() + (self.last_content.size.line_height * 2.);
let bottom = e.region.lower_left().y() - (self.cur_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 { let scroll_delta = if e.position.y() < top {
(top - e.position.y()).powf(1.1) (top - e.position.y()).powf(1.1)
} else if e.position.y() > bottom { } else if e.position.y() > bottom {
@ -848,42 +987,60 @@ impl Terminal {
pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) { pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin); let position = e.position.sub(origin);
let point = mouse_point(position, self.cur_size, self.last_content.display_offset); let point = grid_point(
let side = mouse_side(position, self.cur_size); 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 self.mouse_mode(e.shift) {
if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) { if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
self.pty_tx.notify(bytes); self.pty_tx.notify(bytes);
} }
} else if e.button == MouseButton::Left { } else if e.button == MouseButton::Left {
self.events.push_back(InternalEvent::SetSelection(Some(( self.left_click(e, origin)
Selection::new(SelectionType::Simple, point, side),
point,
))));
} }
} }
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); let position = e.position.sub(origin);
if !self.mouse_mode(e.shift) { if !self.mouse_mode(e.shift) {
let point = mouse_point(position, self.cur_size, self.last_content.display_offset); //Hyperlinks
let side = mouse_side(position, self.cur_size); {
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_type = match e.click_count { // Selections
0 => return, //This is a release {
1 => Some(SelectionType::Simple), let point = grid_point(
2 => Some(SelectionType::Semantic), position,
3 => Some(SelectionType::Lines), self.last_content.size,
_ => None, self.last_content.display_offset,
}; );
let side = mouse_side(position, self.last_content.size);
let selection = let selection_type = match e.click_count {
selection_type.map(|selection_type| Selection::new(selection_type, point, side)); 0 => return, //This is a release
1 => Some(SelectionType::Simple),
2 => Some(SelectionType::Semantic),
3 => Some(SelectionType::Lines),
_ => None,
};
if let Some(sel) = selection { let selection = selection_type
self.events .map(|selection_type| Selection::new(selection_type, point, side));
.push_back(InternalEvent::SetSelection(Some((sel, point))));
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) { pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin); let position = e.position.sub(origin);
if self.mouse_mode(e.shift) { 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) { if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
self.pty_tx.notify(bytes); self.pty_tx.notify(bytes);
@ -901,6 +1062,7 @@ impl Terminal {
// so let's do that here // so let's do that here
self.copy(); self.copy();
} }
self.selection_phase = SelectionPhase::Ended;
self.last_mouse = None; self.last_mouse = None;
} }
@ -910,9 +1072,9 @@ impl Terminal {
if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) { if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
if mouse_mode { if mouse_mode {
let point = mouse_point( let point = grid_point(
e.position.sub(origin), e.position.sub(origin),
self.cur_size, self.last_content.size,
self.last_content.display_offset, 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( fn determine_scroll_lines(
&mut self, &mut self,
e: &ScrollWheelRegionEvent, e: &ScrollWheelRegionEvent,
@ -955,20 +1121,22 @@ impl Terminal {
} }
/* Calculate the appropriate scroll lines */ /* Calculate the appropriate scroll lines */
Some(gpui::TouchPhase::Moved) => { 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; 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 // Whenever we hit the edges, reset our stored scroll to 0
// so we can respond to changes in direction quickly // 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) Some(new_offset - old_offset)
} }
/* Fall back to delta / line_height */ /* 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, _ => None,
} }
} }
@ -1011,30 +1179,36 @@ impl Entity for Terminal {
type Event = Event; 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 { fn make_selection(range: &RangeInclusive<Point>) -> Selection {
let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left); let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
selection.update(*range.end(), AlacDirection::Right); selection.update(*range.end(), AlacDirection::Right);
selection 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>( fn all_search_matches<'a, T>(
term: &'a Term<T>, term: &'a Term<T>,
regex: &'a RegexSearch, regex: &'a RegexSearch,
@ -1044,7 +1218,115 @@ fn all_search_matches<'a, T>(
RegexIter::new(start, end, AlacDirection::Right, term, regex) 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)] #[cfg(test)]
mod tests { 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; 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]
);
}
} }

View file

@ -11,7 +11,6 @@ use util::truncate_and_trailoff;
use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}; use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
use workspace::{Item, ItemEvent, ToolbarItemLocation, Workspace}; use workspace::{Item, ItemEvent, ToolbarItemLocation, Workspace};
use crate::TerminalSize;
use project::{LocalWorktree, Project, ProjectPath}; use project::{LocalWorktree, Project, ProjectPath};
use settings::{AlternateScroll, Settings, WorkingDirectory}; use settings::{AlternateScroll, Settings, WorkingDirectory};
use smallvec::SmallVec; use smallvec::SmallVec;
@ -87,9 +86,6 @@ impl TerminalContainer {
modal: bool, modal: bool,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> 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 settings = cx.global::<Settings>();
let shell = settings.terminal_overrides.shell.clone(); let shell = settings.terminal_overrides.shell.clone();
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap. let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
@ -111,7 +107,6 @@ impl TerminalContainer {
working_directory.clone(), working_directory.clone(),
shell, shell,
envs, envs,
size_info,
settings.terminal_overrides.blinking.clone(), settings.terminal_overrides.blinking.clone(),
scroll, scroll,
cx.window_id(), cx.window_id(),

View file

@ -7,15 +7,17 @@ use alacritty_terminal::{
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine}; use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
use gpui::{ use gpui::{
color::Color, color::Color,
fonts::{Properties, Style::Italic, TextStyle, Underline, Weight}, elements::{Empty, Overlay},
fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight},
geometry::{ geometry::{
rect::RectF, rect::RectF,
vector::{vec2f, Vector2F}, vector::{vec2f, Vector2F},
}, },
serde_json::json, serde_json::json,
text_layout::{Line, RunStyle}, text_layout::{Line, RunStyle},
Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, MouseRegion, Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton,
PaintContext, Quad, TextLayoutCache, WeakModelHandle, WeakViewHandle, MouseRegion, PaintContext, Quad, SizeConstraint, TextLayoutCache, WeakModelHandle,
WeakViewHandle,
}; };
use itertools::Itertools; use itertools::Itertools;
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
@ -42,6 +44,7 @@ pub struct LayoutState {
size: TerminalSize, size: TerminalSize,
mode: TermMode, mode: TermMode,
display_offset: usize, display_offset: usize,
hyperlink_tooltip: Option<ElementBox>,
} }
///Helper struct for converting data between alacritty's cursor points, and displayed cursor points ///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
@ -180,6 +183,7 @@ impl TerminalElement {
text_layout_cache: &TextLayoutCache, text_layout_cache: &TextLayoutCache,
font_cache: &FontCache, font_cache: &FontCache,
modal: bool, modal: bool,
hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
) -> (Vec<LayoutCell>, Vec<LayoutRect>) { ) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
let mut cells = vec![]; let mut cells = vec![];
let mut rects = vec![]; let mut rects = vec![];
@ -237,7 +241,7 @@ impl TerminalElement {
//Layout current cell text //Layout current cell text
{ {
let cell_text = &cell.c.to_string(); let cell_text = &cell.c.to_string();
if cell_text != " " { if !is_blank(&cell) {
let cell_style = TerminalElement::cell_style( let cell_style = TerminalElement::cell_style(
&cell, &cell,
fg, fg,
@ -245,6 +249,7 @@ impl TerminalElement {
text_style, text_style,
font_cache, font_cache,
modal, modal,
hyperlink,
); );
let layout_cell = text_layout_cache.layout_str( 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), Point::new(line_index as i32, cell.point.column.0 as i32),
layout_cell, layout_cell,
)) ))
} };
}; }
} }
if cur_rect.is_some() { if cur_rect.is_some() {
@ -304,11 +309,12 @@ impl TerminalElement {
text_style: &TextStyle, text_style: &TextStyle,
font_cache: &FontCache, font_cache: &FontCache,
modal: bool, modal: bool,
hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
) -> RunStyle { ) -> RunStyle {
let flags = indexed.cell.flags; let flags = indexed.cell.flags;
let fg = convert_color(&fg, &style.colors, modal); let fg = convert_color(&fg, &style.colors, modal);
let underline = flags let mut underline = flags
.intersects(Flags::ALL_UNDERLINES) .intersects(Flags::ALL_UNDERLINES)
.then(|| Underline { .then(|| Underline {
color: Some(fg), color: Some(fg),
@ -317,6 +323,12 @@ impl TerminalElement {
}) })
.unwrap_or_default(); .unwrap_or_default();
if indexed.cell.hyperlink().is_some() {
if underline.thickness == OrderedFloat(0.) {
underline.thickness = OrderedFloat(1.);
}
}
let mut properties = Properties::new(); let mut properties = Properties::new();
if indexed if indexed
.flags .flags
@ -332,11 +344,25 @@ impl TerminalElement {
.select_font(text_style.font_family_id, &properties) .select_font(text_style.font_family_id, &properties)
.unwrap_or(text_style.font_id); .unwrap_or(text_style.font_id);
RunStyle { let mut result = RunStyle {
color: fg, color: fg,
font_id, font_id,
underline, 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>( fn generic_button_handler<E>(
@ -366,7 +392,7 @@ impl TerminalElement {
) { ) {
let connection = self.terminal; 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: // Terminal Emulator controlled behavior:
region = region 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 // Context menu
.on_click(MouseButton::Right, move |e, cx| { .on_click(MouseButton::Right, move |e, cx| {
let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx.app) { 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( .on_scroll(TerminalElement::generic_button_handler(
connection, connection,
origin, 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); cx.scene.push_mouse_region(region);
} }
@ -547,6 +557,9 @@ impl Element for TerminalElement {
//Setup layout information //Setup layout information
let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone. 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 text_style = TerminalElement::make_text_style(font_cache, settings);
let selection_color = settings.theme.editor.selection.selection; let selection_color = settings.theme.editor.selection.selection;
let match_color = settings.theme.search.match_background; let match_color = settings.theme.search.match_background;
@ -569,9 +582,34 @@ impl Element for TerminalElement {
}; };
let terminal_handle = self.terminal.upgrade(cx).unwrap(); 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.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 { let TerminalContent {
@ -581,8 +619,9 @@ impl Element for TerminalElement {
cursor_char, cursor_char,
selection, selection,
cursor, cursor,
last_hovered_hyperlink,
.. ..
} = &terminal_handle.read(cx).last_content; } = { &terminal_handle.read(cx).last_content };
// searches, highlights to a single range representations // searches, highlights to a single range representations
let mut relative_highlighted_ranges = Vec::new(); let mut relative_highlighted_ranges = Vec::new();
@ -602,6 +641,9 @@ impl Element for TerminalElement {
cx.text_layout_cache, cx.text_layout_cache,
cx.font_cache(), cx.font_cache(),
self.modal, 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 //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( TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
move |(cursor_position, block_width)| { move |(cursor_position, block_width)| {
let shape = match cursor.shape { let shape = match cursor.shape {
AlacCursorShape::Block if !self.focused => CursorShape::Hollow, AlacCursorShape::Block if !focused => CursorShape::Hollow,
AlacCursorShape::Block => CursorShape::Block, AlacCursorShape::Block => CursorShape::Block,
AlacCursorShape::Underline => CursorShape::Underscore, AlacCursorShape::Underline => CursorShape::Underscore,
AlacCursorShape::Beam => CursorShape::Bar, AlacCursorShape::Beam => CursorShape::Bar,
@ -669,6 +712,7 @@ impl Element for TerminalElement {
relative_highlighted_ranges, relative_highlighted_ranges,
mode: *mode, mode: *mode,
display_offset: *display_offset, display_offset: *display_offset,
hyperlink_tooltip,
}, },
) )
} }
@ -691,7 +735,11 @@ impl Element for TerminalElement {
cx.scene.push_cursor_region(gpui::CursorRegion { cx.scene.push_cursor_region(gpui::CursorRegion {
bounds, 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| { 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( fn to_highlighted_range_lines(
range: &RangeInclusive<Point>, range: &RangeInclusive<Point>,
layout: &LayoutState, layout: &LayoutState,

View file

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

View file

@ -1,10 +1,17 @@
use std::{path::Path, time::Duration}; use std::{path::Path, time::Duration};
use alacritty_terminal::{
index::{Column, Line, Point},
term::cell::Cell,
};
use gpui::{ModelHandle, TestAppContext, ViewHandle}; use gpui::{ModelHandle, TestAppContext, ViewHandle};
use project::{Entry, Project, ProjectPath, Worktree}; use project::{Entry, Project, ProjectPath, Worktree};
use rand::{rngs::ThreadRng, Rng};
use workspace::{AppState, Workspace}; use workspace::{AppState, Workspace};
use crate::{IndexedCell, TerminalContent, TerminalSize};
pub struct TerminalTestContext<'a> { pub struct TerminalTestContext<'a> {
pub cx: &'a mut TestAppContext, 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)); 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> { impl<'a> Drop for TerminalTestContext<'a> {

View file

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

View file

@ -1,26 +1,22 @@
use super::installation::{npm_install_packages, npm_package_latest_version}; use super::installation::{latest_github_release, GitHubLspBinaryVersion};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_trait::async_trait; use async_trait::async_trait;
use client::http::HttpClient; use client::http::HttpClient;
use collections::HashMap; use collections::HashMap;
use futures::StreamExt; use futures::{io::BufReader, StreamExt};
use language::{LanguageServerName, LspAdapter}; use language::{LanguageServerName, LspAdapter};
use serde_json::json; use serde_json::json;
use smol::fs; use smol::fs::{self, File};
use std::{any::Any, path::PathBuf, sync::Arc}; use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
use util::ResultExt; use util::ResultExt;
pub struct JsonLspAdapter; pub struct JsonLspAdapter;
impl JsonLspAdapter {
const BIN_PATH: &'static str =
"node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
}
#[async_trait] #[async_trait]
impl LspAdapter for JsonLspAdapter { impl LspAdapter for JsonLspAdapter {
async fn name(&self) -> LanguageServerName { async fn name(&self) -> LanguageServerName {
LanguageServerName("vscode-json-languageserver".into()) LanguageServerName("json-language-server".into())
} }
async fn server_args(&self) -> Vec<String> { async fn server_args(&self) -> Vec<String> {
@ -29,28 +25,46 @@ impl LspAdapter for JsonLspAdapter {
async fn fetch_latest_server_version( async fn fetch_latest_server_version(
&self, &self,
_: Arc<dyn HttpClient>, http: Arc<dyn HttpClient>,
) -> Result<Box<dyn 'static + Any + Send>> { ) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(npm_package_latest_version("vscode-json-languageserver").await?) as Box<_>) 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( async fn fetch_server_binary(
&self, &self,
version: Box<dyn 'static + Send + Any>, version: Box<dyn 'static + Send + Any>,
_: Arc<dyn HttpClient>, http: Arc<dyn HttpClient>,
container_dir: PathBuf, container_dir: PathBuf,
) -> Result<PathBuf> { ) -> Result<PathBuf> {
let version = version.downcast::<String>().unwrap(); let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let version_dir = container_dir.join(version.as_str()); let destination_path = container_dir.join(format!(
fs::create_dir_all(&version_dir) "json-language-server-{}-{}",
.await version.name,
.context("failed to create version directory")?; consts::ARCH
let binary_path = version_dir.join(Self::BIN_PATH); ));
if fs::metadata(&binary_path).await.is_err() { if fs::metadata(&destination_path).await.is_err() {
npm_install_packages( let mut response = http
[("vscode-json-languageserver", version.as_str())], .get(&version.url, Default::default(), true)
&version_dir, .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?; .await?;
@ -58,37 +72,25 @@ impl LspAdapter for JsonLspAdapter {
while let Some(entry) = entries.next().await { while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() { if let Some(entry) = entry.log_err() {
let entry_path = entry.path(); let entry_path = entry.path();
if entry_path.as_path() != version_dir { if entry_path.as_path() != destination_path {
fs::remove_dir_all(&entry_path).await.log_err(); 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 fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
(|| async move { (|| async move {
let mut last_version_dir = None; let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?; let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await { while let Some(entry) = entries.next().await {
let entry = entry?; last = Some(entry?.path());
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.ok_or_else(|| anyhow!("no cached binary"))
})() })()
.await .await
.log_err() .log_err()

View file

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