Merge branch 'master' into rescan
This commit is contained in:
commit
db8cce9aa9
22 changed files with 768 additions and 125 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -15,7 +15,7 @@ env:
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
name: Tests
|
name: Tests
|
||||||
runs-on: macos-latest
|
runs-on: self-hosted
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
17
Cargo.lock
generated
17
Cargo.lock
generated
|
@ -968,6 +968,7 @@ dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"async-task",
|
"async-task",
|
||||||
"bindgen",
|
"bindgen",
|
||||||
|
"block",
|
||||||
"cc",
|
"cc",
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
|
@ -990,6 +991,7 @@ dependencies = [
|
||||||
"replace_with",
|
"replace_with",
|
||||||
"resvg",
|
"resvg",
|
||||||
"scoped-pool",
|
"scoped-pool",
|
||||||
|
"seahash",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"simplelog",
|
"simplelog",
|
||||||
|
@ -1837,6 +1839,20 @@ name = "serde"
|
||||||
version = "1.0.125"
|
version = "1.0.125"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
|
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.125"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
|
@ -2429,6 +2445,7 @@ dependencies = [
|
||||||
"rand 0.8.3",
|
"rand 0.8.3",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"seahash",
|
"seahash",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"simplelog",
|
"simplelog",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
|
|
@ -19,7 +19,8 @@ rand = "0.8.3"
|
||||||
replace_with = "0.1.7"
|
replace_with = "0.1.7"
|
||||||
resvg = "0.14"
|
resvg = "0.14"
|
||||||
scoped-pool = "1.0.0"
|
scoped-pool = "1.0.0"
|
||||||
serde = "1.0.125"
|
seahash = "4.1"
|
||||||
|
serde = {version = "1.0.125", features = ["derive"]}
|
||||||
serde_json = "1.0.64"
|
serde_json = "1.0.64"
|
||||||
smallvec = "1.6.1"
|
smallvec = "1.6.1"
|
||||||
smol = "1.2"
|
smol = "1.2"
|
||||||
|
@ -37,6 +38,7 @@ simplelog = "0.9"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
block = "0.1"
|
||||||
cocoa = "0.24"
|
cocoa = "0.24"
|
||||||
core-foundation = "0.9"
|
core-foundation = "0.9"
|
||||||
core-graphics = "0.22.2"
|
core-graphics = "0.22.2"
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
platform::{self, WindowOptions},
|
platform::{self, WindowOptions},
|
||||||
presenter::Presenter,
|
presenter::Presenter,
|
||||||
util::post_inc,
|
util::post_inc,
|
||||||
AssetCache, AssetSource, FontCache, TextLayoutCache,
|
AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use async_std::sync::Condvar;
|
use async_std::sync::Condvar;
|
||||||
|
@ -141,12 +141,13 @@ impl App {
|
||||||
{
|
{
|
||||||
let presenter = presenter.clone();
|
let presenter = presenter.clone();
|
||||||
let path = presenter.borrow().dispatch_path(ctx.as_ref());
|
let path = presenter.borrow().dispatch_path(ctx.as_ref());
|
||||||
if ctx.dispatch_action_any(key_window_id, &path, command, arg.unwrap_or(&())) {
|
ctx.dispatch_action_any(key_window_id, &path, command, arg.unwrap_or(&()));
|
||||||
return;
|
} else {
|
||||||
}
|
ctx.dispatch_global_action_any(command, arg.unwrap_or(&()));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ctx.dispatch_global_action_any(command, arg.unwrap_or(&()));
|
||||||
}
|
}
|
||||||
ctx.dispatch_global_action_any(command, arg.unwrap_or(&()));
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
app.0.borrow_mut().weak_self = Some(Rc::downgrade(&app.0));
|
app.0.borrow_mut().weak_self = Some(Rc::downgrade(&app.0));
|
||||||
|
@ -570,6 +571,22 @@ impl MutableAppContext {
|
||||||
self.platform.set_menus(menus);
|
self.platform.set_menus(menus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prompt_for_paths<F>(&self, options: PathPromptOptions, done_fn: F)
|
||||||
|
where
|
||||||
|
F: 'static + FnOnce(Option<Vec<PathBuf>>, &mut MutableAppContext),
|
||||||
|
{
|
||||||
|
let app = self.weak_self.as_ref().unwrap().upgrade().unwrap();
|
||||||
|
let foreground = self.foreground.clone();
|
||||||
|
self.platform().prompt_for_paths(
|
||||||
|
options,
|
||||||
|
Box::new(move |paths| {
|
||||||
|
foreground
|
||||||
|
.spawn(async move { (done_fn)(paths, &mut *app.borrow_mut()) })
|
||||||
|
.detach();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dispatch_action<T: 'static + Any>(
|
pub fn dispatch_action<T: 'static + Any>(
|
||||||
&mut self,
|
&mut self,
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
|
@ -1213,8 +1230,12 @@ impl MutableAppContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy(&self, text: &str) {
|
pub fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||||
self.platform.copy(text);
|
self.platform.write_to_clipboard(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||||
|
self.platform.read_from_clipboard()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
42
gpui/src/clipboard.rs
Normal file
42
gpui/src/clipboard.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use seahash::SeaHasher;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct ClipboardItem {
|
||||||
|
pub(crate) text: String,
|
||||||
|
pub(crate) metadata: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClipboardItem {
|
||||||
|
pub fn new(text: String) -> Self {
|
||||||
|
Self {
|
||||||
|
text,
|
||||||
|
metadata: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_metadata<T: Serialize>(mut self, metadata: T) -> Self {
|
||||||
|
self.metadata = Some(serde_json::to_string(&metadata).unwrap());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text(&self) -> &String {
|
||||||
|
&self.text
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn metadata<T>(&self) -> Option<T>
|
||||||
|
where
|
||||||
|
T: for<'a> Deserialize<'a>,
|
||||||
|
{
|
||||||
|
self.metadata
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|m| serde_json::from_str(m).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn text_hash(text: &str) -> u64 {
|
||||||
|
let mut hasher = SeaHasher::new();
|
||||||
|
text.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ use crate::{
|
||||||
LayoutContext, PaintContext, SizeConstraint,
|
LayoutContext, PaintContext, SizeConstraint,
|
||||||
};
|
};
|
||||||
use json::ToJson;
|
use json::ToJson;
|
||||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
use pathfinder_geometry::vector::Vector2F;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
pub struct Align {
|
pub struct Align {
|
||||||
|
@ -19,8 +19,13 @@ impl Align {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn top_center(mut self) -> Self {
|
pub fn top(mut self) -> Self {
|
||||||
self.alignment = vec2f(0.0, -1.0);
|
self.alignment.set_y(-1.0);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn right(mut self) -> Self {
|
||||||
|
self.alignment.set_x(1.0);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,11 @@ impl ConstrainedBox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_min_width(mut self, min_width: f32) -> Self {
|
||||||
|
self.constraint.min.set_x(min_width);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_max_width(mut self, max_width: f32) -> Self {
|
pub fn with_max_width(mut self, max_width: f32) -> Self {
|
||||||
self.constraint.max.set_x(max_width);
|
self.constraint.max.set_x(max_width);
|
||||||
self
|
self
|
||||||
|
@ -33,6 +38,12 @@ impl ConstrainedBox {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_width(mut self, width: f32) -> Self {
|
||||||
|
self.constraint.min.set_x(width);
|
||||||
|
self.constraint.max.set_x(width);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_height(mut self, height: f32) -> Self {
|
pub fn with_height(mut self, height: f32) -> Self {
|
||||||
self.constraint.min.set_y(height);
|
self.constraint.min.set_y(height);
|
||||||
self.constraint.max.set_y(height);
|
self.constraint.max.set_y(height);
|
||||||
|
@ -51,6 +62,7 @@ impl Element for ConstrainedBox {
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
) -> (Vector2F, Self::LayoutState) {
|
||||||
constraint.min = constraint.min.max(self.constraint.min);
|
constraint.min = constraint.min.max(self.constraint.min);
|
||||||
constraint.max = constraint.max.min(self.constraint.max);
|
constraint.max = constraint.max.min(self.constraint.max);
|
||||||
|
constraint.max = constraint.max.max(constraint.min);
|
||||||
let size = self.child.layout(constraint, ctx);
|
let size = self.child.layout(constraint, ctx);
|
||||||
(size, ())
|
(size, ())
|
||||||
}
|
}
|
||||||
|
@ -91,6 +103,6 @@ impl Element for ConstrainedBox {
|
||||||
_: &Self::PaintState,
|
_: &Self::PaintState,
|
||||||
ctx: &DebugContext,
|
ctx: &DebugContext,
|
||||||
) -> json::Value {
|
) -> json::Value {
|
||||||
json!({"type": "ConstrainedBox", "constraint": self.constraint.to_json(), "child": self.child.debug(ctx)})
|
json!({"type": "ConstrainedBox", "set_constraint": self.constraint.to_json(), "child": self.child.debug(ctx)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,18 @@ impl Container {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_horizontal_padding(mut self, padding: f32) -> Self {
|
||||||
|
self.padding.left = padding;
|
||||||
|
self.padding.right = padding;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_vertical_padding(mut self, padding: f32) -> Self {
|
||||||
|
self.padding.top = padding;
|
||||||
|
self.padding.bottom = padding;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_uniform_padding(mut self, padding: f32) -> Self {
|
pub fn with_uniform_padding(mut self, padding: f32) -> Self {
|
||||||
self.padding = Padding {
|
self.padding = Padding {
|
||||||
top: padding,
|
top: padding,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::any::Any;
|
use std::{any::Any, f32::INFINITY};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
json::{self, ToJson, Value},
|
json::{self, ToJson, Value},
|
||||||
|
@ -64,8 +64,16 @@ impl Element for Flex {
|
||||||
if let Some(flex) = Self::child_flex(&child) {
|
if let Some(flex) = Self::child_flex(&child) {
|
||||||
total_flex += flex;
|
total_flex += flex;
|
||||||
} else {
|
} else {
|
||||||
let child_constraint =
|
let child_constraint = match self.axis {
|
||||||
SizeConstraint::strict_along(cross_axis, constraint.max_along(cross_axis));
|
Axis::Horizontal => SizeConstraint::new(
|
||||||
|
vec2f(0.0, constraint.min.y()),
|
||||||
|
vec2f(INFINITY, constraint.max.y()),
|
||||||
|
),
|
||||||
|
Axis::Vertical => SizeConstraint::new(
|
||||||
|
vec2f(constraint.min.x(), 0.0),
|
||||||
|
vec2f(constraint.max.x(), INFINITY),
|
||||||
|
),
|
||||||
|
};
|
||||||
let size = child.layout(child_constraint, ctx);
|
let size = child.layout(child_constraint, ctx);
|
||||||
fixed_space += size.along(self.axis);
|
fixed_space += size.along(self.axis);
|
||||||
cross_axis_max = cross_axis_max.max(size.along(cross_axis));
|
cross_axis_max = cross_axis_max.max(size.along(cross_axis));
|
||||||
|
@ -80,16 +88,20 @@ impl Element for Flex {
|
||||||
let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
|
let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
|
||||||
let mut remaining_flex = total_flex;
|
let mut remaining_flex = total_flex;
|
||||||
for child in &mut self.children {
|
for child in &mut self.children {
|
||||||
let space_per_flex = remaining_space / remaining_flex;
|
|
||||||
if let Some(flex) = Self::child_flex(&child) {
|
if let Some(flex) = Self::child_flex(&child) {
|
||||||
let child_max = space_per_flex * flex;
|
let child_max = if remaining_flex == 0.0 {
|
||||||
|
remaining_space
|
||||||
|
} else {
|
||||||
|
let space_per_flex = remaining_space / remaining_flex;
|
||||||
|
space_per_flex * flex
|
||||||
|
};
|
||||||
let child_constraint = match self.axis {
|
let child_constraint = match self.axis {
|
||||||
Axis::Horizontal => SizeConstraint::new(
|
Axis::Horizontal => SizeConstraint::new(
|
||||||
vec2f(0.0, constraint.max.y()),
|
vec2f(0.0, constraint.min.y()),
|
||||||
vec2f(child_max, constraint.max.y()),
|
vec2f(child_max, constraint.max.y()),
|
||||||
),
|
),
|
||||||
Axis::Vertical => SizeConstraint::new(
|
Axis::Vertical => SizeConstraint::new(
|
||||||
vec2f(constraint.max.x(), 0.0),
|
vec2f(constraint.min.x(), 0.0),
|
||||||
vec2f(constraint.max.x(), child_max),
|
vec2f(constraint.max.x(), child_max),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ use crate::{
|
||||||
SizeConstraint,
|
SizeConstraint,
|
||||||
};
|
};
|
||||||
use core::panic;
|
use core::panic;
|
||||||
|
use json::ToJson;
|
||||||
use replace_with::replace_with_or_abort;
|
use replace_with::replace_with_or_abort;
|
||||||
use std::{any::Any, borrow::Cow};
|
use std::{any::Any, borrow::Cow};
|
||||||
|
|
||||||
|
@ -90,11 +91,13 @@ pub enum Lifecycle<T: Element> {
|
||||||
},
|
},
|
||||||
PostLayout {
|
PostLayout {
|
||||||
element: T,
|
element: T,
|
||||||
|
constraint: SizeConstraint,
|
||||||
size: Vector2F,
|
size: Vector2F,
|
||||||
layout: T::LayoutState,
|
layout: T::LayoutState,
|
||||||
},
|
},
|
||||||
PostPaint {
|
PostPaint {
|
||||||
element: T,
|
element: T,
|
||||||
|
constraint: SizeConstraint,
|
||||||
bounds: RectF,
|
bounds: RectF,
|
||||||
layout: T::LayoutState,
|
layout: T::LayoutState,
|
||||||
paint: T::PaintState,
|
paint: T::PaintState,
|
||||||
|
@ -119,6 +122,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||||
result = Some(size);
|
result = Some(size);
|
||||||
Lifecycle::PostLayout {
|
Lifecycle::PostLayout {
|
||||||
element,
|
element,
|
||||||
|
constraint,
|
||||||
size,
|
size,
|
||||||
layout,
|
layout,
|
||||||
}
|
}
|
||||||
|
@ -132,6 +136,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||||
element,
|
element,
|
||||||
size,
|
size,
|
||||||
layout,
|
layout,
|
||||||
|
..
|
||||||
} = self
|
} = self
|
||||||
{
|
{
|
||||||
element.after_layout(*size, layout, ctx);
|
element.after_layout(*size, layout, ctx);
|
||||||
|
@ -144,6 +149,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||||
replace_with_or_abort(self, |me| {
|
replace_with_or_abort(self, |me| {
|
||||||
if let Lifecycle::PostLayout {
|
if let Lifecycle::PostLayout {
|
||||||
mut element,
|
mut element,
|
||||||
|
constraint,
|
||||||
size,
|
size,
|
||||||
mut layout,
|
mut layout,
|
||||||
} = me
|
} = me
|
||||||
|
@ -152,6 +158,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||||
let paint = element.paint(bounds, &mut layout, ctx);
|
let paint = element.paint(bounds, &mut layout, ctx);
|
||||||
Lifecycle::PostPaint {
|
Lifecycle::PostPaint {
|
||||||
element,
|
element,
|
||||||
|
constraint,
|
||||||
bounds,
|
bounds,
|
||||||
layout,
|
layout,
|
||||||
paint,
|
paint,
|
||||||
|
@ -168,6 +175,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||||
bounds,
|
bounds,
|
||||||
layout,
|
layout,
|
||||||
paint,
|
paint,
|
||||||
|
..
|
||||||
} = self
|
} = self
|
||||||
{
|
{
|
||||||
element.dispatch_event(event, *bounds, layout, paint, ctx)
|
element.dispatch_event(event, *bounds, layout, paint, ctx)
|
||||||
|
@ -196,10 +204,25 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||||
match self {
|
match self {
|
||||||
Lifecycle::PostPaint {
|
Lifecycle::PostPaint {
|
||||||
element,
|
element,
|
||||||
|
constraint,
|
||||||
bounds,
|
bounds,
|
||||||
layout,
|
layout,
|
||||||
paint,
|
paint,
|
||||||
} => element.debug(*bounds, layout, paint, ctx),
|
} => {
|
||||||
|
let mut value = element.debug(*bounds, layout, paint, ctx);
|
||||||
|
if let json::Value::Object(map) = &mut value {
|
||||||
|
let mut new_map: crate::json::Map<String, serde_json::Value> =
|
||||||
|
Default::default();
|
||||||
|
if let Some(typ) = map.remove("type") {
|
||||||
|
new_map.insert("type".into(), typ);
|
||||||
|
}
|
||||||
|
new_map.insert("constraint".into(), constraint.to_json());
|
||||||
|
new_map.append(map);
|
||||||
|
json::Value::Object(new_map)
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => panic!("invalid element lifecycle state"),
|
_ => panic!("invalid element lifecycle state"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ pub use assets::*;
|
||||||
pub mod elements;
|
pub mod elements;
|
||||||
pub mod font_cache;
|
pub mod font_cache;
|
||||||
pub use font_cache::FontCache;
|
pub use font_cache::FontCache;
|
||||||
|
mod clipboard;
|
||||||
|
pub use clipboard::ClipboardItem;
|
||||||
pub mod fonts;
|
pub mod fonts;
|
||||||
pub mod geometry;
|
pub mod geometry;
|
||||||
mod presenter;
|
mod presenter;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use super::{BoolExt as _, Dispatcher, FontSystem, Window};
|
use super::{BoolExt as _, Dispatcher, FontSystem, Window};
|
||||||
use crate::{executor, keymap::Keystroke, platform, Event, Menu, MenuItem};
|
use crate::{executor, keymap::Keystroke, platform, ClipboardItem, Event, Menu, MenuItem};
|
||||||
|
use block::ConcreteBlock;
|
||||||
use cocoa::{
|
use cocoa::{
|
||||||
appkit::{
|
appkit::{
|
||||||
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
|
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
|
||||||
|
@ -20,12 +21,14 @@ use objc::{
|
||||||
use ptr::null_mut;
|
use ptr::null_mut;
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
cell::RefCell,
|
cell::{Cell, RefCell},
|
||||||
|
convert::TryInto,
|
||||||
ffi::{c_void, CStr},
|
ffi::{c_void, CStr},
|
||||||
os::raw::c_char,
|
os::raw::c_char,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
ptr,
|
ptr,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
slice, str,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -77,6 +80,9 @@ pub struct MacPlatform {
|
||||||
fonts: Arc<FontSystem>,
|
fonts: Arc<FontSystem>,
|
||||||
callbacks: RefCell<Callbacks>,
|
callbacks: RefCell<Callbacks>,
|
||||||
menu_item_actions: RefCell<Vec<(String, Option<Box<dyn Any>>)>>,
|
menu_item_actions: RefCell<Vec<(String, Option<Box<dyn Any>>)>>,
|
||||||
|
pasteboard: id,
|
||||||
|
text_hash_pasteboard_type: id,
|
||||||
|
metadata_pasteboard_type: id,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -96,6 +102,9 @@ impl MacPlatform {
|
||||||
fonts: Arc::new(FontSystem::new()),
|
fonts: Arc::new(FontSystem::new()),
|
||||||
callbacks: Default::default(),
|
callbacks: Default::default(),
|
||||||
menu_item_actions: Default::default(),
|
menu_item_actions: Default::default(),
|
||||||
|
pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
|
||||||
|
text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
|
||||||
|
metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,6 +185,18 @@ impl MacPlatform {
|
||||||
|
|
||||||
menu_bar
|
menu_bar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe fn read_from_pasteboard(&self, kind: id) -> Option<&[u8]> {
|
||||||
|
let data = self.pasteboard.dataForType(kind);
|
||||||
|
if data == nil {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(slice::from_raw_parts(
|
||||||
|
data.bytes() as *mut u8,
|
||||||
|
data.length() as usize,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl platform::Platform for MacPlatform {
|
impl platform::Platform for MacPlatform {
|
||||||
|
@ -247,31 +268,40 @@ impl platform::Platform for MacPlatform {
|
||||||
fn prompt_for_paths(
|
fn prompt_for_paths(
|
||||||
&self,
|
&self,
|
||||||
options: platform::PathPromptOptions,
|
options: platform::PathPromptOptions,
|
||||||
) -> Option<Vec<std::path::PathBuf>> {
|
done_fn: Box<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
|
||||||
|
) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let panel = NSOpenPanel::openPanel(nil);
|
let panel = NSOpenPanel::openPanel(nil);
|
||||||
panel.setCanChooseDirectories_(options.directories.to_objc());
|
panel.setCanChooseDirectories_(options.directories.to_objc());
|
||||||
panel.setCanChooseFiles_(options.files.to_objc());
|
panel.setCanChooseFiles_(options.files.to_objc());
|
||||||
panel.setAllowsMultipleSelection_(options.multiple.to_objc());
|
panel.setAllowsMultipleSelection_(options.multiple.to_objc());
|
||||||
panel.setResolvesAliases_(false.to_objc());
|
panel.setResolvesAliases_(false.to_objc());
|
||||||
let response = panel.runModal();
|
let done_fn = Cell::new(Some(done_fn));
|
||||||
if response == NSModalResponse::NSModalResponseOk {
|
let block = ConcreteBlock::new(move |response: NSModalResponse| {
|
||||||
let mut result = Vec::new();
|
let result = if response == NSModalResponse::NSModalResponseOk {
|
||||||
let urls = panel.URLs();
|
let mut result = Vec::new();
|
||||||
for i in 0..urls.count() {
|
let urls = panel.URLs();
|
||||||
let url = urls.objectAtIndex(i);
|
for i in 0..urls.count() {
|
||||||
let string = url.absoluteString();
|
let url = urls.objectAtIndex(i);
|
||||||
let string = std::ffi::CStr::from_ptr(string.UTF8String())
|
let string = url.absoluteString();
|
||||||
.to_string_lossy()
|
let string = std::ffi::CStr::from_ptr(string.UTF8String())
|
||||||
.to_string();
|
.to_string_lossy()
|
||||||
if let Some(path) = string.strip_prefix("file://") {
|
.to_string();
|
||||||
result.push(PathBuf::from(path));
|
if let Some(path) = string.strip_prefix("file://") {
|
||||||
|
result.push(PathBuf::from(path));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Some(result)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(done_fn) = done_fn.take() {
|
||||||
|
(done_fn)(result);
|
||||||
}
|
}
|
||||||
Some(result)
|
});
|
||||||
} else {
|
let block = block.copy();
|
||||||
None
|
let _: () = msg_send![panel, beginWithCompletionHandler: block];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,16 +316,72 @@ impl platform::Platform for MacPlatform {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy(&self, text: &str) {
|
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let data = NSData::dataWithBytes_length_(
|
self.pasteboard.clearContents();
|
||||||
|
|
||||||
|
let text_bytes = NSData::dataWithBytes_length_(
|
||||||
nil,
|
nil,
|
||||||
text.as_ptr() as *const c_void,
|
item.text.as_ptr() as *const c_void,
|
||||||
text.len() as u64,
|
item.text.len() as u64,
|
||||||
);
|
);
|
||||||
let pasteboard = NSPasteboard::generalPasteboard(nil);
|
self.pasteboard
|
||||||
pasteboard.clearContents();
|
.setData_forType(text_bytes, NSPasteboardTypeString);
|
||||||
pasteboard.setData_forType(data, NSPasteboardTypeString);
|
|
||||||
|
if let Some(metadata) = item.metadata.as_ref() {
|
||||||
|
let hash_bytes = ClipboardItem::text_hash(&item.text).to_be_bytes();
|
||||||
|
let hash_bytes = NSData::dataWithBytes_length_(
|
||||||
|
nil,
|
||||||
|
hash_bytes.as_ptr() as *const c_void,
|
||||||
|
hash_bytes.len() as u64,
|
||||||
|
);
|
||||||
|
self.pasteboard
|
||||||
|
.setData_forType(hash_bytes, self.text_hash_pasteboard_type);
|
||||||
|
|
||||||
|
let metadata_bytes = NSData::dataWithBytes_length_(
|
||||||
|
nil,
|
||||||
|
metadata.as_ptr() as *const c_void,
|
||||||
|
metadata.len() as u64,
|
||||||
|
);
|
||||||
|
self.pasteboard
|
||||||
|
.setData_forType(metadata_bytes, self.metadata_pasteboard_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||||
|
unsafe {
|
||||||
|
if let Some(text_bytes) = self.read_from_pasteboard(NSPasteboardTypeString) {
|
||||||
|
let text = String::from_utf8_lossy(&text_bytes).to_string();
|
||||||
|
let hash_bytes = self
|
||||||
|
.read_from_pasteboard(self.text_hash_pasteboard_type)
|
||||||
|
.and_then(|bytes| bytes.try_into().ok())
|
||||||
|
.map(u64::from_be_bytes);
|
||||||
|
let metadata_bytes = self
|
||||||
|
.read_from_pasteboard(self.metadata_pasteboard_type)
|
||||||
|
.and_then(|bytes| String::from_utf8(bytes.to_vec()).ok());
|
||||||
|
|
||||||
|
if let Some((hash, metadata)) = hash_bytes.zip(metadata_bytes) {
|
||||||
|
if hash == ClipboardItem::text_hash(&text) {
|
||||||
|
Some(ClipboardItem {
|
||||||
|
text,
|
||||||
|
metadata: Some(metadata),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Some(ClipboardItem {
|
||||||
|
text,
|
||||||
|
metadata: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(ClipboardItem {
|
||||||
|
text,
|
||||||
|
metadata: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -392,3 +478,46 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
|
||||||
unsafe fn ns_string(string: &str) -> id {
|
unsafe fn ns_string(string: &str) -> id {
|
||||||
NSString::alloc(nil).init_str(string).autorelease()
|
NSString::alloc(nil).init_str(string).autorelease()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::platform::Platform;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clipboard() {
|
||||||
|
let platform = build_platform();
|
||||||
|
assert_eq!(platform.read_from_clipboard(), None);
|
||||||
|
|
||||||
|
let item = ClipboardItem::new("1".to_string());
|
||||||
|
platform.write_to_clipboard(item.clone());
|
||||||
|
assert_eq!(platform.read_from_clipboard(), Some(item));
|
||||||
|
|
||||||
|
let item = ClipboardItem::new("2".to_string()).with_metadata(vec![3, 4]);
|
||||||
|
platform.write_to_clipboard(item.clone());
|
||||||
|
assert_eq!(platform.read_from_clipboard(), Some(item));
|
||||||
|
|
||||||
|
let text_from_other_app = "text from other app";
|
||||||
|
unsafe {
|
||||||
|
let bytes = NSData::dataWithBytes_length_(
|
||||||
|
nil,
|
||||||
|
text_from_other_app.as_ptr() as *const c_void,
|
||||||
|
text_from_other_app.len() as u64,
|
||||||
|
);
|
||||||
|
platform
|
||||||
|
.pasteboard
|
||||||
|
.setData_forType(bytes, NSPasteboardTypeString);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
platform.read_from_clipboard(),
|
||||||
|
Some(ClipboardItem::new(text_from_other_app.to_string()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_platform() -> MacPlatform {
|
||||||
|
let mut platform = MacPlatform::new();
|
||||||
|
platform.pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
|
||||||
|
platform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ use crate::{
|
||||||
vector::Vector2F,
|
vector::Vector2F,
|
||||||
},
|
},
|
||||||
text_layout::Line,
|
text_layout::Line,
|
||||||
Menu, Scene,
|
ClipboardItem, Menu, Scene,
|
||||||
};
|
};
|
||||||
use async_task::Runnable;
|
use async_task::Runnable;
|
||||||
pub use event::Event;
|
pub use event::Event;
|
||||||
|
@ -40,9 +40,14 @@ pub trait Platform {
|
||||||
executor: Rc<executor::Foreground>,
|
executor: Rc<executor::Foreground>,
|
||||||
) -> Box<dyn Window>;
|
) -> Box<dyn Window>;
|
||||||
fn key_window_id(&self) -> Option<usize>;
|
fn key_window_id(&self) -> Option<usize>;
|
||||||
fn prompt_for_paths(&self, options: PathPromptOptions) -> Option<Vec<PathBuf>>;
|
fn prompt_for_paths(
|
||||||
|
&self,
|
||||||
|
options: PathPromptOptions,
|
||||||
|
done_fn: Box<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
|
||||||
|
);
|
||||||
fn quit(&self);
|
fn quit(&self);
|
||||||
fn copy(&self, text: &str);
|
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||||
|
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||||
fn set_menus(&self, menus: Vec<Menu>);
|
fn set_menus(&self, menus: Vec<Menu>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
use crate::ClipboardItem;
|
||||||
use pathfinder_geometry::vector::Vector2F;
|
use pathfinder_geometry::vector::Vector2F;
|
||||||
use std::sync::Arc;
|
use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc};
|
||||||
use std::{any::Any, rc::Rc};
|
|
||||||
|
|
||||||
struct Platform {
|
struct Platform {
|
||||||
dispatcher: Arc<dyn super::Dispatcher>,
|
dispatcher: Arc<dyn super::Dispatcher>,
|
||||||
fonts: Arc<dyn super::FontSystem>,
|
fonts: Arc<dyn super::FontSystem>,
|
||||||
|
current_clipboard_item: RefCell<Option<ClipboardItem>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Dispatcher;
|
struct Dispatcher;
|
||||||
|
@ -22,6 +23,7 @@ impl Platform {
|
||||||
Self {
|
Self {
|
||||||
dispatcher: Arc::new(Dispatcher),
|
dispatcher: Arc::new(Dispatcher),
|
||||||
fonts: Arc::new(super::current::FontSystem::new()),
|
fonts: Arc::new(super::current::FontSystem::new()),
|
||||||
|
current_clipboard_item: RefCell::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,11 +70,20 @@ impl super::Platform for Platform {
|
||||||
|
|
||||||
fn quit(&self) {}
|
fn quit(&self) {}
|
||||||
|
|
||||||
fn prompt_for_paths(&self, _: super::PathPromptOptions) -> Option<Vec<std::path::PathBuf>> {
|
fn prompt_for_paths(
|
||||||
None
|
&self,
|
||||||
|
_: super::PathPromptOptions,
|
||||||
|
_: Box<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy(&self, _: &str) {}
|
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||||
|
*self.current_clipboard_item.borrow_mut() = Some(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||||
|
self.current_clipboard_item.borrow().clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
|
|
|
@ -31,6 +31,7 @@ rand = "0.8.3"
|
||||||
rust-embed = "5.9.0"
|
rust-embed = "5.9.0"
|
||||||
seahash = "4.1"
|
seahash = "4.1"
|
||||||
simplelog = "0.9"
|
simplelog = "0.9"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
smallvec = "1.6.1"
|
smallvec = "1.6.1"
|
||||||
smol = "1.2.5"
|
smol = "1.2.5"
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,28 @@ impl Anchor {
|
||||||
.then_with(|| self_bias.cmp(other_bias)),
|
.then_with(|| self_bias.cmp(other_bias)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn bias_left(&self, buffer: &Buffer) -> Result<Anchor> {
|
||||||
|
match self {
|
||||||
|
Anchor::Start
|
||||||
|
| Anchor::Middle {
|
||||||
|
bias: AnchorBias::Left,
|
||||||
|
..
|
||||||
|
} => Ok(self.clone()),
|
||||||
|
_ => buffer.anchor_before(self),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bias_right(&self, buffer: &Buffer) -> Result<Anchor> {
|
||||||
|
match self {
|
||||||
|
Anchor::End
|
||||||
|
| Anchor::Middle {
|
||||||
|
bias: AnchorBias::Right,
|
||||||
|
..
|
||||||
|
} => Ok(self.clone()),
|
||||||
|
_ => buffer.anchor_after(self),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait AnchorRangeExt {
|
pub trait AnchorRangeExt {
|
||||||
|
|
|
@ -563,10 +563,13 @@ impl Buffer {
|
||||||
self.chars().collect()
|
self.chars().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_for_range<T: ToOffset>(&self, range: Range<T>) -> Result<String> {
|
pub fn text_for_range<'a, T: ToOffset>(
|
||||||
|
&'a self,
|
||||||
|
range: Range<T>,
|
||||||
|
) -> Result<impl 'a + Iterator<Item = char>> {
|
||||||
let start = range.start.to_offset(self)?;
|
let start = range.start.to_offset(self)?;
|
||||||
let end = range.end.to_offset(self)?;
|
let end = range.end.to_offset(self)?;
|
||||||
Ok(self.chars_at(start)?.take(end - start).collect())
|
Ok(self.chars_at(start)?.take(end - start))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn chars(&self) -> CharIter {
|
pub fn chars(&self) -> CharIter {
|
||||||
|
@ -2261,6 +2264,12 @@ impl ToOffset for Anchor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> ToOffset for &'a Anchor {
|
||||||
|
fn to_offset(&self, buffer: &Buffer) -> Result<usize> {
|
||||||
|
Ok(buffer.summary_for_anchor(self)?.chars)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait ToPoint {
|
pub trait ToPoint {
|
||||||
fn to_point(&self, buffer: &Buffer) -> Result<Point>;
|
fn to_point(&self, buffer: &Buffer) -> Result<Point>;
|
||||||
}
|
}
|
||||||
|
@ -2470,13 +2479,9 @@ mod tests {
|
||||||
let old_len = old_range.end - old_range.start;
|
let old_len = old_range.end - old_range.start;
|
||||||
let new_len = new_range.end - new_range.start;
|
let new_len = new_range.end - new_range.start;
|
||||||
let old_start = (old_range.start as isize + delta) as usize;
|
let old_start = (old_range.start as isize + delta) as usize;
|
||||||
|
let new_text: String = buffer.text_for_range(new_range).unwrap().collect();
|
||||||
old_buffer
|
old_buffer
|
||||||
.edit(
|
.edit(Some(old_start..old_start + old_len), new_text, None)
|
||||||
Some(old_start..old_start + old_len),
|
|
||||||
buffer.text_for_range(new_range).unwrap(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
delta += new_len as isize - old_len as isize;
|
delta += new_len as isize - old_len as isize;
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
use super::{
|
use super::{
|
||||||
buffer, movement, Anchor, Bias, Buffer, BufferElement, DisplayMap, DisplayPoint, Point,
|
buffer, movement, Anchor, Bias, Buffer, BufferElement, DisplayMap, DisplayPoint, Point,
|
||||||
Selection, SelectionSetId, ToOffset,
|
Selection, SelectionSetId, ToOffset, ToPoint,
|
||||||
};
|
};
|
||||||
use crate::{settings::Settings, watch, workspace};
|
use crate::{settings::Settings, watch, workspace};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
fonts::Properties as FontProperties, keymap::Binding, text_layout, AppContext, Element,
|
fonts::Properties as FontProperties, keymap::Binding, text_layout, AppContext, ClipboardItem,
|
||||||
ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, View, ViewContext,
|
Element, ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, View, ViewContext,
|
||||||
WeakViewHandle,
|
WeakViewHandle,
|
||||||
};
|
};
|
||||||
use gpui::{geometry::vector::Vector2F, TextLayoutCache};
|
use gpui::{geometry::vector::Vector2F, TextLayoutCache};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
|
iter::FromIterator,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
|
@ -28,6 +30,9 @@ pub fn init(app: &mut MutableAppContext) {
|
||||||
app.add_bindings(vec![
|
app.add_bindings(vec![
|
||||||
Binding::new("backspace", "buffer:backspace", Some("BufferView")),
|
Binding::new("backspace", "buffer:backspace", Some("BufferView")),
|
||||||
Binding::new("enter", "buffer:newline", Some("BufferView")),
|
Binding::new("enter", "buffer:newline", Some("BufferView")),
|
||||||
|
Binding::new("cmd-x", "buffer:cut", Some("BufferView")),
|
||||||
|
Binding::new("cmd-c", "buffer:copy", Some("BufferView")),
|
||||||
|
Binding::new("cmd-v", "buffer:paste", Some("BufferView")),
|
||||||
Binding::new("cmd-z", "buffer:undo", Some("BufferView")),
|
Binding::new("cmd-z", "buffer:undo", Some("BufferView")),
|
||||||
Binding::new("cmd-shift-Z", "buffer:redo", Some("BufferView")),
|
Binding::new("cmd-shift-Z", "buffer:redo", Some("BufferView")),
|
||||||
Binding::new("up", "buffer:move_up", Some("BufferView")),
|
Binding::new("up", "buffer:move_up", Some("BufferView")),
|
||||||
|
@ -54,6 +59,9 @@ pub fn init(app: &mut MutableAppContext) {
|
||||||
app.add_action("buffer:insert", BufferView::insert);
|
app.add_action("buffer:insert", BufferView::insert);
|
||||||
app.add_action("buffer:newline", BufferView::newline);
|
app.add_action("buffer:newline", BufferView::newline);
|
||||||
app.add_action("buffer:backspace", BufferView::backspace);
|
app.add_action("buffer:backspace", BufferView::backspace);
|
||||||
|
app.add_action("buffer:cut", BufferView::cut);
|
||||||
|
app.add_action("buffer:copy", BufferView::copy);
|
||||||
|
app.add_action("buffer:paste", BufferView::paste);
|
||||||
app.add_action("buffer:undo", BufferView::undo);
|
app.add_action("buffer:undo", BufferView::undo);
|
||||||
app.add_action("buffer:redo", BufferView::redo);
|
app.add_action("buffer:redo", BufferView::redo);
|
||||||
app.add_action("buffer:move_up", BufferView::move_up);
|
app.add_action("buffer:move_up", BufferView::move_up);
|
||||||
|
@ -102,6 +110,12 @@ pub struct BufferView {
|
||||||
single_line: bool,
|
single_line: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct ClipboardSelection {
|
||||||
|
len: usize,
|
||||||
|
is_entire_line: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl BufferView {
|
impl BufferView {
|
||||||
pub fn single_line(settings: watch::Receiver<Settings>, ctx: &mut ViewContext<Self>) -> Self {
|
pub fn single_line(settings: watch::Receiver<Settings>, ctx: &mut ViewContext<Self>) -> Self {
|
||||||
let buffer = ctx.add_model(|_| Buffer::new(0, String::new()));
|
let buffer = ctx.add_model(|_| Buffer::new(0, String::new()));
|
||||||
|
@ -354,6 +368,25 @@ impl BufferView {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn select_ranges<'a, T>(&mut self, ranges: T, ctx: &mut ViewContext<Self>) -> Result<()>
|
fn select_ranges<'a, T>(&mut self, ranges: T, ctx: &mut ViewContext<Self>) -> Result<()>
|
||||||
|
where
|
||||||
|
T: IntoIterator<Item = &'a Range<usize>>,
|
||||||
|
{
|
||||||
|
let buffer = self.buffer.read(ctx);
|
||||||
|
let mut selections = Vec::new();
|
||||||
|
for range in ranges {
|
||||||
|
selections.push(Selection {
|
||||||
|
start: buffer.anchor_before(range.start)?,
|
||||||
|
end: buffer.anchor_before(range.end)?,
|
||||||
|
reversed: false,
|
||||||
|
goal_column: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.update_selections(selections, ctx);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn select_display_ranges<'a, T>(&mut self, ranges: T, ctx: &mut ViewContext<Self>) -> Result<()>
|
||||||
where
|
where
|
||||||
T: IntoIterator<Item = &'a Range<DisplayPoint>>,
|
T: IntoIterator<Item = &'a Range<DisplayPoint>>,
|
||||||
{
|
{
|
||||||
|
@ -361,7 +394,7 @@ impl BufferView {
|
||||||
let mut selections = Vec::new();
|
let mut selections = Vec::new();
|
||||||
for range in ranges {
|
for range in ranges {
|
||||||
selections.push(Selection {
|
selections.push(Selection {
|
||||||
start: map.anchor_after(range.start, Bias::Left, ctx.as_ref())?,
|
start: map.anchor_before(range.start, Bias::Left, ctx.as_ref())?,
|
||||||
end: map.anchor_before(range.end, Bias::Left, ctx.as_ref())?,
|
end: map.anchor_before(range.end, Bias::Left, ctx.as_ref())?,
|
||||||
reversed: false,
|
reversed: false,
|
||||||
goal_column: None,
|
goal_column: None,
|
||||||
|
@ -454,6 +487,133 @@ impl BufferView {
|
||||||
self.end_transaction(ctx);
|
self.end_transaction(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cut(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
|
||||||
|
self.start_transaction(ctx);
|
||||||
|
let mut text = String::new();
|
||||||
|
let mut selections = self.selections(ctx.as_ref()).to_vec();
|
||||||
|
let mut clipboard_selections = Vec::with_capacity(selections.len());
|
||||||
|
{
|
||||||
|
let buffer = self.buffer.read(ctx);
|
||||||
|
let max_point = buffer.max_point();
|
||||||
|
for selection in &mut selections {
|
||||||
|
let mut start = selection.start.to_point(buffer).expect("invalid start");
|
||||||
|
let mut end = selection.end.to_point(buffer).expect("invalid end");
|
||||||
|
let is_entire_line = start == end;
|
||||||
|
if is_entire_line {
|
||||||
|
start = Point::new(start.row, 0);
|
||||||
|
end = cmp::min(max_point, Point::new(start.row + 1, 0));
|
||||||
|
selection.start = buffer.anchor_before(start).unwrap();
|
||||||
|
selection.end = buffer.anchor_before(end).unwrap();
|
||||||
|
}
|
||||||
|
let mut len = 0;
|
||||||
|
for ch in buffer.text_for_range(start..end).unwrap() {
|
||||||
|
text.push(ch);
|
||||||
|
len += 1;
|
||||||
|
}
|
||||||
|
clipboard_selections.push(ClipboardSelection {
|
||||||
|
len,
|
||||||
|
is_entire_line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.update_selections(selections, ctx);
|
||||||
|
self.changed_selections(ctx);
|
||||||
|
self.insert(&String::new(), ctx);
|
||||||
|
self.end_transaction(ctx);
|
||||||
|
|
||||||
|
ctx.as_mut()
|
||||||
|
.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
|
||||||
|
let buffer = self.buffer.read(ctx);
|
||||||
|
let max_point = buffer.max_point();
|
||||||
|
let mut text = String::new();
|
||||||
|
let selections = self.selections(ctx.as_ref());
|
||||||
|
let mut clipboard_selections = Vec::with_capacity(selections.len());
|
||||||
|
for selection in selections {
|
||||||
|
let mut start = selection.start.to_point(buffer).expect("invalid start");
|
||||||
|
let mut end = selection.end.to_point(buffer).expect("invalid end");
|
||||||
|
let is_entire_line = start == end;
|
||||||
|
if is_entire_line {
|
||||||
|
start = Point::new(start.row, 0);
|
||||||
|
end = cmp::min(max_point, Point::new(start.row + 1, 0));
|
||||||
|
}
|
||||||
|
let mut len = 0;
|
||||||
|
for ch in buffer.text_for_range(start..end).unwrap() {
|
||||||
|
text.push(ch);
|
||||||
|
len += 1;
|
||||||
|
}
|
||||||
|
clipboard_selections.push(ClipboardSelection {
|
||||||
|
len,
|
||||||
|
is_entire_line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.as_mut()
|
||||||
|
.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paste(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(item) = ctx.as_mut().read_from_clipboard() {
|
||||||
|
let clipboard_text = item.text();
|
||||||
|
if let Some(mut clipboard_selections) = item.metadata::<Vec<ClipboardSelection>>() {
|
||||||
|
let selections = self.selections(ctx.as_ref()).to_vec();
|
||||||
|
if clipboard_selections.len() != selections.len() {
|
||||||
|
let merged_selection = ClipboardSelection {
|
||||||
|
len: clipboard_selections.iter().map(|s| s.len).sum(),
|
||||||
|
is_entire_line: clipboard_selections.iter().all(|s| s.is_entire_line),
|
||||||
|
};
|
||||||
|
clipboard_selections.clear();
|
||||||
|
clipboard_selections.push(merged_selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.start_transaction(ctx);
|
||||||
|
let mut new_selections = Vec::with_capacity(selections.len());
|
||||||
|
let mut clipboard_chars = clipboard_text.chars().cycle();
|
||||||
|
for (selection, clipboard_selection) in
|
||||||
|
selections.iter().zip(clipboard_selections.iter().cycle())
|
||||||
|
{
|
||||||
|
let to_insert =
|
||||||
|
String::from_iter(clipboard_chars.by_ref().take(clipboard_selection.len));
|
||||||
|
|
||||||
|
self.buffer.update(ctx, |buffer, ctx| {
|
||||||
|
let selection_start = selection.start.to_point(buffer).unwrap();
|
||||||
|
let selection_end = selection.end.to_point(buffer).unwrap();
|
||||||
|
|
||||||
|
// If the corresponding selection was empty when this slice of the
|
||||||
|
// clipboard text was written, then the entire line containing the
|
||||||
|
// selection was copied. If this selection is also currently empty,
|
||||||
|
// then paste the line before the current line of the buffer.
|
||||||
|
let new_selection_start = selection.end.bias_right(buffer).unwrap();
|
||||||
|
if selection_start == selection_end && clipboard_selection.is_entire_line {
|
||||||
|
let line_start = Point::new(selection_start.row, 0);
|
||||||
|
buffer
|
||||||
|
.edit(Some(line_start..line_start), to_insert, Some(ctx))
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
buffer
|
||||||
|
.edit(Some(&selection.start..&selection.end), to_insert, Some(ctx))
|
||||||
|
.unwrap();
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_selection_start = new_selection_start.bias_left(buffer).unwrap();
|
||||||
|
new_selections.push(Selection {
|
||||||
|
start: new_selection_start.clone(),
|
||||||
|
end: new_selection_start,
|
||||||
|
reversed: false,
|
||||||
|
goal_column: None,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.update_selections(new_selections, ctx);
|
||||||
|
self.end_transaction(ctx);
|
||||||
|
} else {
|
||||||
|
self.insert(clipboard_text, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn undo(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
|
pub fn undo(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
|
||||||
self.buffer
|
self.buffer
|
||||||
.update(ctx, |buffer, ctx| buffer.undo(Some(ctx)));
|
.update(ctx, |buffer, ctx| buffer.undo(Some(ctx)));
|
||||||
|
@ -1417,8 +1577,11 @@ mod tests {
|
||||||
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
|
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
|
||||||
|
|
||||||
view.update(app, |view, ctx| {
|
view.update(app, |view, ctx| {
|
||||||
view.select_ranges(&[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)], ctx)
|
view.select_display_ranges(
|
||||||
.unwrap();
|
&[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)],
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
view.fold(&(), ctx);
|
view.fold(&(), ctx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view.text(ctx.as_ref()),
|
view.text(ctx.as_ref()),
|
||||||
|
@ -1525,7 +1688,7 @@ mod tests {
|
||||||
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
|
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
|
||||||
|
|
||||||
view.update(app, |view, ctx| {
|
view.update(app, |view, ctx| {
|
||||||
view.select_ranges(
|
view.select_display_ranges(
|
||||||
&[
|
&[
|
||||||
// an empty selection - the preceding character is deleted
|
// an empty selection - the preceding character is deleted
|
||||||
DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
|
DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
|
||||||
|
@ -1547,6 +1710,147 @@ mod tests {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clipboard() {
|
||||||
|
App::test((), |app| {
|
||||||
|
let buffer = app.add_model(|_| Buffer::new(0, "one two three four five six "));
|
||||||
|
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||||
|
let view = app
|
||||||
|
.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx))
|
||||||
|
.1;
|
||||||
|
|
||||||
|
// Cut with three selections. Clipboard text is divided into three slices.
|
||||||
|
view.update(app, |view, ctx| {
|
||||||
|
view.select_ranges(&[0..4, 8..14, 19..24], ctx).unwrap();
|
||||||
|
view.cut(&(), ctx);
|
||||||
|
});
|
||||||
|
assert_eq!(view.read(app).text(app.as_ref()), "two four six ");
|
||||||
|
|
||||||
|
// Paste with three cursors. Each cursor pastes one slice of the clipboard text.
|
||||||
|
view.update(app, |view, ctx| {
|
||||||
|
view.select_ranges(&[4..4, 9..9, 13..13], ctx).unwrap();
|
||||||
|
view.paste(&(), ctx);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
view.read(app).text(app.as_ref()),
|
||||||
|
"two one four three six five "
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
view.read(app).selection_ranges(app.as_ref()),
|
||||||
|
&[
|
||||||
|
DisplayPoint::new(0, 8)..DisplayPoint::new(0, 8),
|
||||||
|
DisplayPoint::new(0, 19)..DisplayPoint::new(0, 19),
|
||||||
|
DisplayPoint::new(0, 28)..DisplayPoint::new(0, 28)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Paste again but with only two cursors. Since the number of cursors doesn't
|
||||||
|
// match the number of slices in the clipboard, the entire clipboard text
|
||||||
|
// is pasted at each cursor.
|
||||||
|
view.update(app, |view, ctx| {
|
||||||
|
view.select_ranges(&[0..0, 28..28], ctx).unwrap();
|
||||||
|
view.insert(&"( ".to_string(), ctx);
|
||||||
|
view.paste(&(), ctx);
|
||||||
|
view.insert(&") ".to_string(), ctx);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
view.read(app).text(app.as_ref()),
|
||||||
|
"( one three five ) two one four three six five ( one three five ) "
|
||||||
|
);
|
||||||
|
|
||||||
|
view.update(app, |view, ctx| {
|
||||||
|
view.select_ranges(&[0..0], ctx).unwrap();
|
||||||
|
view.insert(&"123\n4567\n89\n".to_string(), ctx);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
view.read(app).text(app.as_ref()),
|
||||||
|
"123\n4567\n89\n( one three five ) two one four three six five ( one three five ) "
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cut with three selections, one of which is full-line.
|
||||||
|
view.update(app, |view, ctx| {
|
||||||
|
view.select_display_ranges(
|
||||||
|
&[
|
||||||
|
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2),
|
||||||
|
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
|
||||||
|
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1),
|
||||||
|
],
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
view.cut(&(), ctx);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
view.read(app).text(app.as_ref()),
|
||||||
|
"13\n9\n( one three five ) two one four three six five ( one three five ) "
|
||||||
|
);
|
||||||
|
|
||||||
|
// Paste with three selections, noticing how the copied selection that was full-line
|
||||||
|
// gets inserted before the second cursor.
|
||||||
|
view.update(app, |view, ctx| {
|
||||||
|
view.select_display_ranges(
|
||||||
|
&[
|
||||||
|
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
|
||||||
|
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
|
||||||
|
DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3),
|
||||||
|
],
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
view.paste(&(), ctx);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
view.read(app).text(app.as_ref()),
|
||||||
|
"123\n4567\n9\n( 8ne three five ) two one four three six five ( one three five ) "
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
view.read(app).selection_ranges(app.as_ref()),
|
||||||
|
&[
|
||||||
|
DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
|
||||||
|
DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
|
||||||
|
DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy with a single cursor only, which writes the whole line into the clipboard.
|
||||||
|
view.update(app, |view, ctx| {
|
||||||
|
view.select_display_ranges(
|
||||||
|
&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)],
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
view.copy(&(), ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Paste with three selections, noticing how the copied full-line selection is inserted
|
||||||
|
// before the empty selections but replaces the selection that is non-empty.
|
||||||
|
view.update(app, |view, ctx| {
|
||||||
|
view.select_display_ranges(
|
||||||
|
&[
|
||||||
|
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
|
||||||
|
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 2),
|
||||||
|
DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
|
||||||
|
],
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
view.paste(&(), ctx);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
view.read(app).text(app.as_ref()),
|
||||||
|
"123\n123\n123\n67\n123\n9\n( 8ne three five ) two one four three six five ( one three five ) "
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
view.read(app).selection_ranges(app.as_ref()),
|
||||||
|
&[
|
||||||
|
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
|
||||||
|
DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
|
||||||
|
DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
impl BufferView {
|
impl BufferView {
|
||||||
fn selection_ranges(&self, app: &AppContext) -> Vec<Range<DisplayPoint>> {
|
fn selection_ranges(&self, app: &AppContext) -> Vec<Range<DisplayPoint>> {
|
||||||
self.selections_in_range(DisplayPoint::zero()..self.max_point(app), app)
|
self.selections_in_range(DisplayPoint::zero()..self.max_point(app), app)
|
||||||
|
|
|
@ -77,7 +77,7 @@ impl View for FileFinder {
|
||||||
.with_max_height(400.0)
|
.with_max_height(400.0)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.top_center()
|
.top()
|
||||||
.named("file finder")
|
.named("file finder")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,19 +29,19 @@ pub struct OpenParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open(settings: &Receiver<Settings>, ctx: &mut MutableAppContext) {
|
fn open(settings: &Receiver<Settings>, ctx: &mut MutableAppContext) {
|
||||||
if let Some(paths) = ctx.platform().prompt_for_paths(PathPromptOptions {
|
let settings = settings.clone();
|
||||||
files: true,
|
ctx.prompt_for_paths(
|
||||||
directories: true,
|
PathPromptOptions {
|
||||||
multiple: true,
|
files: true,
|
||||||
}) {
|
directories: true,
|
||||||
ctx.dispatch_global_action(
|
multiple: true,
|
||||||
"workspace:open_paths",
|
},
|
||||||
OpenParams {
|
move |paths, ctx| {
|
||||||
paths,
|
if let Some(paths) = paths {
|
||||||
settings: settings.clone(),
|
ctx.dispatch_global_action("workspace:open_paths", OpenParams { paths, settings });
|
||||||
},
|
}
|
||||||
);
|
},
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_paths(params: &OpenParams, app: &mut MutableAppContext) {
|
fn open_paths(params: &OpenParams, app: &mut MutableAppContext) {
|
||||||
|
|
|
@ -189,33 +189,28 @@ impl Pane {
|
||||||
|
|
||||||
let padding = 6.;
|
let padding = 6.;
|
||||||
let mut container = Container::new(
|
let mut container = Container::new(
|
||||||
Align::new(
|
Stack::new()
|
||||||
Flex::row()
|
.with_child(
|
||||||
.with_child(
|
Align::new(
|
||||||
Label::new(title, settings.ui_font_family, settings.ui_font_size)
|
Label::new(title, settings.ui_font_family, settings.ui_font_size)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_child(
|
.boxed(),
|
||||||
Container::new(
|
)
|
||||||
LineBox::new(
|
.with_child(
|
||||||
settings.ui_font_family,
|
LineBox::new(
|
||||||
settings.ui_font_size,
|
settings.ui_font_family,
|
||||||
ConstrainedBox::new(Self::render_modified_icon(
|
settings.ui_font_size,
|
||||||
item.is_dirty(app),
|
Align::new(Self::render_modified_icon(item.is_dirty(app)))
|
||||||
))
|
.right()
|
||||||
.with_max_width(12.)
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
|
||||||
.with_margin_left(20.)
|
|
||||||
.boxed(),
|
|
||||||
)
|
)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_uniform_padding(padding)
|
.with_vertical_padding(padding)
|
||||||
|
.with_horizontal_padding(10.)
|
||||||
.with_border(border);
|
.with_border(border);
|
||||||
|
|
||||||
if ix == self.active_item {
|
if ix == self.active_item {
|
||||||
|
@ -237,6 +232,7 @@ impl Pane {
|
||||||
})
|
})
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
|
.with_min_width(80.0)
|
||||||
.with_max_width(264.0)
|
.with_max_width(264.0)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
|
@ -244,9 +240,29 @@ impl Pane {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure there's always a minimum amount of space after the last tab,
|
||||||
|
// so that the tab's border doesn't abut the window's border.
|
||||||
|
row.add_child(
|
||||||
|
ConstrainedBox::new(
|
||||||
|
Container::new(
|
||||||
|
LineBox::new(
|
||||||
|
settings.ui_font_family,
|
||||||
|
settings.ui_font_size,
|
||||||
|
Empty::new().boxed(),
|
||||||
|
)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_uniform_padding(6.0)
|
||||||
|
.with_border(Border::bottom(1.0, border_color))
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_min_width(20.)
|
||||||
|
.named("fixed-filler"),
|
||||||
|
);
|
||||||
|
|
||||||
row.add_child(
|
row.add_child(
|
||||||
Expanded::new(
|
Expanded::new(
|
||||||
1.0,
|
0.0,
|
||||||
Container::new(
|
Container::new(
|
||||||
LineBox::new(
|
LineBox::new(
|
||||||
settings.ui_font_family,
|
settings.ui_font_family,
|
||||||
|
@ -266,23 +282,24 @@ impl Pane {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_modified_icon(is_modified: bool) -> ElementBox {
|
fn render_modified_icon(is_modified: bool) -> ElementBox {
|
||||||
Canvas::new(move |bounds, ctx| {
|
let diameter = 8.;
|
||||||
if is_modified {
|
ConstrainedBox::new(
|
||||||
let padding = if bounds.height() < bounds.width() {
|
Canvas::new(move |bounds, ctx| {
|
||||||
vec2f(bounds.width() - bounds.height(), 0.0)
|
if is_modified {
|
||||||
} else {
|
let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
|
||||||
vec2f(0.0, bounds.height() - bounds.width())
|
ctx.scene.push_quad(Quad {
|
||||||
};
|
bounds: square,
|
||||||
let square = RectF::new(bounds.origin() + padding / 2., bounds.size() - padding);
|
background: Some(ColorF::new(0.639, 0.839, 1.0, 1.0).to_u8()),
|
||||||
ctx.scene.push_quad(Quad {
|
border: Default::default(),
|
||||||
bounds: square,
|
corner_radius: diameter / 2.,
|
||||||
background: Some(ColorF::new(0.639, 0.839, 1.0, 1.0).to_u8()),
|
});
|
||||||
border: Default::default(),
|
}
|
||||||
corner_radius: square.width() / 2.,
|
})
|
||||||
});
|
.boxed(),
|
||||||
}
|
)
|
||||||
})
|
.with_width(diameter)
|
||||||
.boxed()
|
.with_height(diameter)
|
||||||
|
.named("tab-right-icon")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::{settings::Settings, watch};
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext,
|
color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext,
|
||||||
Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle,
|
ClipboardItem, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use std::{collections::HashSet, path::PathBuf};
|
use std::{collections::HashSet, path::PathBuf};
|
||||||
|
@ -258,10 +258,11 @@ impl WorkspaceView {
|
||||||
pub fn debug_elements(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
|
pub fn debug_elements(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
|
||||||
match to_string_pretty(&ctx.debug_elements()) {
|
match to_string_pretty(&ctx.debug_elements()) {
|
||||||
Ok(json) => {
|
Ok(json) => {
|
||||||
ctx.as_mut().copy(&json);
|
let kib = json.len() as f32 / 1024.;
|
||||||
|
ctx.as_mut().write_to_clipboard(ClipboardItem::new(json));
|
||||||
log::info!(
|
log::info!(
|
||||||
"copied {:.1} KiB of element debug JSON to the clipboard",
|
"copied {:.1} KiB of element debug JSON to the clipboard",
|
||||||
json.len() as f32 / 1024.
|
kib
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue