Merge branch 'main' into site-v2

This commit is contained in:
Nate
2021-09-20 16:01:03 -04:00
72 changed files with 3324 additions and 1870 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
clean: false
- name: Run tests
run: cargo test --no-fail-fast
run: cargo test --workspace --no-fail-fast
bundle:
name: Bundle app
Generated
+101 -10
View File
@@ -344,6 +344,17 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "async-recursion"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "async-rustls"
version = "0.1.2"
@@ -814,7 +825,7 @@ dependencies = [
"error-chain",
"glob 0.2.11",
"icns",
"image",
"image 0.12.4",
"libflate",
"md5",
"msi",
@@ -825,7 +836,7 @@ dependencies = [
"target_build_utils",
"term",
"toml 0.4.10",
"uuid",
"uuid 0.5.1",
"walkdir",
]
@@ -873,7 +884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e7fb075b9b54e939006aa12e1f6cd2d3194041ff4ebe7f2efcbedf18f25b667"
dependencies = [
"byteorder",
"uuid",
"uuid 0.5.1",
]
[[package]]
@@ -2102,6 +2113,16 @@ dependencies = [
"lzw",
]
[[package]]
name = "gif"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "gimli"
version = "0.23.0"
@@ -2167,6 +2188,7 @@ dependencies = [
"font-kit",
"foreign-types",
"gpui_macros",
"image 0.23.14",
"lazy_static",
"log",
"metal",
@@ -2462,15 +2484,34 @@ checksum = "d95816db758249fe16f23a4e23f1a3a817fe11892dbfd1c5836f625324702158"
dependencies = [
"byteorder",
"enum_primitive",
"gif",
"gif 0.9.2",
"jpeg-decoder",
"num-iter",
"num-rational",
"num-rational 0.1.42",
"num-traits 0.1.43",
"png 0.6.2",
"scoped_threadpool",
]
[[package]]
name = "image"
version = "0.23.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"gif 0.11.2",
"jpeg-decoder",
"num-iter",
"num-rational 0.3.2",
"num-traits 0.2.14",
"png 0.16.8",
"scoped_threadpool",
"tiff",
]
[[package]]
name = "indexmap"
version = "1.6.2"
@@ -2766,6 +2807,16 @@ dependencies = [
"value-bag",
]
[[package]]
name = "log-panics"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae0136257df209261daa18d6c16394757c63e032e27aafd8b07788b051082bef"
dependencies = [
"backtrace",
"log",
]
[[package]]
name = "loom"
version = "0.4.0"
@@ -2912,7 +2963,7 @@ dependencies = [
"byteorder",
"cfb",
"encoding",
"uuid",
"uuid 0.5.1",
]
[[package]]
@@ -3014,6 +3065,17 @@ dependencies = [
"num-traits 0.2.14",
]
[[package]]
name = "num-rational"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
"autocfg 1.0.1",
"num-integer",
"num-traits 0.2.14",
]
[[package]]
name = "num-traits"
version = "0.1.43"
@@ -4722,6 +4784,7 @@ dependencies = [
"thiserror",
"time 0.2.25",
"url",
"uuid 0.8.2",
"webpki",
"webpki-roots",
"whoami",
@@ -5067,18 +5130,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.24"
version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.24"
version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c"
dependencies = [
"proc-macro2",
"quote",
@@ -5129,6 +5192,17 @@ dependencies = [
"tide",
]
[[package]]
name = "tiff"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
dependencies = [
"jpeg-decoder",
"miniz_oxide 0.4.4",
"weezl",
]
[[package]]
name = "time"
version = "0.1.44"
@@ -5533,6 +5607,12 @@ dependencies = [
"sha1 0.2.0",
]
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
[[package]]
name = "value-bag"
version = "1.0.0-alpha.7"
@@ -5694,6 +5774,12 @@ dependencies = [
"webpki",
]
[[package]]
name = "weezl"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e"
[[package]]
name = "wepoll-sys"
version = "3.0.1"
@@ -5823,6 +5909,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"arrayvec 0.7.1",
"async-recursion",
"async-trait",
"async-tungstenite",
"cargo-bundle",
@@ -5836,9 +5923,12 @@ dependencies = [
"gpui",
"http-auth-basic",
"ignore",
"image 0.23.14",
"indexmap",
"lazy_static",
"libc",
"log",
"log-panics",
"num_cpus",
"parking_lot",
"postage",
@@ -5855,6 +5945,7 @@ dependencies = [
"smol",
"surf",
"tempdir",
"thiserror",
"time 0.3.2",
"tiny_http",
"toml 0.5.8",
+2 -2
View File
@@ -1,12 +1,12 @@
# syntax = docker/dockerfile:1.2
FROM rust as builder
FROM rust:1.55-bullseye as builder
WORKDIR app
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=./target \
cargo install sqlx-cli --root=/app --target-dir=/app/target --version 0.5.7
FROM debian:buster-slim as runtime
FROM debian:bullseye-slim as runtime
RUN apt-get update; \
apt-get install -y --no-install-recommends libssl1.1
WORKDIR app
+4
View File
@@ -4,6 +4,9 @@ edition = "2018"
name = "gpui"
version = "0.1.0"
[features]
test-support = []
[dependencies]
arrayvec = "0.7.1"
async-task = "4.0.3"
@@ -11,6 +14,7 @@ backtrace = "0.3"
ctor = "0.1"
etagere = "0.2"
gpui_macros = { path = "../gpui_macros" }
image = "0.23"
lazy_static = "1.4.0"
log = "0.4"
num_cpus = "1.13"
+20
View File
@@ -2282,6 +2282,16 @@ impl<'a, T: View> ViewContext<'a, T> {
let handle = self.handle();
self.app.spawn(|cx| f(handle, cx))
}
pub fn spawn_weak<F, Fut, S>(&self, f: F) -> Task<S>
where
F: FnOnce(WeakViewHandle<T>, AsyncAppContext) -> Fut,
Fut: 'static + Future<Output = S>,
S: 'static,
{
let handle = self.handle().downgrade();
self.app.spawn(|cx| f(handle, cx))
}
}
pub struct RenderContext<'a, T: View> {
@@ -2325,6 +2335,16 @@ impl<V: View> ReadModel for RenderContext<'_, V> {
}
}
impl<V: View> UpdateModel for RenderContext<'_, V> {
fn update_model<T, F, S>(&mut self, handle: &ModelHandle<T>, update: F) -> S
where
T: Entity,
F: FnOnce(&mut T, &mut ModelContext<T>) -> S,
{
self.app.update_model(handle, update)
}
}
impl<M> AsRef<AppContext> for ViewContext<'_, M> {
fn as_ref(&self) -> &AppContext {
&self.app.cx
+4
View File
@@ -29,6 +29,10 @@ impl Color {
Self(ColorU::white())
}
pub fn red() -> Self {
Self(ColorU::from_u32(0xff0000ff))
}
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self(ColorU::new(r, g, b, a))
}
+20 -19
View File
@@ -6,6 +6,7 @@ mod empty;
mod event_handler;
mod flex;
mod hook;
mod image;
mod label;
mod line_box;
mod list;
@@ -16,27 +17,17 @@ mod svg;
mod text;
mod uniform_list;
pub use self::{
align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
hook::*, image::*, label::*, line_box::*, list::*, mouse_event_handler::*, overlay::*,
stack::*, svg::*, text::*, uniform_list::*,
};
pub use crate::presenter::ChildView;
pub use align::*;
pub use canvas::*;
pub use constrained_box::*;
pub use container::*;
pub use empty::*;
pub use event_handler::*;
pub use flex::*;
pub use hook::*;
pub use label::*;
pub use line_box::*;
pub use list::*;
pub use mouse_event_handler::*;
pub use overlay::*;
pub use stack::*;
pub use svg::*;
pub use text::*;
pub use uniform_list::*;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
use core::panic;
@@ -371,3 +362,13 @@ pub trait ParentElement<'a>: Extend<ElementBox> + Sized {
}
impl<'a, T> ParentElement<'a> for T where T: Extend<ElementBox> {}
fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
if max_size.x().is_infinite() && max_size.y().is_infinite() {
size
} else if max_size.x().is_infinite() || max_size.x() / max_size.y() > size.x() / size.y() {
vec2f(size.x() * max_size.y() / size.y(), max_size.y())
} else {
vec2f(max_size.x(), size.y() * max_size.x() / size.x())
}
}
+17 -6
View File
@@ -13,7 +13,7 @@ use crate::{
Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
#[derive(Clone, Debug, Default, Deserialize)]
#[derive(Clone, Copy, Debug, Default, Deserialize)]
pub struct ContainerStyle {
#[serde(default)]
pub margin: Margin,
@@ -42,8 +42,8 @@ impl Container {
}
}
pub fn with_style(mut self, style: &ContainerStyle) -> Self {
self.style = style.clone();
pub fn with_style(mut self, style: ContainerStyle) -> Self {
self.style = style;
self
}
@@ -242,7 +242,7 @@ impl ToJson for ContainerStyle {
}
}
#[derive(Clone, Debug, Default)]
#[derive(Clone, Copy, Debug, Default)]
pub struct Margin {
pub top: f32,
pub left: f32,
@@ -269,7 +269,7 @@ impl ToJson for Margin {
}
}
#[derive(Clone, Debug, Default)]
#[derive(Clone, Copy, Debug, Default)]
pub struct Padding {
pub top: f32,
pub left: f32,
@@ -348,6 +348,17 @@ enum Spacing {
},
}
impl Padding {
pub fn uniform(padding: f32) -> Self {
Self {
top: padding,
left: padding,
bottom: padding,
right: padding,
}
}
}
impl ToJson for Padding {
fn to_json(&self) -> serde_json::Value {
let mut value = json!({});
@@ -367,7 +378,7 @@ impl ToJson for Padding {
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[derive(Clone, Copy, Debug, Default, Deserialize)]
pub struct Shadow {
#[serde(default, deserialize_with = "deserialize_vec2f")]
offset: Vector2F,
+90
View File
@@ -0,0 +1,90 @@
use super::constrain_size_preserving_aspect_ratio;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::{json, ToJson},
scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext,
PaintContext, SizeConstraint,
};
use serde::Deserialize;
use std::sync::Arc;
pub struct Image {
data: Arc<ImageData>,
style: ImageStyle,
}
#[derive(Copy, Clone, Default, Deserialize)]
pub struct ImageStyle {
#[serde(default)]
border: Border,
#[serde(default)]
corner_radius: f32,
}
impl Image {
pub fn new(data: Arc<ImageData>) -> Self {
Self {
data,
style: Default::default(),
}
}
pub fn with_style(mut self, style: ImageStyle) -> Self {
self.style = style;
self
}
}
impl Element for Image {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
_: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let size =
constrain_size_preserving_aspect_ratio(constraint.max, self.data.size().to_f32());
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
_: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
cx.scene.push_image(scene::Image {
bounds,
border: self.style.border,
corner_radius: self.style.corner_radius,
data: self.data.clone(),
});
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
) -> bool {
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> serde_json::Value {
json!({
"type": "Image",
"bounds": bounds.to_json(),
})
}
}
+2 -2
View File
@@ -603,7 +603,7 @@ mod tests {
offset_in_item: 0.,
},
40.,
vec2f(0., 54.),
vec2f(0., -54.),
true,
&mut presenter.build_event_context(cx),
);
@@ -654,7 +654,7 @@ mod tests {
assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.);
}
#[crate::test(self, iterations = 10000, seed = 0)]
#[crate::test(self, iterations = 10, seed = 0)]
fn test_random(cx: &mut crate::MutableAppContext, mut rng: StdRng) {
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+2 -1
View File
@@ -116,7 +116,8 @@ impl Element for MouseEventHandler {
let hit_bounds = RectF::from_points(
bounds.origin() - vec2f(self.padding.left, self.padding.top),
bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
);
)
.round_out();
self.state.update(cx, |state, cx| match event {
Event::MouseMoved {
+9 -17
View File
@@ -41,25 +41,15 @@ impl Element for Svg {
) -> (Vector2F, Self::LayoutState) {
match cx.asset_cache.svg(&self.path) {
Ok(tree) => {
let size = if constraint.max.x().is_infinite() && constraint.max.y().is_infinite() {
let rect = from_usvg_rect(tree.svg_node().view_box.rect);
rect.size()
} else {
let max_size = constraint.max;
let svg_size = from_usvg_rect(tree.svg_node().view_box.rect).size();
if max_size.x().is_infinite()
|| max_size.x() / max_size.y() > svg_size.x() / svg_size.y()
{
vec2f(svg_size.x() * max_size.y() / svg_size.y(), max_size.y())
} else {
vec2f(max_size.x(), svg_size.y() * max_size.x() / svg_size.x())
}
};
let size = constrain_size_preserving_aspect_ratio(
constraint.max,
from_usvg_rect(tree.svg_node().view_box.rect).size(),
);
(size, Some(tree))
}
Err(error) => {
log::error!("{}", error);
Err(_error) => {
#[cfg(not(any(test, feature = "test-support")))]
log::error!("{}", _error);
(constraint.min, None)
}
}
@@ -111,6 +101,8 @@ impl Element for Svg {
use crate::json::ToJson;
use super::constrain_size_preserving_aspect_ratio;
fn from_usvg_rect(rect: usvg::Rect) -> RectF {
RectF::new(
vec2f(rect.x() as f32, rect.y() as f32),
+13 -13
View File
@@ -17,7 +17,7 @@ use std::{
pub struct FamilyId(usize);
struct Family {
name: String,
name: Arc<str>,
font_ids: Vec<FontId>,
}
@@ -49,7 +49,7 @@ impl FontCache {
}))
}
pub fn family_name(&self, family_id: FamilyId) -> Result<String> {
pub fn family_name(&self, family_id: FamilyId) -> Result<Arc<str>> {
self.0
.read()
.families
@@ -62,7 +62,7 @@ impl FontCache {
for name in names {
let state = self.0.upgradable_read();
if let Some(ix) = state.families.iter().position(|f| f.name == *name) {
if let Some(ix) = state.families.iter().position(|f| f.name.as_ref() == *name) {
return Ok(FamilyId(ix));
}
@@ -81,7 +81,7 @@ impl FontCache {
}
state.families.push(Family {
name: String::from(*name),
name: Arc::from(*name),
font_ids,
});
return Ok(family_id);
@@ -141,8 +141,8 @@ impl FontCache {
pub fn bounding_box(&self, font_id: FontId, font_size: f32) -> Vector2F {
let bounding_box = self.metric(font_id, |m| m.bounding_box);
let width = self.scale_metric(bounding_box.width(), font_id, font_size);
let height = self.scale_metric(bounding_box.height(), font_id, font_size);
let width = bounding_box.width() * self.em_scale(font_id, font_size);
let height = bounding_box.height() * self.em_scale(font_id, font_size);
vec2f(width, height)
}
@@ -154,28 +154,28 @@ impl FontCache {
glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap();
}
self.scale_metric(bounds.width(), font_id, font_size)
bounds.width() * self.em_scale(font_id, font_size)
}
pub fn line_height(&self, font_id: FontId, font_size: f32) -> f32 {
let height = self.metric(font_id, |m| m.bounding_box.height());
self.scale_metric(height, font_id, font_size)
(height * self.em_scale(font_id, font_size)).ceil()
}
pub fn cap_height(&self, font_id: FontId, font_size: f32) -> f32 {
self.scale_metric(self.metric(font_id, |m| m.cap_height), font_id, font_size)
self.metric(font_id, |m| m.cap_height) * self.em_scale(font_id, font_size)
}
pub fn ascent(&self, font_id: FontId, font_size: f32) -> f32 {
self.scale_metric(self.metric(font_id, |m| m.ascent), font_id, font_size)
self.metric(font_id, |m| m.ascent) * self.em_scale(font_id, font_size)
}
pub fn descent(&self, font_id: FontId, font_size: f32) -> f32 {
self.scale_metric(self.metric(font_id, |m| -m.descent), font_id, font_size)
self.metric(font_id, |m| -m.descent) * self.em_scale(font_id, font_size)
}
pub fn scale_metric(&self, metric: f32, font_id: FontId, font_size: f32) -> f32 {
metric * font_size / self.metric(font_id, |m| m.units_per_em as f32)
pub fn em_scale(&self, font_id: FontId, font_size: f32) -> f32 {
font_size / self.metric(font_id, |m| m.units_per_em as f32)
}
pub fn line_wrapper(self: &Arc<Self>, font_id: FontId, font_size: f32) -> LineWrapperHandle {
+31 -2
View File
@@ -1,5 +1,6 @@
use crate::{
color::Color,
font_cache::FamilyId,
json::{json, ToJson},
text_layout::RunStyle,
FontCache,
@@ -22,6 +23,7 @@ pub type GlyphId = u32;
pub struct TextStyle {
pub color: Color,
pub font_family_name: Arc<str>,
pub font_family_id: FamilyId,
pub font_id: FontId,
pub font_size: f32,
pub font_properties: Properties,
@@ -85,11 +87,12 @@ impl TextStyle {
font_cache: &FontCache,
) -> anyhow::Result<Self> {
let font_family_name = font_family_name.into();
let family_id = font_cache.load_family(&[&font_family_name])?;
let font_id = font_cache.select_font(family_id, &font_properties)?;
let font_family_id = font_cache.load_family(&[&font_family_name])?;
let font_id = font_cache.select_font(font_family_id, &font_properties)?;
Ok(Self {
color,
font_family_name,
font_family_id,
font_id,
font_size,
font_properties,
@@ -124,6 +127,32 @@ impl TextStyle {
}
})
}
pub fn line_height(&self, font_cache: &FontCache) -> f32 {
font_cache.line_height(self.font_id, self.font_size)
}
pub fn em_width(&self, font_cache: &FontCache) -> f32 {
font_cache.em_width(self.font_id, self.font_size)
}
pub fn descent(&self, font_cache: &FontCache) -> f32 {
font_cache.metric(self.font_id, |m| m.descent) * self.em_scale(font_cache)
}
fn em_scale(&self, font_cache: &FontCache) -> f32 {
font_cache.em_scale(self.font_id, self.font_size)
}
}
impl From<TextStyle> for HighlightStyle {
fn from(other: TextStyle) -> Self {
Self {
color: other.color,
font_properties: other.font_properties,
underline: other.underline,
}
}
}
impl HighlightStyle {
+43
View File
@@ -0,0 +1,43 @@
use crate::geometry::vector::{vec2i, Vector2I};
use image::{Bgra, ImageBuffer};
use std::{
fmt,
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
},
};
pub struct ImageData {
pub id: usize,
data: ImageBuffer<Bgra<u8>, Vec<u8>>,
}
impl ImageData {
pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Arc<Self> {
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
Arc::new(Self {
id: NEXT_ID.fetch_add(1, SeqCst),
data,
})
}
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
pub fn size(&self) -> Vector2I {
let (width, height) = self.data.dimensions();
vec2i(width as i32, height as i32)
}
}
impl fmt::Debug for ImageData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ImageData")
.field("id", &self.id)
.field("size", &self.data.dimensions())
.finish()
}
}
+2
View File
@@ -7,6 +7,8 @@ mod test;
pub use assets::*;
pub mod elements;
pub mod font_cache;
mod image_data;
pub use crate::image_data::ImageData;
pub mod views;
pub use font_cache::FontCache;
mod clipboard;
+1
View File
@@ -48,6 +48,7 @@ pub trait Platform: Send + Sync {
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>;
fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>>;
fn delete_credentials(&self, url: &str) -> Result<()>;
fn set_cursor_style(&self, style: CursorStyle);
+1
View File
@@ -3,6 +3,7 @@ mod dispatcher;
mod event;
mod fonts;
mod geometry;
mod image_cache;
mod platform;
mod renderer;
mod sprite_cache;
+72 -12
View File
@@ -1,4 +1,7 @@
use crate::geometry::vector::{vec2i, Vector2I};
use crate::geometry::{
rect::RectI,
vector::{vec2i, Vector2I},
};
use etagere::BucketedAtlasAllocator;
use foreign_types::ForeignType;
use metal::{self, Device, TextureDescriptor};
@@ -11,6 +14,12 @@ pub struct AtlasAllocator {
free_atlases: Vec<Atlas>,
}
#[derive(Copy, Clone)]
pub struct AllocId {
pub atlas_id: usize,
alloc_id: etagere::AllocId,
}
impl AtlasAllocator {
pub fn new(device: Device, texture_descriptor: TextureDescriptor) -> Self {
let mut me = Self {
@@ -31,20 +40,40 @@ impl AtlasAllocator {
)
}
pub fn allocate(&mut self, requested_size: Vector2I) -> anyhow::Result<(usize, Vector2I)> {
let origin = self
pub fn allocate(&mut self, requested_size: Vector2I) -> (AllocId, Vector2I) {
let (alloc_id, origin) = self
.atlases
.last_mut()
.unwrap()
.allocate(requested_size)
.unwrap_or_else(|| {
let mut atlas = self.new_atlas(requested_size);
let origin = atlas.allocate(requested_size).unwrap();
let (id, origin) = atlas.allocate(requested_size).unwrap();
self.atlases.push(atlas);
origin
(id, origin)
});
Ok((self.atlases.len() - 1, origin))
let id = AllocId {
atlas_id: self.atlases.len() - 1,
alloc_id,
};
(id, origin)
}
pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> (AllocId, RectI) {
let (alloc_id, origin) = self.allocate(size);
let bounds = RectI::new(origin, size);
self.atlases[alloc_id.atlas_id].upload(bounds, bytes);
(alloc_id, bounds)
}
pub fn deallocate(&mut self, id: AllocId) {
if let Some(atlas) = self.atlases.get_mut(id.atlas_id) {
atlas.deallocate(id.alloc_id);
if atlas.is_empty() {
self.free_atlases.push(self.atlases.remove(id.atlas_id));
}
}
}
pub fn clear(&mut self) {
@@ -102,13 +131,44 @@ impl Atlas {
vec2i(size.width, size.height)
}
fn allocate(&mut self, size: Vector2I) -> Option<Vector2I> {
let origin = self
fn allocate(&mut self, size: Vector2I) -> Option<(etagere::AllocId, Vector2I)> {
let alloc = self
.allocator
.allocate(etagere::Size::new(size.x(), size.y()))?
.rectangle
.min;
Some(vec2i(origin.x, origin.y))
.allocate(etagere::Size::new(size.x(), size.y()))?;
let origin = alloc.rectangle.min;
Some((alloc.id, vec2i(origin.x, origin.y)))
}
fn upload(&mut self, bounds: RectI, bytes: &[u8]) {
let region = metal::MTLRegion::new_2d(
bounds.origin().x() as u64,
bounds.origin().y() as u64,
bounds.size().x() as u64,
bounds.size().y() as u64,
);
self.texture.replace_region(
region,
0,
bytes.as_ptr() as *const _,
(bounds.size().x() * self.bytes_per_pixel() as i32) as u64,
);
}
fn bytes_per_pixel(&self) -> u8 {
use metal::MTLPixelFormat::*;
match self.texture.pixel_format() {
A8Unorm | R8Unorm => 1,
RGBA8Unorm | BGRA8Unorm => 4,
_ => unimplemented!(),
}
}
fn deallocate(&mut self, id: etagere::AllocId) {
self.allocator.deallocate(id);
}
fn is_empty(&self) -> bool {
self.allocator.is_empty()
}
fn clear(&mut self) {
+49
View File
@@ -0,0 +1,49 @@
use metal::{MTLPixelFormat, TextureDescriptor, TextureRef};
use super::atlas::{AllocId, AtlasAllocator};
use crate::{
geometry::{rect::RectI, vector::Vector2I},
ImageData,
};
use std::{collections::HashMap, mem};
pub struct ImageCache {
prev_frame: HashMap<usize, (AllocId, RectI)>,
curr_frame: HashMap<usize, (AllocId, RectI)>,
atlases: AtlasAllocator,
}
impl ImageCache {
pub fn new(device: metal::Device, size: Vector2I) -> Self {
let descriptor = TextureDescriptor::new();
descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
descriptor.set_width(size.x() as u64);
descriptor.set_height(size.y() as u64);
Self {
prev_frame: Default::default(),
curr_frame: Default::default(),
atlases: AtlasAllocator::new(device, descriptor),
}
}
pub fn render(&mut self, image: &ImageData) -> (AllocId, RectI) {
let (alloc_id, atlas_bounds) = self
.prev_frame
.remove(&image.id)
.or_else(|| self.curr_frame.get(&image.id).copied())
.unwrap_or_else(|| self.atlases.upload(image.size(), image.as_bytes()));
self.curr_frame.insert(image.id, (alloc_id, atlas_bounds));
(alloc_id, atlas_bounds)
}
pub fn finish_frame(&mut self) {
mem::swap(&mut self.prev_frame, &mut self.curr_frame);
for (_, (id, _)) in self.curr_frame.drain() {
self.atlases.deallocate(id);
}
}
pub fn atlas_texture(&self, atlas_id: usize) -> Option<&TextureRef> {
self.atlases.texture(atlas_id)
}
}
+20
View File
@@ -551,6 +551,25 @@ impl platform::Platform for MacPlatform {
}
}
fn delete_credentials(&self, url: &str) -> Result<()> {
let url = CFString::from(url);
unsafe {
use security::*;
let mut query_attrs = CFMutableDictionary::with_capacity(2);
query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
let status = SecItemDelete(query_attrs.as_concrete_TypeRef());
if status != errSecSuccess {
return Err(anyhow!("delete password failed: {}", status));
}
}
Ok(())
}
fn set_cursor_style(&self, style: CursorStyle) {
unsafe {
let cursor: id = match style {
@@ -676,6 +695,7 @@ mod security {
pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus;
pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus;
pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
}
+134 -24
View File
@@ -1,4 +1,4 @@
use super::{atlas::AtlasAllocator, sprite_cache::SpriteCache};
use super::{atlas::AtlasAllocator, image_cache::ImageCache, sprite_cache::SpriteCache};
use crate::{
color::Color,
geometry::{
@@ -6,8 +6,7 @@ use crate::{
vector::{vec2f, vec2i, Vector2F},
},
platform,
scene::{Glyph, Icon, Layer, Quad, Shadow},
Scene,
scene::{Glyph, Icon, Image, Layer, Quad, Scene, Shadow},
};
use cocoa::foundation::NSUInteger;
use metal::{MTLPixelFormat, MTLResourceOptions, NSRange};
@@ -20,10 +19,12 @@ const INSTANCE_BUFFER_SIZE: usize = 1024 * 1024; // This is an arbitrary decisio
pub struct Renderer {
sprite_cache: SpriteCache,
image_cache: ImageCache,
path_atlases: AtlasAllocator,
quad_pipeline_state: metal::RenderPipelineState,
shadow_pipeline_state: metal::RenderPipelineState,
sprite_pipeline_state: metal::RenderPipelineState,
image_pipeline_state: metal::RenderPipelineState,
path_atlas_pipeline_state: metal::RenderPipelineState,
unit_vertices: metal::Buffer,
instances: metal::Buffer,
@@ -64,7 +65,9 @@ impl Renderer {
);
let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), fonts);
let path_atlases = build_path_atlas_allocator(MTLPixelFormat::R8Unorm, &device);
let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768));
let path_atlases =
AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor());
let quad_pipeline_state = build_pipeline_state(
&device,
&library,
@@ -89,6 +92,14 @@ impl Renderer {
"sprite_fragment",
pixel_format,
);
let image_pipeline_state = build_pipeline_state(
&device,
&library,
"image",
"image_vertex",
"image_fragment",
pixel_format,
);
let path_atlas_pipeline_state = build_path_atlas_pipeline_state(
&device,
&library,
@@ -99,10 +110,12 @@ impl Renderer {
);
Self {
sprite_cache,
image_cache,
path_atlases,
quad_pipeline_state,
shadow_pipeline_state,
sprite_pipeline_state,
image_pipeline_state,
path_atlas_pipeline_state,
unit_vertices,
instances,
@@ -117,6 +130,7 @@ impl Renderer {
output: &metal::TextureRef,
) {
let mut offset = 0;
let path_sprites = self.render_path_atlases(scene, &mut offset, command_buffer);
self.render_layers(
scene,
@@ -130,6 +144,7 @@ impl Renderer {
location: 0,
length: offset as NSUInteger,
});
self.image_cache.finish_frame();
}
fn render_path_atlases(
@@ -146,11 +161,11 @@ impl Renderer {
for path in layer.paths() {
let origin = path.bounds.origin() * scene.scale_factor();
let size = (path.bounds.size() * scene.scale_factor()).ceil();
let (atlas_id, atlas_origin) = self.path_atlases.allocate(size.to_i32()).unwrap();
let (alloc_id, atlas_origin) = self.path_atlases.allocate(size.to_i32());
let atlas_origin = atlas_origin.to_f32();
sprites.push(PathSprite {
layer_id,
atlas_id,
atlas_id: alloc_id.atlas_id,
shader_data: shaders::GPUISprite {
origin: origin.floor().to_float2(),
target_size: size.to_float2(),
@@ -162,7 +177,7 @@ impl Renderer {
});
if let Some(current_atlas_id) = current_atlas_id {
if atlas_id != current_atlas_id {
if alloc_id.atlas_id != current_atlas_id {
self.render_paths_to_atlas(
offset,
&vertices,
@@ -173,7 +188,7 @@ impl Renderer {
}
}
current_atlas_id = Some(atlas_id);
current_atlas_id = Some(alloc_id.atlas_id);
for vertex in &path.vertices {
let xy_position =
@@ -316,6 +331,13 @@ impl Renderer {
drawable_size,
command_encoder,
);
self.render_images(
layer.images(),
scale_factor,
offset,
drawable_size,
command_encoder,
);
self.render_quads(
layer.underlines(),
scale_factor,
@@ -559,11 +581,6 @@ impl Renderer {
mem::size_of::<shaders::vector_float2>() as u64,
[drawable_size.to_float2()].as_ptr() as *const c_void,
);
command_encoder.set_vertex_bytes(
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexAtlasSize as u64,
mem::size_of::<shaders::vector_float2>() as u64,
[self.sprite_cache.atlas_size().to_float2()].as_ptr() as *const c_void,
);
for (atlas_id, sprites) in sprites_by_atlas {
align_offset(offset);
@@ -573,13 +590,19 @@ impl Renderer {
"instance buffer exhausted"
);
let texture = self.sprite_cache.atlas_texture(atlas_id).unwrap();
command_encoder.set_vertex_buffer(
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexSprites as u64,
Some(&self.instances),
*offset as u64,
);
command_encoder.set_vertex_bytes(
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexAtlasSize as u64,
mem::size_of::<shaders::vector_float2>() as u64,
[vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
as *const c_void,
);
let texture = self.sprite_cache.atlas_texture(atlas_id).unwrap();
command_encoder.set_fragment_texture(
shaders::GPUISpriteFragmentInputIndex_GPUISpriteFragmentInputIndexAtlas as u64,
Some(texture),
@@ -602,6 +625,96 @@ impl Renderer {
}
}
fn render_images(
&mut self,
images: &[Image],
scale_factor: f32,
offset: &mut usize,
drawable_size: Vector2F,
command_encoder: &metal::RenderCommandEncoderRef,
) {
if images.is_empty() {
return;
}
let mut images_by_atlas = HashMap::new();
for image in images {
let origin = image.bounds.origin() * scale_factor;
let target_size = image.bounds.size() * scale_factor;
let corner_radius = image.corner_radius * scale_factor;
let border_width = image.border.width * scale_factor;
let (alloc_id, atlas_bounds) = self.image_cache.render(&image.data);
images_by_atlas
.entry(alloc_id.atlas_id)
.or_insert_with(Vec::new)
.push(shaders::GPUIImage {
origin: origin.to_float2(),
target_size: target_size.to_float2(),
source_size: atlas_bounds.size().to_float2(),
atlas_origin: atlas_bounds.origin().to_float2(),
border_top: border_width * (image.border.top as usize as f32),
border_right: border_width * (image.border.right as usize as f32),
border_bottom: border_width * (image.border.bottom as usize as f32),
border_left: border_width * (image.border.left as usize as f32),
border_color: image.border.color.to_uchar4(),
corner_radius,
});
}
command_encoder.set_render_pipeline_state(&self.image_pipeline_state);
command_encoder.set_vertex_buffer(
shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexVertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_bytes(
shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexViewportSize as u64,
mem::size_of::<shaders::vector_float2>() as u64,
[drawable_size.to_float2()].as_ptr() as *const c_void,
);
for (atlas_id, images) in images_by_atlas {
align_offset(offset);
let next_offset = *offset + images.len() * mem::size_of::<shaders::GPUIImage>();
assert!(
next_offset <= INSTANCE_BUFFER_SIZE,
"instance buffer exhausted"
);
let texture = self.image_cache.atlas_texture(atlas_id).unwrap();
command_encoder.set_vertex_buffer(
shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexImages as u64,
Some(&self.instances),
*offset as u64,
);
command_encoder.set_vertex_bytes(
shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexAtlasSize as u64,
mem::size_of::<shaders::vector_float2>() as u64,
[vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
as *const c_void,
);
command_encoder.set_fragment_texture(
shaders::GPUIImageFragmentInputIndex_GPUIImageFragmentInputIndexAtlas as u64,
Some(texture),
);
unsafe {
let buffer_contents = (self.instances.contents() as *mut u8)
.offset(*offset as isize)
as *mut shaders::GPUIImage;
std::ptr::copy_nonoverlapping(images.as_ptr(), buffer_contents, images.len());
}
command_encoder.draw_primitives_instanced(
metal::MTLPrimitiveType::Triangle,
0,
6,
images.len() as u64,
);
*offset = next_offset;
}
}
fn render_path_sprites(
&mut self,
layer_id: usize,
@@ -708,19 +821,15 @@ impl Renderer {
}
}
fn build_path_atlas_allocator(
pixel_format: MTLPixelFormat,
device: &metal::Device,
) -> AtlasAllocator {
fn build_path_atlas_texture_descriptor() -> metal::TextureDescriptor {
let texture_descriptor = metal::TextureDescriptor::new();
texture_descriptor.set_width(2048);
texture_descriptor.set_height(2048);
texture_descriptor.set_pixel_format(pixel_format);
texture_descriptor.set_pixel_format(MTLPixelFormat::R8Unorm);
texture_descriptor
.set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead);
texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
let path_atlases = AtlasAllocator::new(device.clone(), texture_descriptor);
path_atlases
texture_descriptor
}
fn align_offset(offset: &mut usize) {
@@ -803,9 +912,10 @@ mod shaders {
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
use pathfinder_geometry::vector::Vector2I;
use crate::{color::Color, geometry::vector::Vector2F};
use crate::{
color::Color,
geometry::vector::{Vector2F, Vector2I},
};
use std::mem;
include!(concat!(env!("OUT_DIR"), "/shaders.rs"));
+47 -10
View File
@@ -1,16 +1,19 @@
#include <simd/simd.h>
typedef struct {
typedef struct
{
vector_float2 viewport_size;
} GPUIUniforms;
typedef enum {
typedef enum
{
GPUIQuadInputIndexVertices = 0,
GPUIQuadInputIndexQuads = 1,
GPUIQuadInputIndexUniforms = 2,
} GPUIQuadInputIndex;
typedef struct {
typedef struct
{
vector_float2 origin;
vector_float2 size;
vector_uchar4 background_color;
@@ -22,13 +25,15 @@ typedef struct {
float corner_radius;
} GPUIQuad;
typedef enum {
typedef enum
{
GPUIShadowInputIndexVertices = 0,
GPUIShadowInputIndexShadows = 1,
GPUIShadowInputIndexUniforms = 2,
} GPUIShadowInputIndex;
typedef struct {
typedef struct
{
vector_float2 origin;
vector_float2 size;
float corner_radius;
@@ -36,18 +41,21 @@ typedef struct {
vector_uchar4 color;
} GPUIShadow;
typedef enum {
typedef enum
{
GPUISpriteVertexInputIndexVertices = 0,
GPUISpriteVertexInputIndexSprites = 1,
GPUISpriteVertexInputIndexViewportSize = 2,
GPUISpriteVertexInputIndexAtlasSize = 3,
} GPUISpriteVertexInputIndex;
typedef enum {
typedef enum
{
GPUISpriteFragmentInputIndexAtlas = 0,
} GPUISpriteFragmentInputIndex;
typedef struct {
typedef struct
{
vector_float2 origin;
vector_float2 target_size;
vector_float2 source_size;
@@ -56,14 +64,43 @@ typedef struct {
uint8_t compute_winding;
} GPUISprite;
typedef enum {
typedef enum
{
GPUIPathAtlasVertexInputIndexVertices = 0,
GPUIPathAtlasVertexInputIndexAtlasSize = 1,
} GPUIPathAtlasVertexInputIndex;
typedef struct {
typedef struct
{
vector_float2 xy_position;
vector_float2 st_position;
vector_float2 clip_rect_origin;
vector_float2 clip_rect_size;
} GPUIPathVertex;
typedef enum
{
GPUIImageVertexInputIndexVertices = 0,
GPUIImageVertexInputIndexImages = 1,
GPUIImageVertexInputIndexViewportSize = 2,
GPUIImageVertexInputIndexAtlasSize = 3,
} GPUIImageVertexInputIndex;
typedef enum
{
GPUIImageFragmentInputIndexAtlas = 0,
} GPUIImageFragmentInputIndex;
typedef struct
{
vector_float2 origin;
vector_float2 target_size;
vector_float2 source_size;
vector_float2 atlas_origin;
float border_top;
float border_right;
float border_bottom;
float border_left;
vector_uchar4 border_color;
float corner_radius;
} GPUIImage;
+81 -36
View File
@@ -34,46 +34,19 @@ float blur_along_x(float x, float y, float sigma, float corner, float2 halfSize)
struct QuadFragmentInput {
float4 position [[position]];
vector_float2 origin;
vector_float2 size;
vector_uchar4 background_color;
float2 atlas_position; // only used in the image shader
float2 origin;
float2 size;
float4 background_color;
float border_top;
float border_right;
float border_bottom;
float border_left;
vector_uchar4 border_color;
float4 border_color;
float corner_radius;
};
vertex QuadFragmentInput quad_vertex(
uint unit_vertex_id [[vertex_id]],
uint quad_id [[instance_id]],
constant float2 *unit_vertices [[buffer(GPUIQuadInputIndexVertices)]],
constant GPUIQuad *quads [[buffer(GPUIQuadInputIndexQuads)]],
constant GPUIUniforms *uniforms [[buffer(GPUIQuadInputIndexUniforms)]]
) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
GPUIQuad quad = quads[quad_id];
float2 position = unit_vertex * quad.size + quad.origin;
float4 device_position = to_device_position(position, uniforms->viewport_size);
return QuadFragmentInput {
device_position,
quad.origin,
quad.size,
quad.background_color,
quad.border_top,
quad.border_right,
quad.border_bottom,
quad.border_left,
quad.border_color,
quad.corner_radius,
};
}
fragment float4 quad_fragment(
QuadFragmentInput input [[stage_in]]
) {
float4 quad_sdf(QuadFragmentInput input) {
float2 half_size = input.size / 2.;
float2 center = input.origin + half_size;
float2 center_to_point = input.position.xy - center;
@@ -95,12 +68,13 @@ fragment float4 quad_fragment(
float4 color;
if (border_width == 0.) {
color = coloru_to_colorf(input.background_color);
color = input.background_color;
} else {
float4 border_color = float4(mix(float3(input.background_color), float3(input.border_color), input.border_color.a), 1.);
float inset_distance = distance + border_width;
color = mix(
coloru_to_colorf(input.border_color),
coloru_to_colorf(input.background_color),
border_color,
input.background_color,
saturate(0.5 - inset_distance)
);
}
@@ -109,6 +83,39 @@ fragment float4 quad_fragment(
return coverage * color;
}
vertex QuadFragmentInput quad_vertex(
uint unit_vertex_id [[vertex_id]],
uint quad_id [[instance_id]],
constant float2 *unit_vertices [[buffer(GPUIQuadInputIndexVertices)]],
constant GPUIQuad *quads [[buffer(GPUIQuadInputIndexQuads)]],
constant GPUIUniforms *uniforms [[buffer(GPUIQuadInputIndexUniforms)]]
) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
GPUIQuad quad = quads[quad_id];
float2 position = unit_vertex * quad.size + quad.origin;
float4 device_position = to_device_position(position, uniforms->viewport_size);
return QuadFragmentInput {
device_position,
float2(0., 0.),
quad.origin,
quad.size,
coloru_to_colorf(quad.background_color),
quad.border_top,
quad.border_right,
quad.border_bottom,
quad.border_left,
coloru_to_colorf(quad.border_color),
quad.corner_radius,
};
}
fragment float4 quad_fragment(
QuadFragmentInput input [[stage_in]]
) {
return quad_sdf(input);
}
struct ShadowFragmentInput {
float4 position [[position]];
vector_float2 origin;
@@ -217,6 +224,44 @@ fragment float4 sprite_fragment(
return color;
}
vertex QuadFragmentInput image_vertex(
uint unit_vertex_id [[vertex_id]],
uint image_id [[instance_id]],
constant float2 *unit_vertices [[buffer(GPUIImageVertexInputIndexVertices)]],
constant GPUIImage *images [[buffer(GPUIImageVertexInputIndexImages)]],
constant float2 *viewport_size [[buffer(GPUIImageVertexInputIndexViewportSize)]],
constant float2 *atlas_size [[buffer(GPUIImageVertexInputIndexAtlasSize)]]
) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
GPUIImage image = images[image_id];
float2 position = unit_vertex * image.target_size + image.origin;
float4 device_position = to_device_position(position, *viewport_size);
float2 atlas_position = (unit_vertex * image.source_size + image.atlas_origin) / *atlas_size;
return QuadFragmentInput {
device_position,
atlas_position,
image.origin,
image.target_size,
float4(0.),
image.border_top,
image.border_right,
image.border_bottom,
image.border_left,
coloru_to_colorf(image.border_color),
image.corner_radius,
};
}
fragment float4 image_fragment(
QuadFragmentInput input [[stage_in]],
texture2d<float> atlas [[ texture(GPUIImageFragmentInputIndexAtlas) ]]
) {
constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
input.background_color = atlas.sample(atlas_sampler, input.atlas_position);
return quad_sdf(input);
}
struct PathAtlasVertexOutput {
float4 position [[position]];
float2 st_position;
+13 -88
View File
@@ -1,12 +1,9 @@
use super::atlas::AtlasAllocator;
use crate::{
fonts::{FontId, GlyphId},
geometry::{
rect::RectI,
vector::{vec2f, vec2i, Vector2F, Vector2I},
},
geometry::vector::{vec2f, Vector2F, Vector2I},
platform,
};
use etagere::BucketedAtlasAllocator;
use metal::{MTLPixelFormat, TextureDescriptor};
use ordered_float::OrderedFloat;
use std::{borrow::Cow, collections::HashMap, sync::Arc};
@@ -42,10 +39,8 @@ pub struct IconSprite {
}
pub struct SpriteCache {
device: metal::Device,
atlas_size: Vector2I,
fonts: Arc<dyn platform::FontSystem>,
atlases: Vec<Atlas>,
atlases: AtlasAllocator,
glyphs: HashMap<GlyphDescriptor, Option<GlyphSprite>>,
icons: HashMap<IconDescriptor, IconSprite>,
}
@@ -56,21 +51,18 @@ impl SpriteCache {
size: Vector2I,
fonts: Arc<dyn platform::FontSystem>,
) -> Self {
let atlases = vec![Atlas::new(&device, size)];
let descriptor = TextureDescriptor::new();
descriptor.set_pixel_format(MTLPixelFormat::A8Unorm);
descriptor.set_width(size.x() as u64);
descriptor.set_height(size.y() as u64);
Self {
device,
atlas_size: size,
fonts,
atlases,
atlases: AtlasAllocator::new(device, descriptor),
glyphs: Default::default(),
icons: Default::default(),
}
}
pub fn atlas_size(&self) -> Vector2I {
self.atlas_size
}
pub fn render_glyph(
&mut self,
font_id: FontId,
@@ -84,8 +76,6 @@ impl SpriteCache {
let target_position = target_position * scale_factor;
let fonts = &self.fonts;
let atlases = &mut self.atlases;
let atlas_size = self.atlas_size;
let device = &self.device;
let subpixel_variant = (
(target_position.x().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
% SUBPIXEL_VARIANTS,
@@ -111,22 +101,10 @@ impl SpriteCache {
subpixel_shift,
scale_factor,
)?;
assert!(glyph_bounds.width() < atlas_size.x());
assert!(glyph_bounds.height() < atlas_size.y());
let atlas_bounds = atlases
.last_mut()
.unwrap()
.try_insert(glyph_bounds.size(), &mask)
.unwrap_or_else(|| {
let mut atlas = Atlas::new(device, atlas_size);
let bounds = atlas.try_insert(glyph_bounds.size(), &mask).unwrap();
atlases.push(atlas);
bounds
});
let (alloc_id, atlas_bounds) = atlases.upload(glyph_bounds.size(), &mask);
Some(GlyphSprite {
atlas_id: atlases.len() - 1,
atlas_id: alloc_id.atlas_id,
atlas_origin: atlas_bounds.origin(),
offset: glyph_bounds.origin(),
size: glyph_bounds.size(),
@@ -142,10 +120,6 @@ impl SpriteCache {
svg: usvg::Tree,
) -> IconSprite {
let atlases = &mut self.atlases;
let atlas_size = self.atlas_size;
let device = &self.device;
assert!(size.x() < atlas_size.x());
assert!(size.y() < atlas_size.y());
self.icons
.entry(IconDescriptor {
path,
@@ -161,19 +135,9 @@ impl SpriteCache {
.map(|a| a.alpha())
.collect::<Vec<_>>();
let atlas_bounds = atlases
.last_mut()
.unwrap()
.try_insert(size, &mask)
.unwrap_or_else(|| {
let mut atlas = Atlas::new(device, atlas_size);
let bounds = atlas.try_insert(size, &mask).unwrap();
atlases.push(atlas);
bounds
});
let (alloc_id, atlas_bounds) = atlases.upload(size, &mask);
IconSprite {
atlas_id: atlases.len() - 1,
atlas_id: alloc_id.atlas_id,
atlas_origin: atlas_bounds.origin(),
size,
}
@@ -182,45 +146,6 @@ impl SpriteCache {
}
pub fn atlas_texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
self.atlases.get(atlas_id).map(|a| a.texture.as_ref())
}
}
struct Atlas {
allocator: BucketedAtlasAllocator,
texture: metal::Texture,
}
impl Atlas {
fn new(device: &metal::DeviceRef, size: Vector2I) -> Self {
let descriptor = TextureDescriptor::new();
descriptor.set_pixel_format(MTLPixelFormat::A8Unorm);
descriptor.set_width(size.x() as u64);
descriptor.set_height(size.y() as u64);
Self {
allocator: BucketedAtlasAllocator::new(etagere::Size::new(size.x(), size.y())),
texture: device.new_texture(&descriptor),
}
}
fn try_insert(&mut self, size: Vector2I, mask: &[u8]) -> Option<RectI> {
let allocation = self
.allocator
.allocate(etagere::size2(size.x() + 1, size.y() + 1))?;
let bounds = allocation.rectangle;
let region = metal::MTLRegion::new_2d(
bounds.min.x as u64,
bounds.min.y as u64,
size.x() as u64,
size.y() as u64,
);
self.texture
.replace_region(region, 0, mask.as_ptr() as *const _, size.x() as u64);
Some(RectI::from_points(
vec2i(bounds.min.x, bounds.min.y),
vec2i(bounds.max.x, bounds.max.y),
))
self.atlases.texture(atlas_id)
}
}
+4
View File
@@ -137,6 +137,10 @@ impl super::Platform for Platform {
Ok(None)
}
fn delete_credentials(&self, _: &str) -> Result<()> {
Ok(())
}
fn set_cursor_style(&self, style: CursorStyle) {
*self.cursor.lock() = style;
}
+8
View File
@@ -286,6 +286,14 @@ impl<'a> PaintContext<'a> {
}
}
impl<'a> Deref for PaintContext<'a> {
type Target = AppContext;
fn deref(&self) -> &Self::Target {
self.app
}
}
pub struct EventContext<'a> {
rendered_views: &'a mut HashMap<usize, ElementBox>,
dispatched_actions: Vec<DispatchDirective>,
+23 -1
View File
@@ -1,12 +1,13 @@
use serde::Deserialize;
use serde_json::json;
use std::borrow::Cow;
use std::{borrow::Cow, sync::Arc};
use crate::{
color::Color,
fonts::{FontId, GlyphId},
geometry::{rect::RectF, vector::Vector2F},
json::ToJson,
ImageData,
};
pub struct Scene {
@@ -25,6 +26,7 @@ pub struct Layer {
clip_bounds: Option<RectF>,
quads: Vec<Quad>,
underlines: Vec<Quad>,
images: Vec<Image>,
shadows: Vec<Shadow>,
glyphs: Vec<Glyph>,
icons: Vec<Icon>,
@@ -124,6 +126,13 @@ pub struct PathVertex {
pub st_position: Vector2F,
}
pub struct Image {
pub bounds: RectF,
pub border: Border,
pub corner_radius: f32,
pub data: Arc<ImageData>,
}
impl Scene {
pub fn new(scale_factor: f32) -> Self {
let stacking_context = StackingContext::new(None);
@@ -166,6 +175,10 @@ impl Scene {
self.active_layer().push_quad(quad)
}
pub fn push_image(&mut self, image: Image) {
self.active_layer().push_image(image)
}
pub fn push_underline(&mut self, underline: Quad) {
self.active_layer().push_underline(underline)
}
@@ -240,6 +253,7 @@ impl Layer {
clip_bounds,
quads: Vec::new(),
underlines: Vec::new(),
images: Vec::new(),
shadows: Vec::new(),
glyphs: Vec::new(),
icons: Vec::new(),
@@ -267,6 +281,14 @@ impl Layer {
self.underlines.as_slice()
}
fn push_image(&mut self, image: Image) {
self.images.push(image);
}
pub fn images(&self) -> &[Image] {
self.images.as_slice()
}
fn push_shadow(&mut self, shadow: Shadow) {
self.shadows.push(shadow);
}
+2 -2
View File
@@ -111,7 +111,7 @@ impl View for Select {
mouse_state.hovered,
cx,
))
.with_style(&style.header)
.with_style(style.header)
.boxed()
})
.on_click(move |cx| cx.dispatch_action(ToggleSelect))
@@ -158,7 +158,7 @@ impl View for Select {
.with_max_height(200.)
.boxed(),
)
.with_style(&style.menu)
.with_style(style.menu)
.boxed(),
)
.boxed(),
+4 -1
View File
@@ -5,6 +5,9 @@ edition = "2018"
name = "zed-server"
version = "0.1.0"
[[bin]]
name = "zed-server"
[[bin]]
name = "seed"
required-features = ["seed-support"]
@@ -47,7 +50,7 @@ default-features = false
[dependencies.sqlx]
version = "0.5.2"
features = ["runtime-async-std-rustls", "postgres", "time"]
features = ["runtime-async-std-rustls", "postgres", "time", "uuid"]
[dev-dependencies]
gpui = { path = "../gpui" }
@@ -0,0 +1,4 @@
ALTER TABLE "channel_messages"
ADD "nonce" UUID NOT NULL DEFAULT gen_random_uuid();
CREATE UNIQUE INDEX "index_channel_messages_nonce" ON "channel_messages" ("nonce");
+4 -2
View File
@@ -17,7 +17,7 @@ use scrypt::{
};
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, convert::TryFrom, sync::Arc};
use surf::Url;
use surf::{StatusCode, Url};
use tide::Server;
use zrpc::auth as zed_auth;
@@ -73,7 +73,9 @@ impl tide::Middleware<Arc<AppState>> for VerifyToken {
request.set_ext(user_id);
Ok(next.run(request).await)
} else {
Err(anyhow!("invalid credentials").into())
let mut response = tide::Response::new(StatusCode::Unauthorized);
response.set_body("invalid credentials");
Ok(response)
}
}
}
+1 -1
View File
@@ -73,7 +73,7 @@ async fn main() {
for timestamp in timestamps {
let sender_id = *zed_user_ids.choose(&mut rng).unwrap();
let body = lipsum::lipsum_words(rng.gen_range(1..=50));
db.create_channel_message(channel_id, sender_id, &body, timestamp)
db.create_channel_message(channel_id, sender_id, &body, timestamp, rng.gen())
.await
.expect("failed to insert message");
}
+148 -8
View File
@@ -1,7 +1,7 @@
use anyhow::Context;
use async_std::task::{block_on, yield_now};
use serde::Serialize;
use sqlx::{FromRow, Result};
use sqlx::{types::Uuid, FromRow, Result};
use time::OffsetDateTime;
pub use async_sqlx_session::PostgresSessionStore as SessionStore;
@@ -128,10 +128,23 @@ impl Db {
requester_id: UserId,
ids: impl Iterator<Item = UserId>,
) -> Result<Vec<User>> {
let mut include_requester = false;
let ids = ids
.map(|id| {
if id == requester_id {
include_requester = true;
}
id.0
})
.collect::<Vec<_>>();
test_support!(self, {
// Only return users that are in a common channel with the requesting user.
// Also allow the requesting user to return their own data, even if they aren't
// in any channels.
let query = "
SELECT users.*
SELECT
users.*
FROM
users, channel_memberships
WHERE
@@ -142,11 +155,19 @@ impl Db {
FROM channel_memberships
WHERE channel_memberships.user_id = $2
)
UNION
SELECT
users.*
FROM
users
WHERE
$3 AND users.id = $2
";
sqlx::query_as(query)
.bind(&ids.map(|id| id.0).collect::<Vec<_>>())
.bind(&ids)
.bind(requester_id)
.bind(include_requester)
.fetch_all(&self.pool)
.await
})
@@ -381,11 +402,13 @@ impl Db {
sender_id: UserId,
body: &str,
timestamp: OffsetDateTime,
nonce: u128,
) -> Result<MessageId> {
test_support!(self, {
let query = "
INSERT INTO channel_messages (channel_id, sender_id, body, sent_at)
VALUES ($1, $2, $3, $4)
INSERT INTO channel_messages (channel_id, sender_id, body, sent_at, nonce)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (nonce) DO UPDATE SET nonce = excluded.nonce
RETURNING id
";
sqlx::query_scalar(query)
@@ -393,6 +416,7 @@ impl Db {
.bind(sender_id.0)
.bind(body)
.bind(timestamp)
.bind(Uuid::from_u128(nonce))
.fetch_one(&self.pool)
.await
.map(MessageId)
@@ -409,7 +433,7 @@ impl Db {
let query = r#"
SELECT * FROM (
SELECT
id, sender_id, body, sent_at AT TIME ZONE 'UTC' as sent_at
id, sender_id, body, sent_at AT TIME ZONE 'UTC' as sent_at, nonce
FROM
channel_messages
WHERE
@@ -455,7 +479,7 @@ macro_rules! id_type {
}
id_type!(UserId);
#[derive(Debug, FromRow, Serialize)]
#[derive(Debug, FromRow, Serialize, PartialEq)]
pub struct User {
pub id: UserId,
pub github_login: String,
@@ -493,6 +517,7 @@ pub struct ChannelMessage {
pub sender_id: UserId,
pub body: String,
pub sent_at: OffsetDateTime,
pub nonce: Uuid,
}
#[cfg(test)]
@@ -563,6 +588,91 @@ pub mod tests {
}
}
#[gpui::test]
async fn test_get_users_by_ids() {
let test_db = TestDb::new();
let db = test_db.db();
let user = db.create_user("user", false).await.unwrap();
let friend1 = db.create_user("friend-1", false).await.unwrap();
let friend2 = db.create_user("friend-2", false).await.unwrap();
let friend3 = db.create_user("friend-3", false).await.unwrap();
let stranger = db.create_user("stranger", false).await.unwrap();
// A user can read their own info, even if they aren't in any channels.
assert_eq!(
db.get_users_by_ids(
user,
[user, friend1, friend2, friend3, stranger].iter().copied()
)
.await
.unwrap(),
vec![User {
id: user,
github_login: "user".to_string(),
admin: false,
},],
);
// A user can read the info of any other user who is in a shared channel
// with them.
let org = db.create_org("test org", "test-org").await.unwrap();
let chan1 = db.create_org_channel(org, "channel-1").await.unwrap();
let chan2 = db.create_org_channel(org, "channel-2").await.unwrap();
let chan3 = db.create_org_channel(org, "channel-3").await.unwrap();
db.add_channel_member(chan1, user, false).await.unwrap();
db.add_channel_member(chan2, user, false).await.unwrap();
db.add_channel_member(chan1, friend1, false).await.unwrap();
db.add_channel_member(chan1, friend2, false).await.unwrap();
db.add_channel_member(chan2, friend2, false).await.unwrap();
db.add_channel_member(chan2, friend3, false).await.unwrap();
db.add_channel_member(chan3, stranger, false).await.unwrap();
assert_eq!(
db.get_users_by_ids(
user,
[user, friend1, friend2, friend3, stranger].iter().copied()
)
.await
.unwrap(),
vec![
User {
id: user,
github_login: "user".to_string(),
admin: false,
},
User {
id: friend1,
github_login: "friend-1".to_string(),
admin: false,
},
User {
id: friend2,
github_login: "friend-2".to_string(),
admin: false,
},
User {
id: friend3,
github_login: "friend-3".to_string(),
admin: false,
}
]
);
// The user's own info is only returned if they request it.
assert_eq!(
db.get_users_by_ids(user, [friend1].iter().copied())
.await
.unwrap(),
vec![User {
id: friend1,
github_login: "friend-1".to_string(),
admin: false,
},]
)
}
#[gpui::test]
async fn test_recent_channel_messages() {
let test_db = TestDb::new();
@@ -571,7 +681,7 @@ pub mod tests {
let org = db.create_org("org", "org").await.unwrap();
let channel = db.create_org_channel(org, "channel").await.unwrap();
for i in 0..10 {
db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc())
db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc(), i)
.await
.unwrap();
}
@@ -591,4 +701,34 @@ pub mod tests {
["1", "2", "3", "4"]
);
}
#[gpui::test]
async fn test_channel_message_nonces() {
let test_db = TestDb::new();
let db = test_db.db();
let user = db.create_user("user", false).await.unwrap();
let org = db.create_org("org", "org").await.unwrap();
let channel = db.create_org_channel(org, "channel").await.unwrap();
let msg1_id = db
.create_channel_message(channel, user, "1", OffsetDateTime::now_utc(), 1)
.await
.unwrap();
let msg2_id = db
.create_channel_message(channel, user, "2", OffsetDateTime::now_utc(), 2)
.await
.unwrap();
let msg3_id = db
.create_channel_message(channel, user, "3", OffsetDateTime::now_utc(), 1)
.await
.unwrap();
let msg4_id = db
.create_channel_message(channel, user, "4", OffsetDateTime::now_utc(), 2)
.await
.unwrap();
assert_ne!(msg1_id, msg2_id);
assert_eq!(msg1_id, msg3_id);
assert_eq!(msg2_id, msg4_id);
}
}
+194 -133
View File
@@ -27,7 +27,7 @@ use time::OffsetDateTime;
use zrpc::{
auth::random_token,
proto::{self, AnyTypedEnvelope, EnvelopedMessage},
Conn, ConnectionId, Peer, TypedEnvelope,
Connection, ConnectionId, Peer, TypedEnvelope,
};
type ReplicaId = u16;
@@ -48,13 +48,13 @@ pub struct Server {
#[derive(Default)]
struct ServerState {
connections: HashMap<ConnectionId, Connection>,
connections: HashMap<ConnectionId, ConnectionState>,
pub worktrees: HashMap<u64, Worktree>,
channels: HashMap<ChannelId, Channel>,
next_worktree_id: u64,
}
struct Connection {
struct ConnectionState {
user_id: UserId,
worktrees: HashSet<u64>,
channels: HashSet<ChannelId>,
@@ -133,7 +133,7 @@ impl Server {
pub fn handle_connection(
self: &Arc<Self>,
connection: Conn,
connection: Connection,
addr: String,
user_id: UserId,
) -> impl Future<Output = ()> {
@@ -211,7 +211,7 @@ impl Server {
async fn add_connection(&self, connection_id: ConnectionId, user_id: UserId) {
self.state.write().await.connections.insert(
connection_id,
Connection {
ConnectionState {
user_id,
worktrees: Default::default(),
channels: Default::default(),
@@ -558,8 +558,8 @@ impl Server {
.into_iter()
.map(|user| proto::User {
id: user.id.to_proto(),
avatar_url: format!("https://github.com/{}.png?size=128", user.github_login),
github_login: user.github_login,
avatar_url: String::new(),
})
.collect();
self.peer
@@ -602,6 +602,7 @@ impl Server {
body: msg.body,
timestamp: msg.sent_at.unix_timestamp() as u64,
sender_id: msg.sender_id.to_proto(),
nonce: Some(msg.nonce.as_u128().into()),
})
.collect::<Vec<_>>();
self.peer
@@ -687,10 +688,24 @@ impl Server {
}
let timestamp = OffsetDateTime::now_utc();
let nonce = if let Some(nonce) = request.payload.nonce {
nonce
} else {
self.peer
.respond_with_error(
receipt,
proto::Error {
message: "nonce can't be blank".to_string(),
},
)
.await?;
return Ok(());
};
let message_id = self
.app_state
.db
.create_channel_message(channel_id, user_id, &body, timestamp)
.create_channel_message(channel_id, user_id, &body, timestamp, nonce.clone().into())
.await?
.to_proto();
let message = proto::ChannelMessage {
@@ -698,6 +713,7 @@ impl Server {
id: message_id,
body,
timestamp: timestamp.unix_timestamp() as u64,
nonce: Some(nonce),
};
broadcast(request.sender_id, connection_ids, |conn_id| {
self.peer.send(
@@ -754,6 +770,7 @@ impl Server {
body: msg.body,
timestamp: msg.sent_at.unix_timestamp() as u64,
sender_id: msg.sender_id.to_proto(),
nonce: Some(msg.nonce.as_u128().into()),
})
.collect::<Vec<_>>();
self.peer
@@ -972,7 +989,7 @@ pub fn add_routes(app: &mut tide::Server<Arc<AppState>>, rpc: &Arc<Peer>) {
let user_id = user_id.ok_or_else(|| anyhow!("user_id is not present on request. ensure auth::VerifyToken middleware is present"))?;
task::spawn(async move {
if let Some(stream) = upgrade_receiver.await {
server.handle_connection(Conn::new(WebSocketStream::from_raw_socket(stream, Role::Server, None).await), addr, user_id).await;
server.handle_connection(Connection::new(WebSocketStream::from_raw_socket(stream, Role::Server, None).await), addr, user_id).await;
}
});
@@ -1020,11 +1037,12 @@ mod tests {
};
use zed::{
channel::{Channel, ChannelDetails, ChannelList},
editor::{Editor, Insert},
editor::{Editor, EditorStyle, Insert},
fs::{FakeFs, Fs as _},
language::LanguageRegistry,
rpc::{self, Client},
rpc::{self, Client, Credentials, EstablishConnectionError},
settings,
test::FakeHttpClient,
user::UserStore,
worktree::Worktree,
};
@@ -1038,8 +1056,8 @@ mod tests {
// Connect to a server as 2 clients.
let mut server = TestServer::start().await;
let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
let (_, client_b) = server.create_client(&mut cx_b, "user_b").await;
let (client_a, _) = server.create_client(&mut cx_a, "user_a").await;
let (client_b, _) = server.create_client(&mut cx_b, "user_b").await;
cx_a.foreground().forbid_parking();
@@ -1103,7 +1121,14 @@ mod tests {
.unwrap();
// Create a selection set as client B and see that selection set as client A.
let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, settings, cx));
let editor_b = cx_b.add_view(window_b, |cx| {
Editor::for_buffer(
buffer_b,
settings,
|cx| EditorStyle::test(cx.font_cache()),
cx,
)
});
buffer_a
.condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
.await;
@@ -1123,7 +1148,7 @@ mod tests {
.await;
// Close the buffer as client A, see that the buffer is closed.
drop(buffer_a);
cx_a.update(move |_| drop(buffer_a));
worktree_a
.condition(&cx_a, |tree, cx| !tree.has_open_buffer("b.txt", cx))
.await;
@@ -1146,9 +1171,9 @@ mod tests {
// Connect to a server as 3 clients.
let mut server = TestServer::start().await;
let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
let (_, client_b) = server.create_client(&mut cx_b, "user_b").await;
let (_, client_c) = server.create_client(&mut cx_c, "user_c").await;
let (client_a, _) = server.create_client(&mut cx_a, "user_a").await;
let (client_b, _) = server.create_client(&mut cx_b, "user_b").await;
let (client_c, _) = server.create_client(&mut cx_c, "user_c").await;
let fs = Arc::new(FakeFs::new());
@@ -1287,8 +1312,8 @@ mod tests {
// Connect to a server as 2 clients.
let mut server = TestServer::start().await;
let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
let (_, client_b) = server.create_client(&mut cx_b, "user_b").await;
let (client_a, _) = server.create_client(&mut cx_a, "user_a").await;
let (client_b, _) = server.create_client(&mut cx_b, "user_b").await;
// Share a local worktree as client A
let fs = Arc::new(FakeFs::new());
@@ -1368,8 +1393,8 @@ mod tests {
// Connect to a server as 2 clients.
let mut server = TestServer::start().await;
let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
let (_, client_b) = server.create_client(&mut cx_b, "user_b").await;
let (client_a, _) = server.create_client(&mut cx_a, "user_a").await;
let (client_b, _) = server.create_client(&mut cx_b, "user_b").await;
// Share a local worktree as client A
let fs = Arc::new(FakeFs::new());
@@ -1428,8 +1453,8 @@ mod tests {
// Connect to a server as 2 clients.
let mut server = TestServer::start().await;
let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
let (_, client_b) = server.create_client(&mut cx_a, "user_b").await;
let (client_a, _) = server.create_client(&mut cx_a, "user_a").await;
let (client_b, _) = server.create_client(&mut cx_a, "user_b").await;
// Share a local worktree as client A
let fs = Arc::new(FakeFs::new());
@@ -1486,33 +1511,37 @@ mod tests {
// Connect to a server as 2 clients.
let mut server = TestServer::start().await;
let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await;
let (user_id_b, client_b) = server.create_client(&mut cx_b, "user_b").await;
let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await;
let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await;
// Create an org that includes these 2 users.
let db = &server.app_state.db;
let org_id = db.create_org("Test Org", "test-org").await.unwrap();
db.add_org_member(org_id, user_id_a, false).await.unwrap();
db.add_org_member(org_id, user_id_b, false).await.unwrap();
db.add_org_member(org_id, current_user_id(&user_store_a), false)
.await
.unwrap();
db.add_org_member(org_id, current_user_id(&user_store_b), false)
.await
.unwrap();
// Create a channel that includes all the users.
let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
db.add_channel_member(channel_id, user_id_a, false)
db.add_channel_member(channel_id, current_user_id(&user_store_a), false)
.await
.unwrap();
db.add_channel_member(channel_id, user_id_b, false)
db.add_channel_member(channel_id, current_user_id(&user_store_b), false)
.await
.unwrap();
db.create_channel_message(
channel_id,
user_id_b,
current_user_id(&user_store_b),
"hello A, it's B.",
OffsetDateTime::now_utc(),
1,
)
.await
.unwrap();
let user_store_a = Arc::new(UserStore::new(client_a.clone()));
let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx));
channels_a
.condition(&mut cx_a, |list, _| list.available_channels().is_some())
@@ -1533,11 +1562,10 @@ mod tests {
channel_a
.condition(&cx_a, |channel, _| {
channel_messages(channel)
== [("user_b".to_string(), "hello A, it's B.".to_string())]
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
})
.await;
let user_store_b = Arc::new(UserStore::new(client_b.clone()));
let channels_b = cx_b.add_model(|cx| ChannelList::new(user_store_b, client_b, cx));
channels_b
.condition(&mut cx_b, |list, _| list.available_channels().is_some())
@@ -1559,7 +1587,7 @@ mod tests {
channel_b
.condition(&cx_b, |channel, _| {
channel_messages(channel)
== [("user_b".to_string(), "hello A, it's B.".to_string())]
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
})
.await;
@@ -1571,28 +1599,25 @@ mod tests {
.detach();
let task = channel.send_message("sup".to_string(), cx).unwrap();
assert_eq!(
channel
.pending_messages()
.iter()
.map(|m| &m.body)
.collect::<Vec<_>>(),
&["oh, hi B.", "sup"]
channel_messages(channel),
&[
("user_b".to_string(), "hello A, it's B.".to_string(), false),
("user_a".to_string(), "oh, hi B.".to_string(), true),
("user_a".to_string(), "sup".to_string(), true)
]
);
task
})
.await
.unwrap();
channel_a
.condition(&cx_a, |channel, _| channel.pending_messages().is_empty())
.await;
channel_b
.condition(&cx_b, |channel, _| {
channel_messages(channel)
== [
("user_b".to_string(), "hello A, it's B.".to_string()),
("user_a".to_string(), "oh, hi B.".to_string()),
("user_a".to_string(), "sup".to_string()),
("user_b".to_string(), "hello A, it's B.".to_string(), false),
("user_a".to_string(), "oh, hi B.".to_string(), false),
("user_a".to_string(), "sup".to_string(), false),
]
})
.await;
@@ -1612,14 +1637,6 @@ mod tests {
server
.condition(|state| !state.channels.contains_key(&channel_id))
.await;
fn channel_messages(channel: &Channel) -> Vec<(String, String)> {
channel
.messages()
.cursor::<(), ()>()
.map(|m| (m.sender.github_login.clone(), m.body.clone()))
.collect()
}
}
#[gpui::test]
@@ -1627,17 +1644,18 @@ mod tests {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start().await;
let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await;
let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await;
let db = &server.app_state.db;
let org_id = db.create_org("Test Org", "test-org").await.unwrap();
let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
db.add_org_member(org_id, user_id_a, false).await.unwrap();
db.add_channel_member(channel_id, user_id_a, false)
db.add_org_member(org_id, current_user_id(&user_store_a), false)
.await
.unwrap();
db.add_channel_member(channel_id, current_user_id(&user_store_a), false)
.await
.unwrap();
let user_store_a = Arc::new(UserStore::new(client_a.clone()));
let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx));
channels_a
.condition(&mut cx_a, |list, _| list.available_channels().is_some())
@@ -1683,37 +1701,44 @@ mod tests {
#[gpui::test]
async fn test_chat_reconnection(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
cx_a.foreground().forbid_parking();
let http = FakeHttpClient::new(|_| async move { Ok(surf::http::Response::new(404)) });
// Connect to a server as 2 clients.
let mut server = TestServer::start().await;
let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await;
let (user_id_b, client_b) = server.create_client(&mut cx_b, "user_b").await;
let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await;
let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await;
let mut status_b = client_b.status();
// Create an org that includes these 2 users.
let db = &server.app_state.db;
let org_id = db.create_org("Test Org", "test-org").await.unwrap();
db.add_org_member(org_id, user_id_a, false).await.unwrap();
db.add_org_member(org_id, user_id_b, false).await.unwrap();
db.add_org_member(org_id, current_user_id(&user_store_a), false)
.await
.unwrap();
db.add_org_member(org_id, current_user_id(&user_store_b), false)
.await
.unwrap();
// Create a channel that includes all the users.
let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
db.add_channel_member(channel_id, user_id_a, false)
db.add_channel_member(channel_id, current_user_id(&user_store_a), false)
.await
.unwrap();
db.add_channel_member(channel_id, user_id_b, false)
db.add_channel_member(channel_id, current_user_id(&user_store_b), false)
.await
.unwrap();
db.create_channel_message(
channel_id,
user_id_b,
current_user_id(&user_store_b),
"hello A, it's B.",
OffsetDateTime::now_utc(),
2,
)
.await
.unwrap();
let user_store_a = Arc::new(UserStore::new(client_a.clone()));
let user_store_a =
UserStore::new(client_a.clone(), http.clone(), cx_a.background().as_ref());
let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx));
channels_a
.condition(&mut cx_a, |list, _| list.available_channels().is_some())
@@ -1735,12 +1760,11 @@ mod tests {
channel_a
.condition(&cx_a, |channel, _| {
channel_messages(channel)
== [("user_b".to_string(), "hello A, it's B.".to_string())]
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
})
.await;
let user_store_b = Arc::new(UserStore::new(client_b.clone()));
let channels_b = cx_b.add_model(|cx| ChannelList::new(user_store_b, client_b, cx));
let channels_b = cx_b.add_model(|cx| ChannelList::new(user_store_b.clone(), client_b, cx));
channels_b
.condition(&mut cx_b, |list, _| list.available_channels().is_some())
.await;
@@ -1761,13 +1785,13 @@ mod tests {
channel_b
.condition(&cx_b, |channel, _| {
channel_messages(channel)
== [("user_b".to_string(), "hello A, it's B.".to_string())]
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
})
.await;
// Disconnect client B, ensuring we can still access its cached channel data.
server.forbid_connections();
server.disconnect_client(user_id_b);
server.disconnect_client(current_user_id(&user_store_b));
while !matches!(
status_b.recv().await,
Some(rpc::Status::ReconnectionError { .. })
@@ -1785,10 +1809,28 @@ mod tests {
channel_b.read_with(&cx_b, |channel, _| {
assert_eq!(
channel_messages(channel),
[("user_b".to_string(), "hello A, it's B.".to_string())]
[("user_b".to_string(), "hello A, it's B.".to_string(), false)]
)
});
// Send a message from client B while it is disconnected.
channel_b
.update(&mut cx_b, |channel, cx| {
let task = channel
.send_message("can you see this?".to_string(), cx)
.unwrap();
assert_eq!(
channel_messages(channel),
&[
("user_b".to_string(), "hello A, it's B.".to_string(), false),
("user_b".to_string(), "can you see this?".to_string(), true)
]
);
task
})
.await
.unwrap_err();
// Send a message from client A while B is disconnected.
channel_a
.update(&mut cx_a, |channel, cx| {
@@ -1798,12 +1840,12 @@ mod tests {
.detach();
let task = channel.send_message("sup".to_string(), cx).unwrap();
assert_eq!(
channel
.pending_messages()
.iter()
.map(|m| &m.body)
.collect::<Vec<_>>(),
&["oh, hi B.", "sup"]
channel_messages(channel),
&[
("user_b".to_string(), "hello A, it's B.".to_string(), false),
("user_a".to_string(), "oh, hi B.".to_string(), true),
("user_a".to_string(), "sup".to_string(), true)
]
);
task
})
@@ -1814,14 +1856,16 @@ mod tests {
server.allow_connections();
cx_b.foreground().advance_clock(Duration::from_secs(10));
// Verify that B sees the new messages upon reconnection.
// Verify that B sees the new messages upon reconnection, as well as the message client B
// sent while offline.
channel_b
.condition(&cx_b, |channel, _| {
channel_messages(channel)
== [
("user_b".to_string(), "hello A, it's B.".to_string()),
("user_a".to_string(), "oh, hi B.".to_string()),
("user_a".to_string(), "sup".to_string()),
("user_b".to_string(), "hello A, it's B.".to_string(), false),
("user_a".to_string(), "oh, hi B.".to_string(), false),
("user_a".to_string(), "sup".to_string(), false),
("user_b".to_string(), "can you see this?".to_string(), false),
]
})
.await;
@@ -1837,10 +1881,11 @@ mod tests {
.condition(&cx_b, |channel, _| {
channel_messages(channel)
== [
("user_b".to_string(), "hello A, it's B.".to_string()),
("user_a".to_string(), "oh, hi B.".to_string()),
("user_a".to_string(), "sup".to_string()),
("user_a".to_string(), "you online?".to_string()),
("user_b".to_string(), "hello A, it's B.".to_string(), false),
("user_a".to_string(), "oh, hi B.".to_string(), false),
("user_a".to_string(), "sup".to_string(), false),
("user_b".to_string(), "can you see this?".to_string(), false),
("user_a".to_string(), "you online?".to_string(), false),
]
})
.await;
@@ -1855,22 +1900,15 @@ mod tests {
.condition(&cx_a, |channel, _| {
channel_messages(channel)
== [
("user_b".to_string(), "hello A, it's B.".to_string()),
("user_a".to_string(), "oh, hi B.".to_string()),
("user_a".to_string(), "sup".to_string()),
("user_a".to_string(), "you online?".to_string()),
("user_b".to_string(), "yep".to_string()),
("user_b".to_string(), "hello A, it's B.".to_string(), false),
("user_a".to_string(), "oh, hi B.".to_string(), false),
("user_a".to_string(), "sup".to_string(), false),
("user_b".to_string(), "can you see this?".to_string(), false),
("user_a".to_string(), "you online?".to_string(), false),
("user_b".to_string(), "yep".to_string(), false),
]
})
.await;
fn channel_messages(channel: &Channel) -> Vec<(String, String)> {
channel
.messages()
.cursor::<(), ()>()
.map(|m| (m.sender.github_login.clone(), m.body.clone()))
.collect()
}
}
struct TestServer {
@@ -1905,8 +1943,8 @@ mod tests {
&mut self,
cx: &mut TestAppContext,
name: &str,
) -> (UserId, Arc<Client>) {
let client_user_id = self.app_state.db.create_user(name, false).await.unwrap();
) -> (Arc<Client>, Arc<UserStore>) {
let user_id = self.app_state.db.create_user(name, false).await.unwrap();
let client_name = name.to_string();
let mut client = Client::new();
let server = self.server.clone();
@@ -1914,45 +1952,50 @@ mod tests {
let forbid_connections = self.forbid_connections.clone();
Arc::get_mut(&mut client)
.unwrap()
.set_login_and_connect_callbacks(
move |cx| {
cx.spawn(|_| async move {
let access_token = "the-token".to_string();
Ok((client_user_id.0 as u64, access_token))
.override_authenticate(move |cx| {
cx.spawn(|_| async move {
let access_token = "the-token".to_string();
Ok(Credentials {
user_id: user_id.0 as u64,
access_token,
})
},
move |user_id, access_token, cx| {
assert_eq!(user_id, client_user_id.0 as u64);
assert_eq!(access_token, "the-token");
})
})
.override_establish_connection(move |credentials, cx| {
assert_eq!(credentials.user_id, user_id.0 as u64);
assert_eq!(credentials.access_token, "the-token");
let server = server.clone();
let connection_killers = connection_killers.clone();
let forbid_connections = forbid_connections.clone();
let client_name = client_name.clone();
cx.spawn(move |cx| async move {
if forbid_connections.load(SeqCst) {
Err(anyhow!("server is forbidding connections"))
} else {
let (client_conn, server_conn, kill_conn) = Conn::in_memory();
connection_killers.lock().insert(client_user_id, kill_conn);
cx.background()
.spawn(server.handle_connection(
server_conn,
client_name,
client_user_id,
))
.detach();
Ok(client_conn)
}
})
},
);
let server = server.clone();
let connection_killers = connection_killers.clone();
let forbid_connections = forbid_connections.clone();
let client_name = client_name.clone();
cx.spawn(move |cx| async move {
if forbid_connections.load(SeqCst) {
Err(EstablishConnectionError::other(anyhow!(
"server is forbidding connections"
)))
} else {
let (client_conn, server_conn, kill_conn) = Connection::in_memory();
connection_killers.lock().insert(user_id, kill_conn);
cx.background()
.spawn(server.handle_connection(server_conn, client_name, user_id))
.detach();
Ok(client_conn)
}
})
});
let http = FakeHttpClient::new(|_| async move { Ok(surf::http::Response::new(404)) });
client
.authenticate_and_connect(&cx.to_async())
.await
.unwrap();
(client_user_id, client)
let user_store = UserStore::new(client.clone(), http, &cx.background());
let mut authed_user = user_store.watch_current_user();
while authed_user.recv().await.unwrap().is_none() {}
(client, user_store)
}
fn disconnect_client(&self, user_id: UserId) {
@@ -2008,6 +2051,24 @@ mod tests {
}
}
fn current_user_id(user_store: &Arc<UserStore>) -> UserId {
UserId::from_proto(user_store.current_user().unwrap().id)
}
fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
channel
.messages()
.cursor::<(), ()>()
.map(|m| {
(
m.sender.github_login.clone(),
m.body.clone(),
m.is_pending(),
)
})
.collect()
}
struct EmptyView;
impl gpui::Entity for EmptyView {
+7 -1
View File
@@ -14,10 +14,11 @@ name = "Zed"
path = "src/main.rs"
[features]
test-support = ["tempdir", "zrpc/test-support"]
test-support = ["tempdir", "zrpc/test-support", "gpui/test-support"]
[dependencies]
anyhow = "1.0.38"
async-recursion = "0.3"
async-trait = "0.1"
arrayvec = "0.7.1"
async-tungstenite = { version = "0.14", features = ["async-tls"] }
@@ -30,9 +31,12 @@ futures = "0.3"
gpui = { path = "../gpui" }
http-auth-basic = "0.1.3"
ignore = "0.4"
image = "0.23"
indexmap = "1.6.2"
lazy_static = "1.4.0"
libc = "0.2"
log = "0.4"
log-panics = { version = "2.0", features = ["with-backtrace"] }
num_cpus = "1.13.0"
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
@@ -49,6 +53,7 @@ smallvec = { version = "1.6", features = ["union"] }
smol = "1.2.5"
surf = "2.2"
tempdir = { version = "0.3.7", optional = true }
thiserror = "1.0.29"
time = { version = "0.3" }
tiny_http = "0.8"
toml = "0.5"
@@ -64,6 +69,7 @@ serde_json = { version = "1.0.64", features = ["preserve_order"] }
tempdir = { version = "0.3.7" }
unindent = "0.1.7"
zrpc = { path = "../zrpc", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
[package.metadata.bundle]
icon = ["app-icon@2x.png", "app-icon.png"]
Binary file not shown.
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.14992 9.84972C1.99189 9.84972 1.04997 8.90759 1.04997 7.74977C1.04997 6.87042 1.61368 6.07944 2.45278 5.78173L3.15692 5.53193L3.15307 4.98616L2.15704 4.18643C2.12729 4.39861 2.10016 4.59111 2.1017 4.79235C0.880227 5.22546 0 6.38043 0 7.74977C0 9.48879 1.41024 10.8997 3.14992 10.8997H10.6988L9.3592 9.84972H3.14992ZM13.0569 10.0816C13.6343 9.5391 13.9996 8.77787 13.9996 7.92477C13.9996 6.58321 13.1056 5.46171 11.8855 5.09203C11.8888 5.04391 11.8997 4.99797 11.8997 4.94985C11.8997 3.59582 10.8038 2.49991 9.44976 2.49991C9.20017 2.49991 8.96436 2.54819 8.73752 2.61753C8.06948 1.70171 6.99544 1.09995 5.77485 1.09995C4.71394 1.09995 3.76678 1.55668 3.1018 2.27679L0.849166 0.51172C0.752699 0.436537 0.638515 0.399963 0.525643 0.399963C0.369897 0.399963 0.215398 0.468999 0.112194 0.600946C-0.0669139 0.82914 -0.0272556 1.15944 0.201048 1.33816L13.1509 11.4881C13.3806 11.6676 13.71 11.6265 13.8879 11.3989C14.067 11.1705 14.0273 10.8405 13.799 10.6617L13.0569 10.0816ZM12.2169 9.42317L3.92865 2.92427C4.40114 2.44916 5.05081 2.14992 5.77485 2.14992C6.61483 2.14992 7.38547 2.54585 7.88923 3.23642L8.32979 3.84016L9.04442 3.62167C10.1132 3.29487 10.8869 4.18078 10.8373 5.03039L10.7893 5.85549L11.58 6.09589C12.3984 6.34544 12.9497 7.08042 12.9497 7.92477C12.9497 8.53288 12.6609 9.0688 12.2169 9.42317Z" fill="#B3B3B3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 3.04688C5.00332 3.04688 4.19531 3.85488 4.19531 4.85156C4.19531 5.84824 5.00332 6.65625 6 6.65625C6.99668 6.65625 7.80469 5.84824 7.80469 4.85156C7.80469 3.85488 6.99668 3.04688 6 3.04688ZM6 5.67188C5.5476 5.67188 5.17969 5.30376 5.17969 4.85156C5.17969 4.39834 5.54678 4.03125 6 4.03125C6.45322 4.03125 6.82031 4.39916 6.82031 4.85156C6.82031 5.30479 6.45322 5.67188 6 5.67188ZM6 0.75C3.1002 0.75 0.75 3.1002 0.75 6C0.75 8.8998 3.1002 11.25 6 11.25C8.8998 11.25 11.25 8.8998 11.25 6C11.25 3.1002 8.8998 0.75 6 0.75ZM6 10.2656C5.04167 10.2656 4.15922 9.94406 3.44678 9.4086C3.80156 8.72754 4.49062 8.29688 5.26582 8.29688H6.73603C7.5102 8.29688 8.19844 8.72774 8.55466 9.4086C7.8416 9.94365 6.95771 10.2656 6 10.2656ZM9.28535 8.71729C8.73164 7.85186 7.78828 7.3125 6.73418 7.3125H5.26582C4.21254 7.3125 3.26938 7.85083 2.71465 8.71688C2.1027 7.979 1.73438 7.03154 1.73438 6C1.73438 3.64775 3.64796 1.73438 6 1.73438C8.35204 1.73438 10.2656 3.64796 10.2656 6C10.2656 7.03154 9.89648 7.979 9.28535 8.71729Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+21 -8
View File
@@ -7,11 +7,18 @@ pane_divider = { width = 1, color = "$border.0" }
[workspace.titlebar]
border = { width = 1, bottom = true, color = "$border.0" }
text = { extends = "$text.0" }
title = "$text.0"
avatar_width = 20
icon_color = "$text.2.color"
avatar = { corner_radius = 10, border = { width = 1, color = "#00000088" } }
[workspace.titlebar.offline_icon]
padding = { right = 4 }
width = 16
[workspace.tab]
text = "$text.2"
padding = { left = 10, right = 10 }
padding = { left = 12, right = 12 }
icon_width = 8
spacing = 10
icon_close = "$text.2.color"
@@ -26,7 +33,7 @@ background = "$surface.1"
text = "$text.0"
[workspace.sidebar]
padding = { left = 12, right = 12 }
width = 32
border = { right = true, width = 1, color = "$border.0" }
[workspace.sidebar.resize_handle]
@@ -60,6 +67,12 @@ sender = { extends = "$text.0", weight = "bold", margin.right = 8 }
timestamp = "$text.2"
padding.bottom = 6
[chat_panel.pending_message]
extends = "$chat_panel.message"
body = { color = "$text.3.color" }
sender = { color = "$text.3.color" }
timestamp = { color = "$text.3.color" }
[chat_panel.channel_select.item]
padding = 4
name = "$text.1"
@@ -94,8 +107,8 @@ shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" }
background = "$surface.1"
corner_radius = 6
padding = { left = 8, right = 8, top = 7, bottom = 7 }
text = "$text.0.color"
placeholder_text = "$text.2.color"
text = "$text.0"
placeholder_text = "$text.2"
selection = "$selection.host"
border = { width = 1, color = "$border.0" }
@@ -119,8 +132,8 @@ border = { width = 1, color = "$border.0" }
background = "$surface.1"
corner_radius = 6
padding = { left = 16, right = 16, top = 7, bottom = 7 }
text = "$text.0.color"
placeholder_text = "$text.2.color"
text = "$text.0"
placeholder_text = "$text.2"
selection = "$selection.host"
border = { width = 1, color = "$border.0" }
@@ -140,7 +153,7 @@ background = "$state.hover"
text = "$text.0"
[editor]
text = "$text.1.color"
text = "$text.1"
background = "$surface.1"
gutter_background = "$surface.1"
active_line_background = "$state.active_line"
+1 -2
View File
@@ -9,7 +9,6 @@ extends = "_base"
0 = "#0F1011"
[text]
base = { family = "Inconsolata", size = 15 }
0 = { extends = "$text.base", color = "#ffffff" }
1 = { extends = "$text.base", color = "#b3b3b3" }
2 = { extends = "$text.base", color = "#7b7d80" }
@@ -49,4 +48,4 @@ number = "#b5cea8"
comment = "#6a9955"
property = "#4e94ce"
variant = "#4fc1ff"
constant = "#9cdcfe"
constant = "#9cdcfe"
-1
View File
@@ -9,7 +9,6 @@ extends = "_base"
0 = "#1B222B"
[text]
base = { family = "Inconsolata", size = 15 }
0 = { extends = "$text.base", color = "#FFFFFF" }
1 = { extends = "$text.base", color = "#CDD1E2" }
2 = { extends = "$text.base", color = "#9BA8BE" }
-1
View File
@@ -9,7 +9,6 @@ extends = "_base"
0 = "#DDDDDC"
[text]
base = { family = "Inconsolata", size = 15 }
0 = { extends = "$text.base", color = "#000000" }
1 = { extends = "$text.base", color = "#29292B" }
2 = { extends = "$text.base", color = "#7E7E83" }
+161 -56
View File
@@ -1,7 +1,7 @@
use crate::{
rpc::{self, Client},
user::{User, UserStore},
util::TryFutureExt,
util::{post_inc, TryFutureExt},
};
use anyhow::{anyhow, Context, Result};
use gpui::{
@@ -9,6 +9,7 @@ use gpui::{
Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle,
};
use postage::prelude::Stream;
use rand::prelude::*;
use std::{
collections::{HashMap, HashSet},
mem,
@@ -39,29 +40,31 @@ pub struct Channel {
details: ChannelDetails,
messages: SumTree<ChannelMessage>,
loaded_all_messages: bool,
pending_messages: Vec<PendingChannelMessage>,
next_local_message_id: u64,
next_pending_message_id: usize,
user_store: Arc<UserStore>,
rpc: Arc<Client>,
rng: StdRng,
_subscription: rpc::Subscription,
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug)]
pub struct ChannelMessage {
pub id: u64,
pub id: ChannelMessageId,
pub body: String,
pub timestamp: OffsetDateTime,
pub sender: Arc<User>,
pub nonce: u128,
}
pub struct PendingChannelMessage {
pub body: String,
local_id: u64,
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChannelMessageId {
Saved(u64),
Pending(usize),
}
#[derive(Clone, Debug, Default)]
pub struct ChannelMessageSummary {
max_id: u64,
max_id: ChannelMessageId,
count: usize,
}
@@ -118,7 +121,7 @@ impl ChannelList {
cx.notify();
});
}
rpc::Status::Disconnected { .. } => {
rpc::Status::SignedOut { .. } => {
this.update(&mut cx, |this, cx| {
this.available_channels = None;
this.channels.clear();
@@ -216,9 +219,9 @@ impl Channel {
user_store,
rpc,
messages: Default::default(),
pending_messages: Default::default(),
loaded_all_messages: false,
next_local_message_id: 0,
next_pending_message_id: 0,
rng: StdRng::from_entropy(),
_subscription,
}
}
@@ -236,17 +239,35 @@ impl Channel {
Err(anyhow!("message body can't be empty"))?;
}
let current_user = self
.user_store
.current_user()
.ok_or_else(|| anyhow!("current_user is not present"))?;
let channel_id = self.details.id;
let local_id = self.next_local_message_id;
self.next_local_message_id += 1;
self.pending_messages.push(PendingChannelMessage {
local_id,
body: body.clone(),
});
let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
let nonce = self.rng.gen();
self.insert_messages(
SumTree::from_item(
ChannelMessage {
id: pending_id,
body: body.clone(),
sender: current_user,
timestamp: OffsetDateTime::now_utc(),
nonce,
},
&(),
),
cx,
);
let user_store = self.user_store.clone();
let rpc = self.rpc.clone();
Ok(cx.spawn(|this, mut cx| async move {
let request = rpc.request(proto::SendChannelMessage { channel_id, body });
let request = rpc.request(proto::SendChannelMessage {
channel_id,
body,
nonce: Some(nonce.into()),
});
let response = request.await?;
let message = ChannelMessage::from_proto(
response.message.ok_or_else(|| anyhow!("invalid message"))?,
@@ -254,13 +275,7 @@ impl Channel {
)
.await?;
this.update(&mut cx, |this, cx| {
if let Ok(i) = this
.pending_messages
.binary_search_by_key(&local_id, |msg| msg.local_id)
{
this.pending_messages.remove(i);
this.insert_messages(SumTree::from_item(message, &()), cx);
}
this.insert_messages(SumTree::from_item(message, &()), cx);
Ok(())
})
}))
@@ -271,7 +286,12 @@ impl Channel {
let rpc = self.rpc.clone();
let user_store = self.user_store.clone();
let channel_id = self.details.id;
if let Some(before_message_id) = self.messages.first().map(|message| message.id) {
if let Some(before_message_id) =
self.messages.first().and_then(|message| match message.id {
ChannelMessageId::Saved(id) => Some(id),
ChannelMessageId::Pending(_) => None,
})
{
cx.spawn(|this, mut cx| {
async move {
let response = rpc
@@ -301,32 +321,51 @@ impl Channel {
let user_store = self.user_store.clone();
let rpc = self.rpc.clone();
let channel_id = self.details.id;
cx.spawn(|channel, mut cx| {
cx.spawn(|this, mut cx| {
async move {
let response = rpc.request(proto::JoinChannel { channel_id }).await?;
let messages = messages_from_proto(response.messages, &user_store).await?;
let loaded_all_messages = response.done;
channel.update(&mut cx, |channel, cx| {
let pending_messages = this.update(&mut cx, |this, cx| {
if let Some((first_new_message, last_old_message)) =
messages.first().zip(channel.messages.last())
messages.first().zip(this.messages.last())
{
if first_new_message.id > last_old_message.id {
let old_messages = mem::take(&mut channel.messages);
let old_messages = mem::take(&mut this.messages);
cx.emit(ChannelEvent::MessagesUpdated {
old_range: 0..old_messages.summary().count,
new_count: 0,
});
channel.loaded_all_messages = loaded_all_messages;
this.loaded_all_messages = loaded_all_messages;
}
}
channel.insert_messages(messages, cx);
this.insert_messages(messages, cx);
if loaded_all_messages {
channel.loaded_all_messages = loaded_all_messages;
this.loaded_all_messages = loaded_all_messages;
}
this.pending_messages().cloned().collect::<Vec<_>>()
});
for pending_message in pending_messages {
let request = rpc.request(proto::SendChannelMessage {
channel_id,
body: pending_message.body,
nonce: Some(pending_message.nonce.into()),
});
let response = request.await?;
let message = ChannelMessage::from_proto(
response.message.ok_or_else(|| anyhow!("invalid message"))?,
&user_store,
)
.await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
});
}
Ok(())
}
.log_err()
@@ -354,8 +393,10 @@ impl Channel {
cursor.take(range.len())
}
pub fn pending_messages(&self) -> &[PendingChannelMessage] {
&self.pending_messages
pub fn pending_messages(&self) -> impl Iterator<Item = &ChannelMessage> {
let mut cursor = self.messages.cursor::<ChannelMessageId, ()>();
cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &());
cursor
}
fn handle_message_sent(
@@ -386,7 +427,12 @@ impl Channel {
fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut ModelContext<Self>) {
if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
let mut old_cursor = self.messages.cursor::<u64, Count>();
let nonces = messages
.cursor::<(), ()>()
.map(|m| m.nonce)
.collect::<HashSet<_>>();
let mut old_cursor = self.messages.cursor::<ChannelMessageId, Count>();
let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &());
let start_ix = old_cursor.sum_start().0;
let removed_messages = old_cursor.slice(&last_message.id, Bias::Right, &());
@@ -395,10 +441,40 @@ impl Channel {
let end_ix = start_ix + removed_count;
new_messages.push_tree(messages, &());
new_messages.push_tree(old_cursor.suffix(&()), &());
let mut ranges = Vec::<Range<usize>>::new();
if new_messages.last().unwrap().is_pending() {
new_messages.push_tree(old_cursor.suffix(&()), &());
} else {
new_messages.push_tree(
old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left, &()),
&(),
);
while let Some(message) = old_cursor.item() {
let message_ix = old_cursor.sum_start().0;
if nonces.contains(&message.nonce) {
if ranges.last().map_or(false, |r| r.end == message_ix) {
ranges.last_mut().unwrap().end += 1;
} else {
ranges.push(message_ix..message_ix + 1);
}
} else {
new_messages.push(message.clone(), &());
}
old_cursor.next(&());
}
}
drop(old_cursor);
self.messages = new_messages;
for range in ranges.into_iter().rev() {
cx.emit(ChannelEvent::MessagesUpdated {
old_range: range,
new_count: 0,
});
}
cx.emit(ChannelEvent::MessagesUpdated {
old_range: start_ix..end_ix,
new_count,
@@ -443,14 +519,22 @@ impl ChannelMessage {
message: proto::ChannelMessage,
user_store: &UserStore,
) -> Result<Self> {
let sender = user_store.get_user(message.sender_id).await?;
let sender = user_store.fetch_user(message.sender_id).await?;
Ok(ChannelMessage {
id: message.id,
id: ChannelMessageId::Saved(message.id),
body: message.body,
timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
sender,
nonce: message
.nonce
.ok_or_else(|| anyhow!("nonce is required"))?
.into(),
})
}
pub fn is_pending(&self) -> bool {
matches!(self.id, ChannelMessageId::Pending(_))
}
}
impl sum_tree::Item for ChannelMessage {
@@ -464,6 +548,12 @@ impl sum_tree::Item for ChannelMessage {
}
}
impl Default for ChannelMessageId {
fn default() -> Self {
Self::Saved(0)
}
}
impl sum_tree::Summary for ChannelMessageSummary {
type Context = ();
@@ -473,7 +563,7 @@ impl sum_tree::Summary for ChannelMessageSummary {
}
}
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for u64 {
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for ChannelMessageId {
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
debug_assert!(summary.max_id > *self);
*self = summary.max_id;
@@ -495,19 +585,36 @@ impl<'a> sum_tree::SeekDimension<'a, ChannelMessageSummary> for Count {
#[cfg(test)]
mod tests {
use super::*;
use crate::test::FakeServer;
use crate::test::{FakeHttpClient, FakeServer};
use gpui::TestAppContext;
use surf::http::Response;
#[gpui::test]
async fn test_channel_messages(mut cx: TestAppContext) {
let user_id = 5;
let mut client = Client::new();
let http_client = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) });
let server = FakeServer::for_client(user_id, &mut client, &cx).await;
let user_store = Arc::new(UserStore::new(client.clone()));
let user_store = UserStore::new(client.clone(), http_client, cx.background().as_ref());
let channel_list = cx.add_model(|cx| ChannelList::new(user_store, client.clone(), cx));
channel_list.read_with(&cx, |list, _| assert_eq!(list.available_channels(), None));
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
assert_eq!(get_users.payload.user_ids, vec![5]);
server
.respond(
get_users.receipt(),
proto::GetUsersResponse {
users: vec![proto::User {
id: 5,
github_login: "nathansobo".into(),
avatar_url: "http://avatar.com/nathansobo".into(),
}],
},
)
.await;
// Get the available channels.
let get_channels = server.receive::<proto::GetChannels>().await.unwrap();
server
@@ -551,12 +658,14 @@ mod tests {
body: "a".into(),
timestamp: 1000,
sender_id: 5,
nonce: Some(1.into()),
},
proto::ChannelMessage {
id: 11,
body: "b".into(),
timestamp: 1001,
sender_id: 6,
nonce: Some(2.into()),
},
],
done: false,
@@ -567,23 +676,16 @@ mod tests {
// Client requests all users for the received messages
let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
get_users.payload.user_ids.sort();
assert_eq!(get_users.payload.user_ids, vec![5, 6]);
assert_eq!(get_users.payload.user_ids, vec![6]);
server
.respond(
get_users.receipt(),
proto::GetUsersResponse {
users: vec![
proto::User {
id: 5,
github_login: "nathansobo".into(),
avatar_url: "http://avatar.com/nathansobo".into(),
},
proto::User {
id: 6,
github_login: "maxbrunsfeld".into(),
avatar_url: "http://avatar.com/maxbrunsfeld".into(),
},
],
users: vec![proto::User {
id: 6,
github_login: "maxbrunsfeld".into(),
avatar_url: "http://avatar.com/maxbrunsfeld".into(),
}],
},
)
.await;
@@ -617,6 +719,7 @@ mod tests {
body: "c".into(),
timestamp: 1002,
sender_id: 7,
nonce: Some(3.into()),
}),
})
.await;
@@ -672,12 +775,14 @@ mod tests {
body: "y".into(),
timestamp: 998,
sender_id: 5,
nonce: Some(4.into()),
},
proto::ChannelMessage {
id: 9,
body: "z".into(),
timestamp: 999,
sender_id: 6,
nonce: Some(5.into()),
},
],
},
+27 -16
View File
@@ -54,10 +54,15 @@ impl ChatPanel {
cx: &mut ViewContext<Self>,
) -> Self {
let input_editor = cx.add_view(|cx| {
Editor::auto_height(4, settings.clone(), cx).with_style({
let settings = settings.clone();
move |_| settings.borrow().theme.chat_panel.input_editor.as_editor()
})
Editor::auto_height(
4,
settings.clone(),
{
let settings = settings.clone();
move |_| settings.borrow().theme.chat_panel.input_editor.as_editor()
},
cx,
)
});
let channel_select = cx.add_view(|cx| {
let channel_list = channel_list.clone();
@@ -209,7 +214,7 @@ impl ChatPanel {
Flex::column()
.with_child(
Container::new(ChildView::new(self.channel_select.id()).boxed())
.with_style(&theme.chat_panel.channel_select.container)
.with_style(theme.chat_panel.channel_select.container)
.boxed(),
)
.with_child(self.render_active_channel_messages())
@@ -230,7 +235,12 @@ impl ChatPanel {
fn render_message(&self, message: &ChannelMessage) -> ElementBox {
let now = OffsetDateTime::now_utc();
let settings = self.settings.borrow();
let theme = &settings.theme.chat_panel.message;
let theme = if message.is_pending() {
&settings.theme.chat_panel.pending_message
} else {
&settings.theme.chat_panel.message
};
Container::new(
Flex::column()
.with_child(
@@ -243,7 +253,7 @@ impl ChatPanel {
)
.boxed(),
)
.with_style(&theme.sender.container)
.with_style(theme.sender.container)
.boxed(),
)
.with_child(
@@ -254,7 +264,7 @@ impl ChatPanel {
)
.boxed(),
)
.with_style(&theme.timestamp.container)
.with_style(theme.timestamp.container)
.boxed(),
)
.boxed(),
@@ -262,14 +272,14 @@ impl ChatPanel {
.with_child(Text::new(message.body.clone(), theme.body.clone()).boxed())
.boxed(),
)
.with_style(&theme.container)
.with_style(theme.container)
.boxed()
}
fn render_input_box(&self) -> ElementBox {
let theme = &self.settings.borrow().theme;
Container::new(ChildView::new(self.input_editor.id()).boxed())
.with_style(&theme.chat_panel.input_editor.container)
.with_style(theme.chat_panel.input_editor.container)
.boxed()
}
@@ -293,13 +303,13 @@ impl ChatPanel {
Flex::row()
.with_child(
Container::new(Label::new("#".to_string(), theme.hash.text.clone()).boxed())
.with_style(&theme.hash.container)
.with_style(theme.hash.container)
.boxed(),
)
.with_child(Label::new(channel.name.clone(), theme.name.clone()).boxed())
.boxed(),
)
.with_style(&theme.container)
.with_style(theme.container)
.boxed()
}
@@ -381,13 +391,14 @@ impl View for ChatPanel {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &self.settings.borrow().theme;
let element = match *self.rpc.status().borrow() {
rpc::Status::Connected { .. } => self.render_channel(),
_ => self.render_sign_in_prompt(cx),
let element = if self.rpc.user_id().is_some() {
self.render_channel()
} else {
self.render_sign_in_prompt(cx)
};
ConstrainedBox::new(
Container::new(element)
.with_style(&theme.chat_panel.container)
.with_style(theme.chat_panel.container)
.boxed(),
)
.with_min_width(150.)
+233 -364
View File
@@ -4,8 +4,9 @@ mod element;
pub mod movement;
use crate::{
settings::{HighlightId, Settings},
theme::{EditorStyle, Theme},
language::Language,
settings::Settings,
theme::Theme,
time::ReplicaId,
util::{post_inc, Bias},
workspace,
@@ -17,15 +18,9 @@ pub use display_map::DisplayPoint;
use display_map::*;
pub use element::*;
use gpui::{
action,
color::Color,
font_cache::FamilyId,
fonts::Properties as FontProperties,
geometry::vector::Vector2F,
keymap::Binding,
text_layout::{self, RunStyle},
AppContext, ClipboardItem, Element, ElementBox, Entity, FontCache, ModelHandle,
MutableAppContext, RenderContext, Task, TextLayoutCache, View, ViewContext, WeakViewHandle,
action, color::Color, fonts::TextStyle, geometry::vector::Vector2F, keymap::Binding,
text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle,
MutableAppContext, RenderContext, Task, View, ViewContext, WeakViewHandle,
};
use postage::watch;
use serde::{Deserialize, Serialize};
@@ -34,8 +29,6 @@ use smol::Timer;
use std::{
cell::RefCell,
cmp::{self, Ordering},
collections::BTreeMap,
fmt::Write,
iter::FromIterator,
mem,
ops::{Range, RangeInclusive},
@@ -278,6 +271,26 @@ pub enum EditorMode {
Full,
}
#[derive(Clone, Deserialize)]
pub struct EditorStyle {
pub text: TextStyle,
#[serde(default)]
pub placeholder_text: Option<TextStyle>,
pub background: Color,
pub selection: SelectionStyle,
pub gutter_background: Color,
pub active_line_background: Color,
pub line_number: Color,
pub line_number_active: Color,
pub guest_selections: Vec<SelectionStyle>,
}
#[derive(Clone, Copy, Default, Deserialize)]
pub struct SelectionStyle {
pub cursor: Color,
pub selection: Color,
}
pub struct Editor {
handle: WeakViewHandle<Self>,
buffer: ModelHandle<Buffer>,
@@ -290,10 +303,10 @@ pub struct Editor {
scroll_position: Vector2F,
scroll_top_anchor: Anchor,
autoscroll_requested: bool,
build_style: Option<Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>>,
build_style: Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>,
settings: watch::Receiver<Settings>,
focused: bool,
cursors_visible: bool,
show_local_cursors: bool,
blink_epoch: usize,
blinking_paused: bool,
mode: EditorMode,
@@ -305,8 +318,6 @@ pub struct Snapshot {
pub display_snapshot: DisplayMapSnapshot,
pub placeholder_text: Option<Arc<str>>,
pub theme: Arc<Theme>,
pub font_family: FamilyId,
pub font_size: f32,
is_focused: bool,
scroll_position: Vector2F,
scroll_top_anchor: Anchor,
@@ -324,9 +335,13 @@ struct ClipboardSelection {
}
impl Editor {
pub fn single_line(settings: watch::Receiver<Settings>, cx: &mut ViewContext<Self>) -> Self {
pub fn single_line(
settings: watch::Receiver<Settings>,
build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
cx: &mut ViewContext<Self>,
) -> Self {
let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
let mut view = Self::for_buffer(buffer, settings, cx);
let mut view = Self::for_buffer(buffer, settings, build_style, cx);
view.mode = EditorMode::SingleLine;
view
}
@@ -334,10 +349,11 @@ impl Editor {
pub fn auto_height(
max_lines: usize,
settings: watch::Receiver<Settings>,
build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
cx: &mut ViewContext<Self>,
) -> Self {
let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
let mut view = Self::for_buffer(buffer, settings, cx);
let mut view = Self::for_buffer(buffer, settings, build_style, cx);
view.mode = EditorMode::AutoHeight { max_lines };
view
}
@@ -345,10 +361,29 @@ impl Editor {
pub fn for_buffer(
buffer: ModelHandle<Buffer>,
settings: watch::Receiver<Settings>,
build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
cx: &mut ViewContext<Self>,
) -> Self {
let display_map =
cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.clone(), None, cx));
Self::new(buffer, settings, Rc::new(RefCell::new(build_style)), cx)
}
fn new(
buffer: ModelHandle<Buffer>,
settings: watch::Receiver<Settings>,
build_style: Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>,
cx: &mut ViewContext<Self>,
) -> Self {
let style = build_style.borrow_mut()(cx);
let display_map = cx.add_model(|cx| {
DisplayMap::new(
buffer.clone(),
settings.borrow().tab_size,
style.text.font_id,
style.text.font_size,
None,
cx,
)
});
cx.observe(&buffer, Self::on_buffer_changed).detach();
cx.subscribe(&buffer, Self::on_buffer_event).detach();
cx.observe(&display_map, Self::on_display_map_changed)
@@ -376,13 +411,13 @@ impl Editor {
next_selection_id,
add_selections_state: None,
select_larger_syntax_node_stack: Vec::new(),
build_style: None,
build_style,
scroll_position: Vector2F::zero(),
scroll_top_anchor: Anchor::min(),
autoscroll_requested: false,
settings,
focused: false,
cursors_visible: false,
show_local_cursors: false,
blink_epoch: 0,
blinking_paused: false,
mode: EditorMode::Full,
@@ -390,14 +425,6 @@ impl Editor {
}
}
pub fn with_style(
mut self,
f: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
) -> Self {
self.build_style = Some(Rc::new(RefCell::new(f)));
self
}
pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
self.buffer.read(cx).replica_id()
}
@@ -416,8 +443,6 @@ impl Editor {
scroll_top_anchor: self.scroll_top_anchor.clone(),
theme: settings.theme.clone(),
placeholder_text: self.placeholder_text.clone(),
font_family: settings.buffer_font_family,
font_size: settings.buffer_font_size,
is_focused: self
.handle
.upgrade(cx)
@@ -425,6 +450,10 @@ impl Editor {
}
}
pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
self.buffer.read(cx).language()
}
pub fn set_placeholder_text(
&mut self,
placeholder_text: impl Into<Arc<str>>,
@@ -2229,7 +2258,7 @@ impl Editor {
}
fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
self.cursors_visible = true;
self.show_local_cursors = true;
cx.notify();
let epoch = self.next_blink_epoch();
@@ -2254,7 +2283,7 @@ impl Editor {
fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
if epoch == self.blink_epoch && self.focused && !self.blinking_paused {
self.cursors_visible = !self.cursors_visible;
self.show_local_cursors = !self.show_local_cursors;
cx.notify();
let epoch = self.next_blink_epoch();
@@ -2271,8 +2300,8 @@ impl Editor {
}
}
pub fn cursors_visible(&self) -> bool {
self.cursors_visible
pub fn show_local_cursors(&self) -> bool {
self.show_local_cursors
}
fn on_buffer_changed(&mut self, _: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
@@ -2301,6 +2330,38 @@ impl Editor {
}
impl Snapshot {
pub fn is_empty(&self) -> bool {
self.display_snapshot.is_empty()
}
pub fn is_focused(&self) -> bool {
self.is_focused
}
pub fn placeholder_text(&self) -> Option<&Arc<str>> {
self.placeholder_text.as_ref()
}
pub fn buffer_row_count(&self) -> u32 {
self.display_snapshot.buffer_row_count()
}
pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
self.display_snapshot.buffer_rows(start_row)
}
pub fn highlighted_chunks_for_rows(
&mut self,
display_rows: Range<u32>,
) -> display_map::HighlightedChunks {
self.display_snapshot
.highlighted_chunks_for_rows(display_rows)
}
pub fn theme(&self) -> &Arc<Theme> {
&self.theme
}
pub fn scroll_position(&self) -> Vector2F {
compute_scroll_position(
&self.display_snapshot,
@@ -2321,243 +2382,8 @@ impl Snapshot {
self.display_snapshot.line_len(display_row)
}
pub fn font_ascent(&self, font_cache: &FontCache) -> f32 {
let font_id = font_cache.default_font(self.font_family);
let ascent = font_cache.metric(font_id, |m| m.ascent);
font_cache.scale_metric(ascent, font_id, self.font_size)
}
pub fn font_descent(&self, font_cache: &FontCache) -> f32 {
let font_id = font_cache.default_font(self.font_family);
let descent = font_cache.metric(font_id, |m| m.descent);
font_cache.scale_metric(descent, font_id, self.font_size)
}
pub fn line_height(&self, font_cache: &FontCache) -> f32 {
let font_id = font_cache.default_font(self.font_family);
font_cache.line_height(font_id, self.font_size).ceil()
}
pub fn em_width(&self, font_cache: &FontCache) -> f32 {
let font_id = font_cache.default_font(self.font_family);
font_cache.em_width(font_id, self.font_size)
}
// TODO: Can we make this not return a result?
pub fn max_line_number_width(
&self,
font_cache: &FontCache,
layout_cache: &TextLayoutCache,
) -> Result<f32> {
let font_size = self.font_size;
let font_id = font_cache.select_font(self.font_family, &FontProperties::new())?;
let digit_count = (self.display_snapshot.buffer_row_count() as f32)
.log10()
.floor() as usize
+ 1;
Ok(layout_cache
.layout_str(
"1".repeat(digit_count).as_str(),
font_size,
&[(
digit_count,
RunStyle {
font_id,
color: Color::black(),
underline: false,
},
)],
)
.width())
}
pub fn layout_line_numbers(
&self,
rows: Range<u32>,
active_rows: &BTreeMap<u32, bool>,
font_cache: &FontCache,
layout_cache: &TextLayoutCache,
theme: &Theme,
) -> Result<Vec<Option<text_layout::Line>>> {
let font_id = font_cache.select_font(self.font_family, &FontProperties::new())?;
let mut layouts = Vec::with_capacity(rows.len());
let mut line_number = String::new();
for (ix, (buffer_row, soft_wrapped)) in self
.display_snapshot
.buffer_rows(rows.start)
.take((rows.end - rows.start) as usize)
.enumerate()
{
let display_row = rows.start + ix as u32;
let color = if active_rows.contains_key(&display_row) {
theme.editor.line_number_active
} else {
theme.editor.line_number
};
if soft_wrapped {
layouts.push(None);
} else {
line_number.clear();
write!(&mut line_number, "{}", buffer_row + 1).unwrap();
layouts.push(Some(layout_cache.layout_str(
&line_number,
self.font_size,
&[(
line_number.len(),
RunStyle {
font_id,
color,
underline: false,
},
)],
)));
}
}
Ok(layouts)
}
pub fn layout_lines(
&mut self,
mut rows: Range<u32>,
style: &EditorStyle,
font_cache: &FontCache,
layout_cache: &TextLayoutCache,
) -> Result<Vec<text_layout::Line>> {
rows.end = cmp::min(rows.end, self.display_snapshot.max_point().row() + 1);
if rows.start >= rows.end {
return Ok(Vec::new());
}
// When the editor is empty and unfocused, then show the placeholder.
if self.display_snapshot.is_empty() && !self.is_focused {
let placeholder_lines = self
.placeholder_text
.as_ref()
.map_or("", AsRef::as_ref)
.split('\n')
.skip(rows.start as usize)
.take(rows.len());
let font_id = font_cache
.select_font(self.font_family, &style.placeholder_text.font_properties)?;
return Ok(placeholder_lines
.into_iter()
.map(|line| {
layout_cache.layout_str(
line,
self.font_size,
&[(
line.len(),
RunStyle {
font_id,
color: style.placeholder_text.color,
underline: false,
},
)],
)
})
.collect());
}
let mut prev_font_properties = FontProperties::new();
let mut prev_font_id = font_cache
.select_font(self.font_family, &prev_font_properties)
.unwrap();
let mut layouts = Vec::with_capacity(rows.len());
let mut line = String::new();
let mut styles = Vec::new();
let mut row = rows.start;
let mut line_exceeded_max_len = false;
let chunks = self
.display_snapshot
.highlighted_chunks_for_rows(rows.clone());
'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) {
for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
if ix > 0 {
layouts.push(layout_cache.layout_str(&line, self.font_size, &styles));
line.clear();
styles.clear();
row += 1;
line_exceeded_max_len = false;
if row == rows.end {
break 'outer;
}
}
if !line_chunk.is_empty() && !line_exceeded_max_len {
let style = self
.theme
.syntax
.highlight_style(style_ix)
.unwrap_or(style.text.clone());
// Avoid a lookup if the font properties match the previous ones.
let font_id = if style.font_properties == prev_font_properties {
prev_font_id
} else {
font_cache.select_font(self.font_family, &style.font_properties)?
};
if line.len() + line_chunk.len() > MAX_LINE_LEN {
let mut chunk_len = MAX_LINE_LEN - line.len();
while !line_chunk.is_char_boundary(chunk_len) {
chunk_len -= 1;
}
line_chunk = &line_chunk[..chunk_len];
line_exceeded_max_len = true;
}
line.push_str(line_chunk);
styles.push((
line_chunk.len(),
RunStyle {
font_id,
color: style.color,
underline: style.underline,
},
));
prev_font_id = font_id;
prev_font_properties = style.font_properties;
}
}
}
Ok(layouts)
}
pub fn layout_line(
&self,
row: u32,
font_cache: &FontCache,
layout_cache: &TextLayoutCache,
) -> Result<text_layout::Line> {
let font_id = font_cache.select_font(self.font_family, &FontProperties::new())?;
let mut line = self.display_snapshot.line(row);
if line.len() > MAX_LINE_LEN {
let mut len = MAX_LINE_LEN;
while !line.is_char_boundary(len) {
len -= 1;
}
line.truncate(len);
}
Ok(layout_cache.layout_str(
&line,
self.font_size,
&[(
self.display_snapshot.line_len(row) as usize,
RunStyle {
font_id,
color: Color::black(),
underline: false,
},
)],
))
pub fn line(&self, display_row: u32) -> String {
self.display_snapshot.line(display_row)
}
pub fn prev_row_boundary(&self, point: DisplayPoint) -> (DisplayPoint, Point) {
@@ -2569,6 +2395,41 @@ impl Snapshot {
}
}
impl EditorStyle {
#[cfg(any(test, feature = "test-support"))]
pub fn test(font_cache: &gpui::FontCache) -> Self {
let font_family_name = Arc::from("Monaco");
let font_properties = Default::default();
let font_family_id = font_cache.load_family(&[&font_family_name]).unwrap();
let font_id = font_cache
.select_font(font_family_id, &font_properties)
.unwrap();
Self {
text: TextStyle {
font_family_name,
font_family_id,
font_id,
font_size: 14.,
color: Color::from_u32(0xff0000ff),
font_properties,
underline: false,
},
placeholder_text: None,
background: Default::default(),
gutter_background: Default::default(),
active_line_background: Default::default(),
line_number: Default::default(),
line_number_active: Default::default(),
selection: Default::default(),
guest_selections: Default::default(),
}
}
fn placeholder_text(&self) -> &TextStyle {
self.placeholder_text.as_ref().unwrap_or(&self.text)
}
}
fn compute_scroll_position(
snapshot: &DisplayMapSnapshot,
mut scroll_position: Vector2F,
@@ -2604,10 +2465,10 @@ impl Entity for Editor {
impl View for Editor {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let style = self
.build_style
.as_ref()
.map_or(Default::default(), |build| (build.borrow_mut())(cx));
let style = self.build_style.borrow_mut()(cx);
self.display_map.update(cx, |map, cx| {
map.set_font(style.text.font_id, style.text.font_size, cx)
});
EditorElement::new(self.handle.clone(), style).boxed()
}
@@ -2627,7 +2488,7 @@ impl View for Editor {
fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
self.focused = false;
self.cursors_visible = false;
self.show_local_cursors = false;
self.buffer.update(cx, |buffer, cx| {
buffer.set_active_selection_set(None, cx).unwrap();
});
@@ -2659,8 +2520,34 @@ impl workspace::Item for Buffer {
settings: watch::Receiver<Settings>,
cx: &mut ViewContext<Self::View>,
) -> Self::View {
Editor::for_buffer(handle, settings.clone(), cx)
.with_style(move |_| settings.borrow().theme.editor.clone())
Editor::for_buffer(
handle,
settings.clone(),
move |cx| {
let settings = settings.borrow();
let font_cache = cx.font_cache();
let font_family_id = settings.buffer_font_family;
let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
let font_properties = Default::default();
let font_id = font_cache
.select_font(font_family_id, &font_properties)
.unwrap();
let font_size = settings.buffer_font_size;
let mut theme = settings.theme.editor.clone();
theme.text = TextStyle {
color: theme.text.color,
font_family_name,
font_family_id,
font_id,
font_size,
font_properties,
underline: false,
};
theme
},
cx,
)
}
}
@@ -2697,10 +2584,14 @@ impl workspace::ItemView for Editor {
where
Self: Sized,
{
let mut clone = Editor::for_buffer(self.buffer.clone(), self.settings.clone(), cx);
let mut clone = Editor::new(
self.buffer.clone(),
self.settings.clone(),
self.build_style.clone(),
cx,
);
clone.scroll_position = self.scroll_position;
clone.scroll_top_anchor = self.scroll_top_anchor.clone();
clone.build_style = self.build_style.clone();
Some(clone)
}
@@ -2742,9 +2633,8 @@ mod tests {
fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx));
let settings = settings::test(&cx).1;
let (_, editor) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, editor) =
cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
editor.update(cx, |view, cx| {
view.begin_selection(DisplayPoint::new(2, 2), false, cx);
@@ -2810,9 +2700,7 @@ mod tests {
fn test_canceling_pending_selection(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx));
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.begin_selection(DisplayPoint::new(2, 2), false, cx);
@@ -2844,9 +2732,7 @@ mod tests {
fn test_cancel(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx));
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.begin_selection(DisplayPoint::new(3, 4), false, cx);
@@ -2882,33 +2768,6 @@ mod tests {
});
}
#[gpui::test]
fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
let layout_cache = TextLayoutCache::new(cx.platform().fonts());
let font_cache = cx.font_cache().clone();
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
let settings = settings::test(&cx).1;
let (_, editor) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer.clone(), settings.clone(), cx)
});
let layouts = editor.update(cx, |editor, cx| {
editor
.snapshot(cx)
.layout_line_numbers(
0..6,
&Default::default(),
&font_cache,
&layout_cache,
&settings.borrow().theme,
)
.unwrap()
});
assert_eq!(layouts.len(), 6);
}
#[gpui::test]
fn test_fold(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| {
@@ -2937,7 +2796,7 @@ mod tests {
});
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer.clone(), settings, cx)
build_editor(buffer.clone(), settings, cx)
});
view.update(cx, |view, cx| {
@@ -3005,7 +2864,7 @@ mod tests {
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer.clone(), settings, cx)
build_editor(buffer.clone(), settings, cx)
});
buffer.update(cx, |buffer, cx| {
@@ -3082,7 +2941,7 @@ mod tests {
let buffer = cx.add_model(|cx| Buffer::new(0, "ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx));
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer.clone(), settings, cx)
build_editor(buffer.clone(), settings, cx)
});
assert_eq!('ⓐ'.len_utf8(), 3);
@@ -3140,7 +2999,7 @@ mod tests {
let buffer = cx.add_model(|cx| Buffer::new(0, "ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx));
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer.clone(), settings, cx)
build_editor(buffer.clone(), settings, cx)
});
view.update(cx, |view, cx| {
view.select_display_ranges(&[empty_range(0, "ⓐⓑⓒⓓⓔ".len())], cx)
@@ -3170,9 +3029,7 @@ mod tests {
fn test_beginning_end_of_line(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\n def", cx));
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(
&[
@@ -3315,9 +3172,7 @@ mod tests {
let buffer =
cx.add_model(|cx| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}", cx));
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(
&[
@@ -3509,9 +3364,7 @@ mod tests {
let buffer =
cx.add_model(|cx| Buffer::new(0, "use one::{\n two::three::four::five\n};", cx));
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.set_wrap_width(130., cx);
@@ -3572,7 +3425,7 @@ mod tests {
});
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer.clone(), settings, cx)
build_editor(buffer.clone(), settings, cx)
});
view.update(cx, |view, cx| {
@@ -3608,7 +3461,7 @@ mod tests {
});
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer.clone(), settings, cx)
build_editor(buffer.clone(), settings, cx)
});
view.update(cx, |view, cx| {
@@ -3637,9 +3490,7 @@ mod tests {
fn test_delete_line(cx: &mut gpui::MutableAppContext) {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(
&[
@@ -3663,9 +3514,7 @@ mod tests {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], cx)
.unwrap();
@@ -3682,9 +3531,7 @@ mod tests {
fn test_duplicate_line(cx: &mut gpui::MutableAppContext) {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(
&[
@@ -3711,9 +3558,7 @@ mod tests {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(
&[
@@ -3739,9 +3584,7 @@ mod tests {
fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(10, 5), cx));
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.fold_ranges(
vec![
@@ -3840,7 +3683,7 @@ mod tests {
let settings = settings::test(&cx).1;
let view = cx
.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer.clone(), settings, cx)
build_editor(buffer.clone(), settings, cx)
})
.1;
@@ -3973,9 +3816,7 @@ mod tests {
fn test_select_all(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\nde\nfgh", cx));
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_all(&SelectAll, cx);
assert_eq!(
@@ -3989,9 +3830,7 @@ mod tests {
fn test_select_line(cx: &mut gpui::MutableAppContext) {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 5), cx));
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(
&[
@@ -4037,9 +3876,7 @@ mod tests {
fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(9, 5), cx));
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.fold_ranges(
vec![
@@ -4107,9 +3944,7 @@ mod tests {
fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefghi\n\njk\nlmno\n", cx));
let (_, view) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(buffer, settings, cx)
});
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)], cx)
@@ -4295,7 +4130,7 @@ mod tests {
let history = History::new(text.into());
Buffer::from_history(0, history, None, lang.cloned(), cx)
});
let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings.clone(), cx));
let (_, view) = cx.add_window(|cx| build_editor(buffer, settings.clone(), cx));
view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing())
.await;
@@ -4433,6 +4268,40 @@ mod tests {
let point = DisplayPoint::new(row as u32, column as u32);
point..point
}
fn build_editor(
buffer: ModelHandle<Buffer>,
settings: watch::Receiver<Settings>,
cx: &mut ViewContext<Editor>,
) -> Editor {
let style = {
let font_cache = cx.font_cache();
let settings = settings.borrow();
EditorStyle {
text: TextStyle {
color: Default::default(),
font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
font_family_id: settings.buffer_font_family,
font_id: font_cache
.select_font(settings.buffer_font_family, &Default::default())
.unwrap(),
font_size: settings.buffer_font_size,
font_properties: Default::default(),
underline: false,
},
placeholder_text: None,
background: Default::default(),
selection: Default::default(),
gutter_background: Default::default(),
active_line_background: Default::default(),
line_number: Default::default(),
line_number_active: Default::default(),
guest_selections: Default::default(),
}
};
Editor::for_buffer(buffer, settings, move |_| style.clone(), cx)
}
}
trait RangeExt<T> {
+16 -1
View File
@@ -714,9 +714,16 @@ impl Buffer {
path: impl Into<Arc<Path>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let path = path.into();
let handle = cx.handle();
let text = self.visible_text.clone();
let version = self.version.clone();
if let Some(language) = worktree.read(cx).languages().select_language(&path).cloned() {
self.language = Some(language);
self.reparse(cx);
}
let save_as = worktree.update(cx, |worktree, cx| {
worktree
.as_local_mut()
@@ -794,6 +801,10 @@ impl Buffer {
cx.emit(Event::FileHandleChanged);
}
pub fn language(&self) -> Option<&Arc<Language>> {
self.language.as_ref()
}
pub fn parse_count(&self) -> usize {
self.parse_count
}
@@ -871,7 +882,11 @@ impl Buffer {
cx.spawn(move |this, mut cx| async move {
let new_tree = parse_task.await;
this.update(&mut cx, move |this, cx| {
let parse_again = this.version > parsed_version;
let language_changed =
this.language.as_ref().map_or(true, |curr_language| {
!Arc::ptr_eq(curr_language, &language)
});
let parse_again = this.version > parsed_version || language_changed;
*this.syntax_tree.lock() = Some(SyntaxTree {
tree: new_tree,
dirty: false,
+97 -64
View File
@@ -2,14 +2,13 @@ mod fold_map;
mod tab_map;
mod wrap_map;
use super::{buffer, Anchor, Bias, Buffer, Point, Settings, ToOffset, ToPoint};
use super::{buffer, Anchor, Bias, Buffer, Point, ToOffset, ToPoint};
use fold_map::FoldMap;
use gpui::{Entity, ModelContext, ModelHandle};
use postage::watch;
use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle};
use std::ops::Range;
use tab_map::TabMap;
pub use wrap_map::BufferRows;
use wrap_map::WrapMap;
pub use wrap_map::{BufferRows, HighlightedChunks};
pub struct DisplayMap {
buffer: ModelHandle<Buffer>,
@@ -25,13 +24,16 @@ impl Entity for DisplayMap {
impl DisplayMap {
pub fn new(
buffer: ModelHandle<Buffer>,
settings: watch::Receiver<Settings>,
tab_size: usize,
font_id: FontId,
font_size: f32,
wrap_width: Option<f32>,
cx: &mut ModelContext<Self>,
) -> Self {
let (fold_map, snapshot) = FoldMap::new(buffer.clone(), cx);
let (tab_map, snapshot) = TabMap::new(snapshot, settings.borrow().tab_size);
let wrap_map = cx.add_model(|cx| WrapMap::new(snapshot, settings, wrap_width, cx));
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let wrap_map =
cx.add_model(|cx| WrapMap::new(snapshot, font_id, font_size, wrap_width, cx));
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
DisplayMap {
buffer,
@@ -85,6 +87,11 @@ impl DisplayMap {
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
}
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
self.wrap_map
.update(cx, |map, cx| map.set_font(font_id, font_size, cx));
}
pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
self.wrap_map
.update(cx, |map, cx| map.set_wrap_width(width, cx))
@@ -367,12 +374,12 @@ mod tests {
.unwrap_or(10);
let font_cache = cx.font_cache().clone();
let settings = Settings {
tab_size: rng.gen_range(1..=4),
buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
buffer_font_size: 14.0,
..cx.read(Settings::test)
};
let tab_size = rng.gen_range(1..=4);
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let max_wrap_width = 300.0;
let mut wrap_width = if rng.gen_bool(0.1) {
None
@@ -380,7 +387,7 @@ mod tests {
Some(rng.gen_range(0.0..=max_wrap_width))
};
log::info!("tab size: {}", settings.tab_size);
log::info!("tab size: {}", tab_size);
log::info!("wrap width: {:?}", wrap_width);
let buffer = cx.add_model(|cx| {
@@ -388,9 +395,10 @@ mod tests {
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
Buffer::new(0, text, cx)
});
let settings = watch::channel_with(settings).1;
let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings, wrap_width, cx));
let map = cx.add_model(|cx| {
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
});
let (_observer, notifications) = Observer::new(&map, &mut cx);
let mut fold_count = 0;
@@ -529,26 +537,27 @@ mod tests {
}
#[gpui::test]
async fn test_soft_wraps(mut cx: gpui::TestAppContext) {
fn test_soft_wraps(cx: &mut MutableAppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
cx.foreground().forbid_parking();
let font_cache = cx.font_cache();
let settings = Settings {
buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
buffer_font_size: 12.0,
tab_size: 4,
..cx.read(Settings::test)
};
let tab_size = 4;
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 12.0;
let wrap_width = Some(64.);
let text = "one two three four five\nsix seven eight";
let buffer = cx.add_model(|cx| Buffer::new(0, text.to_string(), cx));
let (mut settings_tx, settings_rx) = watch::channel_with(settings);
let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings_rx, wrap_width, cx));
let map = cx.add_model(|cx| {
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
});
let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
snapshot.chunks_at(0).collect::<String>(),
"one two \nthree four \nfive\nsix seven \neight"
@@ -592,23 +601,21 @@ mod tests {
(DisplayPoint::new(2, 4), SelectionGoal::Column(10))
);
buffer.update(&mut cx, |buffer, cx| {
buffer.update(cx, |buffer, cx| {
let ix = buffer.text().find("seven").unwrap();
buffer.edit(vec![ix..ix], "and ", cx);
});
let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
snapshot.chunks_at(1).collect::<String>(),
"three four \nfive\nsix and \nseven eight"
);
// Re-wrap on font size changes
settings_tx.borrow_mut().buffer_font_size += 3.;
map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
map.next_notification(&mut cx).await;
let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
snapshot.chunks_at(1).collect::<String>(),
"three \nfour five\nsix and \nseven \neight"
@@ -619,11 +626,16 @@ mod tests {
fn test_chunks_at(cx: &mut gpui::MutableAppContext) {
let text = sample_text(6, 6);
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
let settings = watch::channel_with(Settings {
tab_size: 4,
..Settings::test(cx)
let tab_size = 4;
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let map = cx.add_model(|cx| {
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
});
let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
buffer.update(cx, |buffer, cx| {
buffer.edit(
vec![
@@ -695,13 +707,16 @@ mod tests {
});
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
let settings = cx.update(|cx| {
watch::channel_with(Settings {
tab_size: 2,
..Settings::test(cx)
})
});
let map = cx.add_model(|cx| DisplayMap::new(buffer, settings.1, None, cx));
let tab_size = 2;
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let map =
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
assert_eq!(
cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
vec![
@@ -782,15 +797,16 @@ mod tests {
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
let font_cache = cx.font_cache();
let settings = cx.update(|cx| {
watch::channel_with(Settings {
tab_size: 4,
buffer_font_family: font_cache.load_family(&["Courier"]).unwrap(),
buffer_font_size: 16.0,
..Settings::test(cx)
})
});
let map = cx.add_model(|cx| DisplayMap::new(buffer, settings.1, Some(40.0), cx));
let tab_size = 4;
let family_id = font_cache.load_family(&["Courier"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 16.0;
let map = cx
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), cx));
assert_eq!(
cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
[
@@ -825,11 +841,17 @@ mod tests {
let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n";
let display_text = "\n'a', 'α', '✋', '❎', '🍐'\n";
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
let settings = watch::channel_with(Settings {
tab_size: 4,
..Settings::test(cx)
let tab_size = 4;
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let map = cx.add_model(|cx| {
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
});
let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
let map = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(map.text(), display_text);
@@ -863,11 +885,17 @@ mod tests {
fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
let text = "\t\tα\nβ\t\n🏀β\t\tγ";
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
let settings = watch::channel_with(Settings {
tab_size: 4,
..Settings::test(cx)
let tab_size = 4;
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let map = cx.add_model(|cx| {
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
});
let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
let map = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(map.text(), "α\nβ \n🏀β γ");
assert_eq!(
@@ -924,11 +952,16 @@ mod tests {
#[gpui::test]
fn test_max_point(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "aaa\n\t\tbbb", cx));
let settings = watch::channel_with(Settings {
tab_size: 4,
..Settings::test(cx)
let tab_size = 4;
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let map = cx.add_model(|cx| {
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
});
let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
assert_eq!(
map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
DisplayPoint::new(1, 11)
+30 -57
View File
@@ -2,14 +2,14 @@ use super::{
fold_map,
tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint, TextSummary},
};
use crate::{editor::Point, settings::HighlightId, util::Bias, Settings};
use crate::{editor::Point, settings::HighlightId, util::Bias};
use gpui::{
fonts::FontId,
sum_tree::{self, Cursor, SumTree},
text_layout::LineWrapper,
Entity, ModelContext, Task,
};
use lazy_static::lazy_static;
use postage::{prelude::Stream, watch};
use smol::future::yield_now;
use std::{collections::VecDeque, ops::Range, time::Duration};
@@ -18,8 +18,7 @@ pub struct WrapMap {
pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
wrap_width: Option<f32>,
background_task: Option<Task<()>>,
_watch_settings: Task<()>,
settings: watch::Receiver<Settings>,
font: (FontId, f32),
}
impl Entity for WrapMap {
@@ -76,36 +75,17 @@ pub struct BufferRows<'a> {
impl WrapMap {
pub fn new(
tab_snapshot: TabSnapshot,
settings: watch::Receiver<Settings>,
font_id: FontId,
font_size: f32,
wrap_width: Option<f32>,
cx: &mut ModelContext<Self>,
) -> Self {
let _watch_settings = cx.spawn_weak({
let mut prev_font = (
settings.borrow().buffer_font_size,
settings.borrow().buffer_font_family,
);
let mut settings = settings.clone();
move |this, mut cx| async move {
while let Some(settings) = settings.recv().await {
if let Some(this) = this.upgrade(&cx) {
let font = (settings.buffer_font_size, settings.buffer_font_family);
if font != prev_font {
prev_font = font;
this.update(&mut cx, |this, cx| this.rewrap(cx));
}
}
}
}
});
let mut this = Self {
font: (font_id, font_size),
wrap_width: None,
pending_edits: Default::default(),
snapshot: Snapshot::new(tab_snapshot),
settings,
background_task: None,
_watch_settings,
};
this.set_wrap_width(wrap_width, cx);
@@ -128,6 +108,13 @@ impl WrapMap {
self.snapshot.clone()
}
pub fn set_font(&mut self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
if (font_id, font_size) != self.font {
self.font = (font_id, font_size);
self.rewrap(cx)
}
}
pub fn set_wrap_width(&mut self, wrap_width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
if wrap_width == self.wrap_width {
return false;
@@ -144,15 +131,9 @@ impl WrapMap {
if let Some(wrap_width) = self.wrap_width {
let mut new_snapshot = self.snapshot.clone();
let font_cache = cx.font_cache().clone();
let settings = self.settings.clone();
let (font_id, font_size) = self.font;
let task = cx.background().spawn(async move {
let mut line_wrapper = {
let settings = settings.borrow();
let font_id = font_cache
.select_font(settings.buffer_font_family, &Default::default())
.unwrap();
font_cache.line_wrapper(font_id, settings.buffer_font_size)
};
let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
let tab_snapshot = new_snapshot.tab_snapshot.clone();
let range = TabPoint::zero()..tab_snapshot.max_point();
new_snapshot
@@ -222,15 +203,9 @@ impl WrapMap {
let pending_edits = self.pending_edits.clone();
let mut snapshot = self.snapshot.clone();
let font_cache = cx.font_cache().clone();
let settings = self.settings.clone();
let (font_id, font_size) = self.font;
let update_task = cx.background().spawn(async move {
let mut line_wrapper = {
let settings = settings.borrow();
let font_id = font_cache
.select_font(settings.buffer_font_family, &Default::default())
.unwrap();
font_cache.line_wrapper(font_id, settings.buffer_font_size)
};
let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
for (tab_snapshot, edits) in pending_edits {
snapshot
@@ -950,13 +925,14 @@ mod tests {
} else {
Some(rng.gen_range(0.0..=1000.0))
};
let settings = Settings {
tab_size: rng.gen_range(1..=4),
buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
buffer_font_size: 14.0,
..cx.read(Settings::test)
};
log::info!("Tab size: {}", settings.tab_size);
let tab_size = rng.gen_range(1..=4);
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
log::info!("Tab size: {}", tab_size);
log::info!("Wrap width: {:?}", wrap_width);
let buffer = cx.add_model(|cx| {
@@ -965,7 +941,7 @@ mod tests {
Buffer::new(0, text, cx)
});
let (mut fold_map, folds_snapshot) = cx.read(|cx| FoldMap::new(buffer.clone(), cx));
let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), settings.tab_size);
let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
log::info!(
"Unwrapped text (no folds): {:?}",
buffer.read_with(&cx, |buf, _| buf.text())
@@ -976,16 +952,13 @@ mod tests {
);
log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text());
let font_id = font_cache
.select_font(settings.buffer_font_family, &Default::default())
.unwrap();
let mut line_wrapper = LineWrapper::new(font_id, settings.buffer_font_size, font_system);
let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system);
let unwrapped_text = tabs_snapshot.text();
let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
let settings = watch::channel_with(settings).1;
let wrap_map = cx
.add_model(|cx| WrapMap::new(tabs_snapshot.clone(), settings.clone(), wrap_width, cx));
let wrap_map = cx.add_model(|cx| {
WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)
});
let (_observer, notifications) = Observer::new(&wrap_map, &mut cx);
if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) {
+271 -67
View File
@@ -1,5 +1,8 @@
use super::{DisplayPoint, Editor, EditorMode, Insert, Scroll, Select, SelectPhase, Snapshot};
use crate::{theme::EditorStyle, time::ReplicaId};
use super::{
DisplayPoint, Editor, EditorMode, EditorStyle, Insert, Scroll, Select, SelectPhase, Snapshot,
MAX_LINE_LEN,
};
use crate::{theme::HighlightId, time::ReplicaId};
use gpui::{
color::Color,
geometry::{
@@ -9,7 +12,7 @@ use gpui::{
},
json::{self, ToJson},
keymap::Keystroke,
text_layout::{self, TextLayoutCache},
text_layout::{self, RunStyle, TextLayoutCache},
AppContext, Axis, Border, Element, Event, EventContext, FontCache, LayoutContext,
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
};
@@ -18,6 +21,7 @@ use smallvec::SmallVec;
use std::{
cmp::{self, Ordering},
collections::{BTreeMap, HashMap},
fmt::Write,
ops::Range,
};
@@ -265,6 +269,7 @@ impl EditorElement {
let view = self.view(cx.app);
let settings = self.view(cx.app).settings.borrow();
let theme = &settings.theme.editor;
let local_replica_id = view.replica_id(cx);
let scroll_position = layout.snapshot.scroll_position();
let start_row = scroll_position.y() as u32;
let scroll_top = scroll_position.y() * layout.line_height;
@@ -334,7 +339,7 @@ impl EditorElement {
selection.paint(bounds, cx.scene);
}
if view.cursors_visible() {
if view.show_local_cursors() || *replica_id != local_replica_id {
let cursor_position = selection.end;
if (start_row..end_row).contains(&cursor_position.row()) {
let cursor_row_layout =
@@ -374,6 +379,176 @@ impl EditorElement {
cx.scene.pop_layer();
}
fn max_line_number_width(&self, snapshot: &Snapshot, cx: &LayoutContext) -> f32 {
let digit_count = (snapshot.buffer_row_count() as f32).log10().floor() as usize + 1;
cx.text_layout_cache
.layout_str(
"1".repeat(digit_count).as_str(),
self.style.text.font_size,
&[(
digit_count,
RunStyle {
font_id: self.style.text.font_id,
color: Color::black(),
underline: false,
},
)],
)
.width()
}
fn layout_line_numbers(
&self,
rows: Range<u32>,
active_rows: &BTreeMap<u32, bool>,
snapshot: &Snapshot,
cx: &LayoutContext,
) -> Vec<Option<text_layout::Line>> {
let mut layouts = Vec::with_capacity(rows.len());
let mut line_number = String::new();
for (ix, (buffer_row, soft_wrapped)) in snapshot
.buffer_rows(rows.start)
.take((rows.end - rows.start) as usize)
.enumerate()
{
let display_row = rows.start + ix as u32;
let color = if active_rows.contains_key(&display_row) {
self.style.line_number_active
} else {
self.style.line_number
};
if soft_wrapped {
layouts.push(None);
} else {
line_number.clear();
write!(&mut line_number, "{}", buffer_row + 1).unwrap();
layouts.push(Some(cx.text_layout_cache.layout_str(
&line_number,
self.style.text.font_size,
&[(
line_number.len(),
RunStyle {
font_id: self.style.text.font_id,
color,
underline: false,
},
)],
)));
}
}
layouts
}
fn layout_lines(
&mut self,
mut rows: Range<u32>,
snapshot: &mut Snapshot,
cx: &LayoutContext,
) -> Vec<text_layout::Line> {
rows.end = cmp::min(rows.end, snapshot.max_point().row() + 1);
if rows.start >= rows.end {
return Vec::new();
}
// When the editor is empty and unfocused, then show the placeholder.
if snapshot.is_empty() && !snapshot.is_focused() {
let placeholder_style = self.style.placeholder_text();
let placeholder_text = snapshot.placeholder_text();
let placeholder_lines = placeholder_text
.as_ref()
.map_or("", AsRef::as_ref)
.split('\n')
.skip(rows.start as usize)
.take(rows.len());
return placeholder_lines
.map(|line| {
cx.text_layout_cache.layout_str(
line,
placeholder_style.font_size,
&[(
line.len(),
RunStyle {
font_id: placeholder_style.font_id,
color: placeholder_style.color,
underline: false,
},
)],
)
})
.collect();
}
let mut prev_font_properties = self.style.text.font_properties.clone();
let mut prev_font_id = self.style.text.font_id;
let theme = snapshot.theme().clone();
let mut layouts = Vec::with_capacity(rows.len());
let mut line = String::new();
let mut styles = Vec::new();
let mut row = rows.start;
let mut line_exceeded_max_len = false;
let chunks = snapshot.highlighted_chunks_for_rows(rows.clone());
'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) {
for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
if ix > 0 {
layouts.push(cx.text_layout_cache.layout_str(
&line,
self.style.text.font_size,
&styles,
));
line.clear();
styles.clear();
row += 1;
line_exceeded_max_len = false;
if row == rows.end {
break 'outer;
}
}
if !line_chunk.is_empty() && !line_exceeded_max_len {
let style = theme
.syntax
.highlight_style(style_ix)
.unwrap_or(self.style.text.clone().into());
// Avoid a lookup if the font properties match the previous ones.
let font_id = if style.font_properties == prev_font_properties {
prev_font_id
} else {
cx.font_cache
.select_font(self.style.text.font_family_id, &style.font_properties)
.unwrap_or(self.style.text.font_id)
};
if line.len() + line_chunk.len() > MAX_LINE_LEN {
let mut chunk_len = MAX_LINE_LEN - line.len();
while !line_chunk.is_char_boundary(chunk_len) {
chunk_len -= 1;
}
line_chunk = &line_chunk[..chunk_len];
line_exceeded_max_len = true;
}
line.push_str(line_chunk);
styles.push((
line_chunk.len(),
RunStyle {
font_id,
color: style.color,
underline: style.underline,
},
));
prev_font_id = font_id;
prev_font_properties = style.font_properties;
}
}
}
layouts
}
}
impl Element for EditorElement {
@@ -390,30 +565,22 @@ impl Element for EditorElement {
unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
}
let font_cache = &cx.font_cache;
let layout_cache = &cx.text_layout_cache;
let snapshot = self.snapshot(cx.app);
let line_height = snapshot.line_height(font_cache);
let line_height = self.style.text.line_height(cx.font_cache);
let gutter_padding;
let gutter_width;
if snapshot.mode == EditorMode::Full {
gutter_padding = snapshot.em_width(cx.font_cache);
match snapshot.max_line_number_width(cx.font_cache, cx.text_layout_cache) {
Err(error) => {
log::error!("error computing max line number width: {}", error);
return (size, None);
}
Ok(width) => gutter_width = width + gutter_padding * 2.0,
}
gutter_padding = self.style.text.em_width(cx.font_cache);
gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
} else {
gutter_padding = 0.0;
gutter_width = 0.0
};
let text_width = size.x() - gutter_width;
let text_offset = vec2f(-snapshot.font_descent(cx.font_cache), 0.);
let em_width = snapshot.em_width(font_cache);
let text_offset = vec2f(-self.style.text.descent(cx.font_cache), 0.);
let em_width = self.style.text.em_width(cx.font_cache);
let overscroll = vec2f(em_width, 0.);
let wrap_width = text_width - text_offset.x() - overscroll.x() - em_width;
let snapshot = self.update_view(cx.app, |view, cx| {
@@ -488,51 +655,18 @@ impl Element for EditorElement {
});
let line_number_layouts = if snapshot.mode == EditorMode::Full {
let settings = self
.view
.upgrade(cx.app)
.unwrap()
.read(cx.app)
.settings
.borrow();
match snapshot.layout_line_numbers(
start_row..end_row,
&active_rows,
cx.font_cache,
cx.text_layout_cache,
&settings.theme,
) {
Err(error) => {
log::error!("error laying out line numbers: {}", error);
return (size, None);
}
Ok(layouts) => layouts,
}
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx)
} else {
Vec::new()
};
let mut max_visible_line_width = 0.0;
let line_layouts = match snapshot.layout_lines(
start_row..end_row,
&self.style,
font_cache,
layout_cache,
) {
Err(error) => {
log::error!("error laying out lines: {}", error);
return (size, None);
let line_layouts = self.layout_lines(start_row..end_row, &mut snapshot, cx);
for line in &line_layouts {
if line.width() > max_visible_line_width {
max_visible_line_width = line.width();
}
Ok(layouts) => {
for line in &layouts {
if line.width() > max_visible_line_width {
max_visible_line_width = line.width();
}
}
layouts
}
};
}
let mut layout = LayoutState {
size,
@@ -542,6 +676,7 @@ impl Element for EditorElement {
overscroll,
text_offset,
snapshot,
style: self.style.clone(),
active_rows,
line_layouts,
line_number_layouts,
@@ -551,15 +686,18 @@ impl Element for EditorElement {
max_visible_line_width,
};
let scroll_max = layout.scroll_max(cx.font_cache, cx.text_layout_cache).x();
let scroll_width = layout.scroll_width(cx.text_layout_cache);
let max_glyph_width = self.style.text.em_width(&cx.font_cache);
self.update_view(cx.app, |view, cx| {
let clamped = view.clamp_scroll_left(layout.scroll_max(font_cache, layout_cache).x());
let clamped = view.clamp_scroll_left(scroll_max);
let autoscrolled;
if autoscroll_horizontally {
autoscrolled = view.autoscroll_horizontally(
start_row,
layout.text_size.x(),
layout.scroll_width(font_cache, layout_cache),
layout.snapshot.em_width(font_cache),
scroll_width,
max_glyph_width,
&layout.line_layouts,
cx,
);
@@ -659,6 +797,7 @@ pub struct LayoutState {
gutter_size: Vector2F,
gutter_padding: f32,
text_size: Vector2F,
style: EditorStyle,
snapshot: Snapshot,
active_rows: BTreeMap<u32, bool>,
line_layouts: Vec<text_layout::Line>,
@@ -672,20 +811,16 @@ pub struct LayoutState {
}
impl LayoutState {
fn scroll_width(&self, font_cache: &FontCache, layout_cache: &TextLayoutCache) -> f32 {
fn scroll_width(&self, layout_cache: &TextLayoutCache) -> f32 {
let row = self.snapshot.longest_row();
let longest_line_width = self
.snapshot
.layout_line(row, font_cache, layout_cache)
.unwrap()
.width();
let longest_line_width = self.layout_line(row, &self.snapshot, layout_cache).width();
longest_line_width.max(self.max_visible_line_width) + self.overscroll.x()
}
fn scroll_max(&self, font_cache: &FontCache, layout_cache: &TextLayoutCache) -> Vector2F {
let text_width = self.text_size.x();
let scroll_width = self.scroll_width(font_cache, layout_cache);
let em_width = self.snapshot.em_width(font_cache);
let scroll_width = self.scroll_width(layout_cache);
let em_width = self.style.text.em_width(font_cache);
let max_row = self.snapshot.max_point().row();
vec2f(
@@ -693,6 +828,36 @@ impl LayoutState {
max_row.saturating_sub(1) as f32,
)
}
pub fn layout_line(
&self,
row: u32,
snapshot: &Snapshot,
layout_cache: &TextLayoutCache,
) -> text_layout::Line {
let mut line = snapshot.line(row);
if line.len() > MAX_LINE_LEN {
let mut len = MAX_LINE_LEN;
while !line.is_char_boundary(len) {
len -= 1;
}
line.truncate(len);
}
layout_cache.layout_str(
&line,
self.style.text.font_size,
&[(
snapshot.line_len(row) as usize,
RunStyle {
font_id: self.style.text.font_id,
color: Color::black(),
underline: false,
},
)],
)
}
}
pub struct PaintState {
@@ -864,3 +1029,42 @@ fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 {
fn scale_horizontal_mouse_autoscroll_delta(delta: f32) -> f32 {
delta.powf(1.2) / 300.0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
editor::{Buffer, Editor, EditorStyle},
settings,
test::sample_text,
};
#[gpui::test]
fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
let font_cache = cx.font_cache().clone();
let settings = settings::test(&cx).1;
let style = EditorStyle::test(&font_cache);
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(
buffer,
settings.clone(),
{
let style = style.clone();
move |_| style.clone()
},
cx,
)
});
let element = EditorElement::new(editor.downgrade(), style);
let layouts = editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let mut presenter = cx.build_presenter(window_id, 30.);
let mut layout_cx = presenter.build_layout_context(false, cx);
element.layout_line_numbers(0..6, &Default::default(), &snapshot, &mut layout_cx)
});
assert_eq!(layouts.len(), 6);
}
}
+11 -6
View File
@@ -180,16 +180,21 @@ fn char_kind(c: char) -> CharKind {
#[cfg(test)]
mod tests {
use super::*;
use crate::{
editor::{display_map::DisplayMap, Buffer},
test::test_app_state,
};
use crate::editor::{display_map::DisplayMap, Buffer};
#[gpui::test]
fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
let settings = test_app_state(cx).settings.clone();
let tab_size = 4;
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let buffer = cx.add_model(|cx| Buffer::new(0, "a bcΔ defγ", cx));
let display_map = cx.add_model(|cx| DisplayMap::new(buffer, settings, None, cx));
let display_map =
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)).unwrap(),
+12 -8
View File
@@ -88,13 +88,13 @@ impl View for FileFinder {
Flex::new(Axis::Vertical)
.with_child(
Container::new(ChildView::new(self.query_editor.id()).boxed())
.with_style(&settings.theme.selector.input_editor.container)
.with_style(settings.theme.selector.input_editor.container)
.boxed(),
)
.with_child(Flexible::new(1.0, self.render_matches()).boxed())
.boxed(),
)
.with_style(&settings.theme.selector.container)
.with_style(settings.theme.selector.container)
.boxed(),
)
.with_max_width(500.0)
@@ -127,7 +127,7 @@ impl FileFinder {
)
.boxed(),
)
.with_style(&settings.theme.selector.empty.container)
.with_style(settings.theme.selector.empty.container)
.named("empty matches");
}
@@ -200,7 +200,7 @@ impl FileFinder {
)
.boxed(),
)
.with_style(&style.container);
.with_style(style.container);
let action = Select(Entry {
worktree_id: path_match.tree_id,
@@ -275,10 +275,14 @@ impl FileFinder {
cx.observe(&workspace, Self::workspace_updated).detach();
let query_editor = cx.add_view(|cx| {
Editor::single_line(settings.clone(), cx).with_style({
let settings = settings.clone();
move |_| settings.borrow().theme.selector.input_editor.as_editor()
})
Editor::single_line(
settings.clone(),
{
let settings = settings.clone();
move |_| settings.borrow().theme.selector.input_editor.as_editor()
},
cx,
)
});
cx.subscribe(&query_editor, Self::on_query_editor_event)
.detach();
+26
View File
@@ -0,0 +1,26 @@
pub use anyhow::{anyhow, Result};
use futures::future::BoxFuture;
use std::sync::Arc;
pub use surf::{
http::{Method, Response as ServerResponse},
Request, Response, Url,
};
pub trait HttpClient: Send + Sync {
fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>>;
}
pub fn client() -> Arc<dyn HttpClient> {
Arc::new(surf::client())
}
impl HttpClient for surf::Client {
fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>> {
Box::pin(async move {
Ok(self
.send(req)
.await
.map_err(|e| anyhow!("http request failed: {}", e))?)
})
}
}
+2
View File
@@ -5,6 +5,7 @@ pub mod editor;
pub mod file_finder;
pub mod fs;
mod fuzzy;
pub mod http;
pub mod language;
pub mod menus;
pub mod project_browser;
@@ -42,6 +43,7 @@ pub struct AppState {
pub languages: Arc<language::LanguageRegistry>,
pub themes: Arc<settings::ThemeRegistry>,
pub rpc: Arc<rpc::Client>,
pub user_store: Arc<user::UserStore>,
pub fs: Arc<dyn fs::Fs>,
pub channel_list: ModelHandle<ChannelList>,
}
+6 -3
View File
@@ -13,7 +13,7 @@ use zed::{
channel::ChannelList,
chat_panel, editor, file_finder,
fs::RealFs,
language, menus, rpc, settings, theme_selector,
http, language, menus, rpc, settings, theme_selector,
user::UserStore,
workspace::{self, OpenNew, OpenParams, OpenPaths},
AppState,
@@ -37,14 +37,16 @@ fn main() {
app.run(move |cx| {
let rpc = rpc::Client::new();
let user_store = Arc::new(UserStore::new(rpc.clone()));
let http = http::client();
let user_store = UserStore::new(rpc.clone(), http.clone(), cx.background());
let app_state = Arc::new(AppState {
languages: languages.clone(),
settings_tx: Arc::new(Mutex::new(settings_tx)),
settings,
themes,
channel_list: cx.add_model(|cx| ChannelList::new(user_store, rpc.clone(), cx)),
channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), rpc.clone(), cx)),
rpc,
user_store,
fs: Arc::new(RealFs),
});
@@ -88,6 +90,7 @@ fn init_logger() {
.expect("could not open logfile");
simplelog::WriteLogger::init(level, simplelog::Config::default(), log_file)
.expect("could not initialize logger");
log_panics::init();
}
}
+205 -99
View File
@@ -1,6 +1,10 @@
use crate::util::ResultExt;
use anyhow::{anyhow, Context, Result};
use async_tungstenite::tungstenite::http::Request;
use async_recursion::async_recursion;
use async_tungstenite::tungstenite::{
error::Error as WebsocketError,
http::{Request, StatusCode},
};
use gpui::{AsyncAppContext, Entity, ModelContext, Task};
use lazy_static::lazy_static;
use parking_lot::RwLock;
@@ -15,10 +19,11 @@ use std::{
time::{Duration, Instant},
};
use surf::Url;
use thiserror::Error;
pub use zrpc::{proto, ConnectionId, PeerId, TypedEnvelope};
use zrpc::{
proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage},
Conn, Peer, Receipt,
Connection, Peer, Receipt,
};
lazy_static! {
@@ -29,37 +34,65 @@ lazy_static! {
pub struct Client {
peer: Arc<Peer>,
state: RwLock<ClientState>,
auth_callback: Option<
Box<dyn 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<(u64, String)>>>,
>,
connect_callback: Option<
Box<dyn 'static + Send + Sync + Fn(u64, &str, &AsyncAppContext) -> Task<Result<Conn>>>,
authenticate:
Option<Box<dyn 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<Credentials>>>>,
establish_connection: Option<
Box<
dyn 'static
+ Send
+ Sync
+ Fn(
&Credentials,
&AsyncAppContext,
) -> Task<Result<Connection, EstablishConnectionError>>,
>,
>,
}
#[derive(Error, Debug)]
pub enum EstablishConnectionError {
#[error("unauthorized")]
Unauthorized,
#[error("{0}")]
Other(#[from] anyhow::Error),
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Http(#[from] async_tungstenite::tungstenite::http::Error),
}
impl From<WebsocketError> for EstablishConnectionError {
fn from(error: WebsocketError) -> Self {
if let WebsocketError::Http(response) = &error {
if response.status() == StatusCode::UNAUTHORIZED {
return EstablishConnectionError::Unauthorized;
}
}
EstablishConnectionError::Other(error.into())
}
}
impl EstablishConnectionError {
pub fn other(error: impl Into<anyhow::Error> + Send + Sync) -> Self {
Self::Other(error.into())
}
}
#[derive(Copy, Clone, Debug)]
pub enum Status {
Disconnected,
SignedOut,
Authenticating,
Connecting {
user_id: u64,
},
Connecting,
ConnectionError,
Connected {
connection_id: ConnectionId,
user_id: u64,
},
Connected { connection_id: ConnectionId },
ConnectionLost,
Reauthenticating,
Reconnecting {
user_id: u64,
},
ReconnectionError {
next_reconnection: Instant,
},
Reconnecting,
ReconnectionError { next_reconnection: Instant },
}
struct ClientState {
credentials: Option<Credentials>,
status: (watch::Sender<Status>, watch::Receiver<Status>),
entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>,
model_handlers: HashMap<
@@ -70,10 +103,17 @@ struct ClientState {
heartbeat_interval: Duration,
}
#[derive(Clone)]
pub struct Credentials {
pub user_id: u64,
pub access_token: String,
}
impl Default for ClientState {
fn default() -> Self {
Self {
status: watch::channel_with(Status::Disconnected),
credentials: None,
status: watch::channel_with(Status::SignedOut),
entity_id_extractors: Default::default(),
model_handlers: Default::default(),
_maintain_connection: None,
@@ -107,22 +147,38 @@ impl Client {
Arc::new(Self {
peer: Peer::new(),
state: Default::default(),
auth_callback: None,
connect_callback: None,
authenticate: None,
establish_connection: None,
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_login_and_connect_callbacks<Login, Connect>(
&mut self,
login: Login,
connect: Connect,
) where
Login: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<(u64, String)>>,
Connect: 'static + Send + Sync + Fn(u64, &str, &AsyncAppContext) -> Task<Result<Conn>>,
pub fn override_authenticate<F>(&mut self, authenticate: F) -> &mut Self
where
F: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<Credentials>>,
{
self.auth_callback = Some(Box::new(login));
self.connect_callback = Some(Box::new(connect));
self.authenticate = Some(Box::new(authenticate));
self
}
#[cfg(any(test, feature = "test-support"))]
pub fn override_establish_connection<F>(&mut self, connect: F) -> &mut Self
where
F: 'static
+ Send
+ Sync
+ Fn(&Credentials, &AsyncAppContext) -> Task<Result<Connection, EstablishConnectionError>>,
{
self.establish_connection = Some(Box::new(connect));
self
}
pub fn user_id(&self) -> Option<u64> {
self.state
.read()
.credentials
.as_ref()
.map(|credentials| credentials.user_id)
}
pub fn status(&self) -> watch::Receiver<Status> {
@@ -167,7 +223,7 @@ impl Client {
}
}));
}
Status::Disconnected => {
Status::SignedOut => {
state._maintain_connection.take();
}
_ => {}
@@ -227,12 +283,13 @@ impl Client {
}
}
#[async_recursion(?Send)]
pub async fn authenticate_and_connect(
self: &Arc<Self>,
cx: &AsyncAppContext,
) -> anyhow::Result<()> {
let was_disconnected = match *self.status().borrow() {
Status::Disconnected => true,
Status::SignedOut => true,
Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
false
}
@@ -249,33 +306,60 @@ impl Client {
self.set_status(Status::Reauthenticating, cx)
}
let (user_id, access_token) = match self.authenticate(&cx).await {
Ok(result) => result,
Err(err) => {
self.set_status(Status::ConnectionError, cx);
return Err(err);
}
let mut read_from_keychain = false;
let credentials = self.state.read().credentials.clone();
let credentials = if let Some(credentials) = credentials {
credentials
} else if let Some(credentials) = read_credentials_from_keychain(cx) {
read_from_keychain = true;
credentials
} else {
let credentials = match self.authenticate(&cx).await {
Ok(credentials) => credentials,
Err(err) => {
self.set_status(Status::ConnectionError, cx);
return Err(err);
}
};
credentials
};
if was_disconnected {
self.set_status(Status::Connecting { user_id }, cx);
self.set_status(Status::Connecting, cx);
} else {
self.set_status(Status::Reconnecting { user_id }, cx);
self.set_status(Status::Reconnecting, cx);
}
match self.connect(user_id, &access_token, cx).await {
match self.establish_connection(&credentials, cx).await {
Ok(conn) => {
log::info!("connected to rpc address {}", *ZED_SERVER_URL);
self.set_connection(user_id, conn, cx).await;
self.state.write().credentials = Some(credentials.clone());
if !read_from_keychain {
write_credentials_to_keychain(&credentials, cx).log_err();
}
self.set_connection(conn, cx).await;
Ok(())
}
Err(err) => {
self.set_status(Status::ConnectionError, cx);
Err(err)
if matches!(err, EstablishConnectionError::Unauthorized) {
self.state.write().credentials.take();
cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
if read_from_keychain {
self.set_status(Status::SignedOut, cx);
self.authenticate_and_connect(cx).await
} else {
self.set_status(Status::ConnectionError, cx);
Err(err)?
}
} else {
self.set_status(Status::ConnectionError, cx);
Err(err)?
}
}
}
}
async fn set_connection(self: &Arc<Self>, user_id: u64, conn: Conn, cx: &AsyncAppContext) {
async fn set_connection(self: &Arc<Self>, conn: Connection, cx: &AsyncAppContext) {
let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn).await;
cx.foreground()
.spawn({
@@ -287,11 +371,11 @@ impl Client {
if let Some(extract_entity_id) =
state.entity_id_extractors.get(&message.payload_type_id())
{
let payload_type_id = message.payload_type_id();
let entity_id = (extract_entity_id)(message.as_ref());
if let Some(handler) = state
.model_handlers
.get_mut(&(message.payload_type_id(), entity_id))
{
let handler_key = (payload_type_id, entity_id);
if let Some(mut handler) = state.model_handlers.remove(&handler_key) {
drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
let start_time = Instant::now();
log::info!("RPC client message {}", message.payload_type_name());
(handler)(message, &mut cx);
@@ -299,6 +383,10 @@ impl Client {
"RPC message handled. duration:{:?}",
start_time.elapsed()
);
this.state
.write()
.model_handlers
.insert(handler_key, handler);
} else {
log::info!("unhandled message {}", message.payload_type_name());
}
@@ -310,13 +398,7 @@ impl Client {
})
.detach();
self.set_status(
Status::Connected {
connection_id,
user_id,
},
cx,
);
self.set_status(Status::Connected { connection_id }, cx);
let handle_io = cx.background().spawn(handle_io);
let this = self.clone();
@@ -324,7 +406,7 @@ impl Client {
cx.foreground()
.spawn(async move {
match handle_io.await {
Ok(()) => this.set_status(Status::Disconnected, &cx),
Ok(()) => this.set_status(Status::SignedOut, &cx),
Err(err) => {
log::error!("connection error: {:?}", err);
this.set_status(Status::ConnectionLost, &cx);
@@ -334,52 +416,49 @@ impl Client {
.detach();
}
fn authenticate(self: &Arc<Self>, cx: &AsyncAppContext) -> Task<Result<(u64, String)>> {
if let Some(callback) = self.auth_callback.as_ref() {
fn authenticate(self: &Arc<Self>, cx: &AsyncAppContext) -> Task<Result<Credentials>> {
if let Some(callback) = self.authenticate.as_ref() {
callback(cx)
} else {
self.authenticate_with_browser(cx)
}
}
fn connect(
fn establish_connection(
self: &Arc<Self>,
user_id: u64,
access_token: &str,
credentials: &Credentials,
cx: &AsyncAppContext,
) -> Task<Result<Conn>> {
if let Some(callback) = self.connect_callback.as_ref() {
callback(user_id, access_token, cx)
) -> Task<Result<Connection, EstablishConnectionError>> {
if let Some(callback) = self.establish_connection.as_ref() {
callback(credentials, cx)
} else {
self.connect_with_websocket(user_id, access_token, cx)
self.establish_websocket_connection(credentials, cx)
}
}
fn connect_with_websocket(
fn establish_websocket_connection(
self: &Arc<Self>,
user_id: u64,
access_token: &str,
credentials: &Credentials,
cx: &AsyncAppContext,
) -> Task<Result<Conn>> {
let request =
Request::builder().header("Authorization", format!("{} {}", user_id, access_token));
) -> Task<Result<Connection, EstablishConnectionError>> {
let request = Request::builder().header(
"Authorization",
format!("{} {}", credentials.user_id, credentials.access_token),
);
cx.background().spawn(async move {
if let Some(host) = ZED_SERVER_URL.strip_prefix("https://") {
let stream = smol::net::TcpStream::connect(host).await?;
let request = request.uri(format!("wss://{}/rpc", host)).body(())?;
let (stream, _) = async_tungstenite::async_tls::client_async_tls(request, stream)
.await
.context("websocket handshake")?;
Ok(Conn::new(stream))
let (stream, _) =
async_tungstenite::async_tls::client_async_tls(request, stream).await?;
Ok(Connection::new(stream))
} else if let Some(host) = ZED_SERVER_URL.strip_prefix("http://") {
let stream = smol::net::TcpStream::connect(host).await?;
let request = request.uri(format!("ws://{}/rpc", host)).body(())?;
let (stream, _) = async_tungstenite::client_async(request, stream)
.await
.context("websocket handshake")?;
Ok(Conn::new(stream))
let (stream, _) = async_tungstenite::client_async(request, stream).await?;
Ok(Connection::new(stream))
} else {
Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL))
Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL))?
}
})
}
@@ -387,19 +466,10 @@ impl Client {
pub fn authenticate_with_browser(
self: &Arc<Self>,
cx: &AsyncAppContext,
) -> Task<Result<(u64, String)>> {
) -> Task<Result<Credentials>> {
let platform = cx.platform();
let executor = cx.background();
executor.clone().spawn(async move {
if let Some((user_id, access_token)) = platform
.read_credentials(&ZED_SERVER_URL)
.log_err()
.flatten()
{
log::info!("already signed in. user_id: {}", user_id);
return Ok((user_id.parse()?, String::from_utf8(access_token).unwrap()));
}
// Generate a pair of asymmetric encryption keys. The public key will be used by the
// zed server to encrypt the user's access token, so that it can'be intercepted by
// any other app running on the user's device.
@@ -460,17 +530,18 @@ impl Client {
.decrypt_string(&access_token)
.context("failed to decrypt access token")?;
platform.activate(true);
platform
.write_credentials(&ZED_SERVER_URL, &user_id, access_token.as_bytes())
.log_err();
Ok((user_id.parse()?, access_token))
Ok(Credentials {
user_id: user_id.parse()?,
access_token,
})
})
}
pub async fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
let conn_id = self.connection_id()?;
self.peer.disconnect(conn_id).await;
self.set_status(Status::Disconnected, cx);
self.set_status(Status::SignedOut, cx);
Ok(())
}
@@ -499,6 +570,26 @@ impl Client {
}
}
fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
let (user_id, access_token) = cx
.platform()
.read_credentials(&ZED_SERVER_URL)
.log_err()
.flatten()?;
Some(Credentials {
user_id: user_id.parse().ok()?,
access_token: String::from_utf8(access_token).ok()?,
})
}
fn write_credentials_to_keychain(credentials: &Credentials, cx: &AsyncAppContext) -> Result<()> {
cx.platform().write_credentials(
&ZED_SERVER_URL,
&credentials.user_id.to_string(),
credentials.access_token.as_bytes(),
)
}
const WORKTREE_URL_PREFIX: &'static str = "zed://worktrees/";
pub fn encode_worktree_url(id: u64, access_token: &str) -> String {
@@ -561,6 +652,7 @@ mod tests {
status.recv().await,
Some(Status::Connected { .. })
));
assert_eq!(server.auth_count(), 1);
server.forbid_connections();
server.disconnect().await;
@@ -569,6 +661,20 @@ mod tests {
server.allow_connections();
cx.foreground().advance_clock(Duration::from_secs(10));
while !matches!(status.recv().await, Some(Status::Connected { .. })) {}
assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting
server.forbid_connections();
server.disconnect().await;
while !matches!(status.recv().await, Some(Status::ReconnectionError { .. })) {}
// Clear cached credentials after authentication fails
server.roll_access_token();
server.allow_connections();
cx.foreground().advance_clock(Duration::from_secs(10));
assert_eq!(server.auth_count(), 1);
cx.foreground().advance_clock(Duration::from_secs(10));
while !matches!(status.recv().await, Some(Status::Connected { .. })) {}
assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token
}
#[test]
+98 -33
View File
@@ -2,28 +2,31 @@ use crate::{
assets::Assets,
channel::ChannelList,
fs::RealFs,
http::{HttpClient, Request, Response, ServerResponse},
language::LanguageRegistry,
rpc::{self, Client},
rpc::{self, Client, Credentials, EstablishConnectionError},
settings::{self, ThemeRegistry},
time::ReplicaId,
user::UserStore,
AppState,
};
use anyhow::{anyhow, Result};
use futures::{future::BoxFuture, Future};
use gpui::{AsyncAppContext, Entity, ModelHandle, MutableAppContext, TestAppContext};
use parking_lot::Mutex;
use postage::{mpsc, prelude::Stream as _};
use smol::channel;
use std::{
fmt,
marker::PhantomData,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
Arc,
},
};
use tempdir::TempDir;
use zrpc::{proto, Conn, ConnectionId, Peer, Receipt, TypedEnvelope};
use zrpc::{proto, Connection, ConnectionId, Peer, Receipt, TypedEnvelope};
#[cfg(test)]
#[ctor::ctor]
@@ -164,14 +167,16 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
let languages = Arc::new(LanguageRegistry::new());
let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
let rpc = rpc::Client::new();
let user_store = Arc::new(UserStore::new(rpc.clone()));
let http = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
let user_store = UserStore::new(rpc.clone(), http, cx.background());
Arc::new(AppState {
settings_tx: Arc::new(Mutex::new(settings_tx)),
settings,
themes,
languages: languages.clone(),
channel_list: cx.add_model(|cx| ChannelList::new(user_store, rpc.clone(), cx)),
channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), rpc.clone(), cx)),
rpc,
user_store,
fs: Arc::new(RealFs),
})
}
@@ -204,6 +209,9 @@ pub struct FakeServer {
incoming: Mutex<Option<mpsc::Receiver<Box<dyn proto::AnyTypedEnvelope>>>>,
connection_id: Mutex<Option<ConnectionId>>,
forbid_connections: AtomicBool,
auth_count: AtomicUsize,
access_token: AtomicUsize,
user_id: u64,
}
impl FakeServer {
@@ -212,40 +220,47 @@ impl FakeServer {
client: &mut Arc<Client>,
cx: &TestAppContext,
) -> Arc<Self> {
let result = Arc::new(Self {
let server = Arc::new(Self {
peer: Peer::new(),
incoming: Default::default(),
connection_id: Default::default(),
forbid_connections: Default::default(),
auth_count: Default::default(),
access_token: Default::default(),
user_id: client_user_id,
});
Arc::get_mut(client)
.unwrap()
.set_login_and_connect_callbacks(
.override_authenticate({
let server = server.clone();
move |cx| {
cx.spawn(|_| async move {
let access_token = "the-token".to_string();
Ok((client_user_id, access_token))
})
},
{
let server = result.clone();
move |user_id, access_token, cx| {
assert_eq!(user_id, client_user_id);
assert_eq!(access_token, "the-token");
cx.spawn({
let server = server.clone();
move |cx| async move { server.connect(&cx).await }
server.auth_count.fetch_add(1, SeqCst);
let access_token = server.access_token.load(SeqCst).to_string();
cx.spawn(move |_| async move {
Ok(Credentials {
user_id: client_user_id,
access_token,
})
}
},
);
})
}
})
.override_establish_connection({
let server = server.clone();
move |credentials, cx| {
let credentials = credentials.clone();
cx.spawn({
let server = server.clone();
move |cx| async move { server.establish_connection(&credentials, &cx).await }
})
}
});
client
.authenticate_and_connect(&cx.to_async())
.await
.unwrap();
result
server
}
pub async fn disconnect(&self) {
@@ -254,17 +269,37 @@ impl FakeServer {
self.incoming.lock().take();
}
async fn connect(&self, cx: &AsyncAppContext) -> Result<Conn> {
async fn establish_connection(
&self,
credentials: &Credentials,
cx: &AsyncAppContext,
) -> Result<Connection, EstablishConnectionError> {
assert_eq!(credentials.user_id, self.user_id);
if self.forbid_connections.load(SeqCst) {
Err(anyhow!("server is forbidding connections"))
} else {
let (client_conn, server_conn, _) = Conn::in_memory();
let (connection_id, io, incoming) = self.peer.add_connection(server_conn).await;
cx.background().spawn(io).detach();
*self.incoming.lock() = Some(incoming);
*self.connection_id.lock() = Some(connection_id);
Ok(client_conn)
Err(EstablishConnectionError::Other(anyhow!(
"server is forbidding connections"
)))?
}
if credentials.access_token != self.access_token.load(SeqCst).to_string() {
Err(EstablishConnectionError::Unauthorized)?
}
let (client_conn, server_conn, _) = Connection::in_memory();
let (connection_id, io, incoming) = self.peer.add_connection(server_conn).await;
cx.background().spawn(io).detach();
*self.incoming.lock() = Some(incoming);
*self.connection_id.lock() = Some(connection_id);
Ok(client_conn)
}
pub fn auth_count(&self) -> usize {
self.auth_count.load(SeqCst)
}
pub fn roll_access_token(&self) {
self.access_token.fetch_add(1, SeqCst);
}
pub fn forbid_connections(&self) {
@@ -312,3 +347,33 @@ impl FakeServer {
self.connection_id.lock().expect("not connected")
}
}
pub struct FakeHttpClient {
handler:
Box<dyn 'static + Send + Sync + Fn(Request) -> BoxFuture<'static, Result<ServerResponse>>>,
}
impl FakeHttpClient {
pub fn new<Fut, F>(handler: F) -> Arc<dyn HttpClient>
where
Fut: 'static + Send + Future<Output = Result<ServerResponse>>,
F: 'static + Send + Sync + Fn(Request) -> Fut,
{
Arc::new(Self {
handler: Box::new(move |req| Box::pin(handler(req))),
})
}
}
impl fmt::Debug for FakeHttpClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FakeHttpClient").finish()
}
}
impl HttpClient for FakeHttpClient {
fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>> {
let future = (self.handler)(req);
Box::pin(async move { future.await.map(Into::into) })
}
}
+32 -49
View File
@@ -1,10 +1,12 @@
mod highlight_map;
mod resolution;
mod theme_registry;
use crate::editor::{EditorStyle, SelectionStyle};
use anyhow::Result;
use gpui::{
color::Color,
elements::{ContainerStyle, LabelStyle},
elements::{ContainerStyle, ImageStyle, LabelStyle},
fonts::{HighlightStyle, TextStyle},
Border,
};
@@ -34,7 +36,7 @@ pub struct SyntaxTheme {
#[derive(Deserialize)]
pub struct Workspace {
pub background: Color,
pub titlebar: ContainedLabel,
pub titlebar: Titlebar,
pub tab: Tab,
pub active_tab: Tab,
pub pane_divider: Border,
@@ -42,6 +44,24 @@ pub struct Workspace {
pub right_sidebar: Sidebar,
}
#[derive(Clone, Deserialize)]
pub struct Titlebar {
#[serde(flatten)]
pub container: ContainerStyle,
pub title: TextStyle,
pub avatar_width: f32,
pub offline_icon: OfflineIcon,
pub icon_color: Color,
pub avatar: ImageStyle,
}
#[derive(Clone, Deserialize)]
pub struct OfflineIcon {
#[serde(flatten)]
pub container: ContainerStyle,
pub width: f32,
}
#[derive(Clone, Deserialize)]
pub struct Tab {
#[serde(flatten)]
@@ -60,6 +80,7 @@ pub struct Tab {
pub struct Sidebar {
#[serde(flatten)]
pub container: ContainerStyle,
pub width: f32,
pub icon: SidebarIcon,
pub active_icon: SidebarIcon,
pub resize_handle: ContainerStyle,
@@ -76,6 +97,7 @@ pub struct ChatPanel {
#[serde(flatten)]
pub container: ContainerStyle,
pub message: ChatMessage,
pub pending_message: ChatMessage,
pub channel_select: ChannelSelect,
pub input_editor: InputEditorStyle,
pub sign_in_prompt: TextStyle,
@@ -137,35 +159,16 @@ pub struct ContainedLabel {
pub label: LabelStyle,
}
#[derive(Clone, Deserialize)]
pub struct EditorStyle {
pub text: HighlightStyle,
#[serde(default)]
pub placeholder_text: HighlightStyle,
pub background: Color,
pub selection: SelectionStyle,
pub gutter_background: Color,
pub active_line_background: Color,
pub line_number: Color,
pub line_number_active: Color,
pub guest_selections: Vec<SelectionStyle>,
}
#[derive(Clone, Deserialize)]
pub struct InputEditorStyle {
#[serde(flatten)]
pub container: ContainerStyle,
pub text: HighlightStyle,
pub placeholder_text: HighlightStyle,
pub text: TextStyle,
#[serde(default)]
pub placeholder_text: Option<TextStyle>,
pub selection: SelectionStyle,
}
#[derive(Clone, Copy, Default, Deserialize)]
pub struct SelectionStyle {
pub cursor: Color,
pub selection: Color,
}
impl SyntaxTheme {
pub fn new(highlights: Vec<(String, HighlightStyle)>) -> Self {
Self { highlights }
@@ -183,30 +186,6 @@ impl SyntaxTheme {
}
}
impl Default for EditorStyle {
fn default() -> Self {
Self {
text: HighlightStyle {
color: Color::from_u32(0xff0000ff),
font_properties: Default::default(),
underline: false,
},
placeholder_text: HighlightStyle {
color: Color::from_u32(0x00ff00ff),
font_properties: Default::default(),
underline: false,
},
background: Default::default(),
gutter_background: Default::default(),
active_line_background: Default::default(),
line_number: Default::default(),
line_number_active: Default::default(),
selection: Default::default(),
guest_selections: Default::default(),
}
}
}
impl InputEditorStyle {
pub fn as_editor(&self) -> EditorStyle {
EditorStyle {
@@ -217,7 +196,11 @@ impl InputEditorStyle {
.background_color
.unwrap_or(Color::transparent_black()),
selection: self.selection,
..Default::default()
gutter_background: Default::default(),
active_line_background: Default::default(),
line_number: Default::default(),
line_number_active: Default::default(),
guest_selections: Default::default(),
}
}
}
+476
View File
@@ -0,0 +1,476 @@
use anyhow::{anyhow, Result};
use indexmap::IndexMap;
use serde_json::Value;
use std::{
cell::RefCell,
mem,
rc::{Rc, Weak},
};
pub fn resolve_references(value: Value) -> Result<Value> {
let tree = Tree::from_json(value)?;
tree.resolve()?;
tree.to_json()
}
#[derive(Clone)]
enum Node {
Reference {
path: String,
parent: Option<Weak<RefCell<Node>>>,
},
Object {
base: Option<String>,
children: IndexMap<String, Tree>,
resolved: bool,
parent: Option<Weak<RefCell<Node>>>,
},
Array {
children: Vec<Tree>,
resolved: bool,
parent: Option<Weak<RefCell<Node>>>,
},
String {
value: String,
parent: Option<Weak<RefCell<Node>>>,
},
Number {
value: serde_json::Number,
parent: Option<Weak<RefCell<Node>>>,
},
Bool {
value: bool,
parent: Option<Weak<RefCell<Node>>>,
},
Null {
parent: Option<Weak<RefCell<Node>>>,
},
}
#[derive(Clone)]
struct Tree(Rc<RefCell<Node>>);
impl Tree {
pub fn new(node: Node) -> Self {
Self(Rc::new(RefCell::new(node)))
}
fn from_json(value: Value) -> Result<Self> {
match value {
Value::String(value) => {
if let Some(path) = value.strip_prefix("$") {
Ok(Self::new(Node::Reference {
path: path.to_string(),
parent: None,
}))
} else {
Ok(Self::new(Node::String {
value,
parent: None,
}))
}
}
Value::Number(value) => Ok(Self::new(Node::Number {
value,
parent: None,
})),
Value::Bool(value) => Ok(Self::new(Node::Bool {
value,
parent: None,
})),
Value::Null => Ok(Self::new(Node::Null { parent: None })),
Value::Object(object) => {
let tree = Self::new(Node::Object {
base: Default::default(),
children: Default::default(),
resolved: false,
parent: None,
});
let mut children = IndexMap::new();
let mut resolved = true;
let mut base = None;
for (key, value) in object.into_iter() {
let value = if key == "extends" {
if value.is_string() {
if let Value::String(value) = value {
base = value.strip_prefix("$").map(str::to_string);
resolved = false;
Self::new(Node::String {
value,
parent: None,
})
} else {
unreachable!()
}
} else {
Tree::from_json(value)?
}
} else {
Tree::from_json(value)?
};
value
.0
.borrow_mut()
.set_parent(Some(Rc::downgrade(&tree.0)));
resolved &= value.is_resolved();
children.insert(key.clone(), value);
}
*tree.0.borrow_mut() = Node::Object {
base,
children,
resolved,
parent: None,
};
Ok(tree)
}
Value::Array(elements) => {
let tree = Self::new(Node::Array {
children: Default::default(),
resolved: false,
parent: None,
});
let mut children = Vec::new();
let mut resolved = true;
for element in elements {
let child = Tree::from_json(element)?;
child
.0
.borrow_mut()
.set_parent(Some(Rc::downgrade(&tree.0)));
resolved &= child.is_resolved();
children.push(child);
}
*tree.0.borrow_mut() = Node::Array {
children,
resolved,
parent: None,
};
Ok(tree)
}
}
}
fn to_json(&self) -> Result<Value> {
match &*self.0.borrow() {
Node::Reference { .. } => Err(anyhow!("unresolved tree")),
Node::String { value, .. } => Ok(Value::String(value.clone())),
Node::Number { value, .. } => Ok(Value::Number(value.clone())),
Node::Bool { value, .. } => Ok(Value::Bool(*value)),
Node::Null { .. } => Ok(Value::Null),
Node::Object { children, .. } => {
let mut json_children = serde_json::Map::new();
for (key, value) in children {
json_children.insert(key.clone(), value.to_json()?);
}
Ok(Value::Object(json_children))
}
Node::Array { children, .. } => {
let mut json_children = Vec::new();
for child in children {
json_children.push(child.to_json()?);
}
Ok(Value::Array(json_children))
}
}
}
fn parent(&self) -> Option<Tree> {
match &*self.0.borrow() {
Node::Reference { parent, .. }
| Node::Object { parent, .. }
| Node::Array { parent, .. }
| Node::String { parent, .. }
| Node::Number { parent, .. }
| Node::Bool { parent, .. }
| Node::Null { parent } => parent.as_ref().and_then(|p| p.upgrade()).map(Tree),
}
}
fn get(&self, path: &str) -> Result<Option<Tree>> {
let mut tree = self.clone();
for component in path.split('.') {
let node = tree.0.borrow();
match &*node {
Node::Object { children, .. } => {
if let Some(subtree) = children.get(component).cloned() {
drop(node);
tree = subtree;
} else {
return Err(anyhow!(
"key \"{}\" does not exist in path \"{}\"",
component,
path
));
}
}
Node::Reference { .. } => return Ok(None),
Node::Array { .. }
| Node::String { .. }
| Node::Number { .. }
| Node::Bool { .. }
| Node::Null { .. } => {
return Err(anyhow!(
"key \"{}\" in path \"{}\" is not an object",
component,
path
))
}
}
}
Ok(Some(tree))
}
fn is_resolved(&self) -> bool {
match &*self.0.borrow() {
Node::Reference { .. } => false,
Node::Object { resolved, .. } | Node::Array { resolved, .. } => *resolved,
Node::String { .. } | Node::Number { .. } | Node::Bool { .. } | Node::Null { .. } => {
true
}
}
}
fn update_resolved(&self) {
match &mut *self.0.borrow_mut() {
Node::Object {
resolved, children, ..
} => {
*resolved = children.values().all(|c| c.is_resolved());
}
Node::Array {
resolved, children, ..
} => {
*resolved = children.iter().all(|c| c.is_resolved());
}
_ => {}
}
}
pub fn resolve(&self) -> Result<()> {
let mut unresolved = vec![self.clone()];
let mut made_progress = true;
while made_progress && !unresolved.is_empty() {
made_progress = false;
for mut tree in mem::take(&mut unresolved) {
made_progress |= tree.resolve_subtree(self, &mut unresolved)?;
if tree.is_resolved() {
while let Some(parent) = tree.parent() {
parent.update_resolved();
tree = parent;
}
}
}
}
if unresolved.is_empty() {
Ok(())
} else {
Err(anyhow!("tree contains cycles"))
}
}
fn resolve_subtree(&self, root: &Tree, unresolved: &mut Vec<Tree>) -> Result<bool> {
let node = self.0.borrow();
match &*node {
Node::Reference { path, parent } => {
if let Some(subtree) = root.get(&path)? {
if subtree.is_resolved() {
let parent = parent.clone();
drop(node);
let mut new_node = subtree.0.borrow().clone();
new_node.set_parent(parent);
*self.0.borrow_mut() = new_node;
Ok(true)
} else {
unresolved.push(self.clone());
Ok(false)
}
} else {
unresolved.push(self.clone());
Ok(false)
}
}
Node::Object {
base,
children,
resolved,
..
} => {
if *resolved {
Ok(false)
} else {
let mut made_progress = false;
let mut children_resolved = true;
for child in children.values() {
made_progress |= child.resolve_subtree(root, unresolved)?;
children_resolved &= child.is_resolved();
}
if children_resolved {
let mut has_base = false;
let mut resolved_base = None;
if let Some(base) = base {
has_base = true;
if let Some(base) = root.get(base)? {
if base.is_resolved() {
resolved_base = Some(base);
}
}
}
drop(node);
if let Some(base) = resolved_base.as_ref() {
self.extend_from(&base);
made_progress = true;
}
if let Node::Object { resolved, .. } = &mut *self.0.borrow_mut() {
if has_base {
if resolved_base.is_some() {
*resolved = true;
} else {
unresolved.push(self.clone());
}
} else {
*resolved = true;
}
}
}
Ok(made_progress)
}
}
Node::Array {
children, resolved, ..
} => {
if *resolved {
Ok(false)
} else {
let mut made_progress = false;
let mut children_resolved = true;
for child in children.iter() {
made_progress |= child.resolve_subtree(root, unresolved)?;
children_resolved &= child.is_resolved();
}
if children_resolved {
drop(node);
if let Node::Array { resolved, .. } = &mut *self.0.borrow_mut() {
*resolved = true;
}
}
Ok(made_progress)
}
}
Node::String { .. } | Node::Number { .. } | Node::Bool { .. } | Node::Null { .. } => {
Ok(false)
}
}
}
fn extend_from(&self, base: &Tree) {
if Rc::ptr_eq(&self.0, &base.0) {
return;
}
if let (
Node::Object { children, .. },
Node::Object {
children: base_children,
..
},
) = (&mut *self.0.borrow_mut(), &*base.0.borrow())
{
for (key, base_value) in base_children {
if let Some(value) = children.get(key) {
value.extend_from(base_value);
} else {
let base_value = base_value.clone();
base_value
.0
.borrow_mut()
.set_parent(Some(Rc::downgrade(&self.0)));
children.insert(key.clone(), base_value);
}
}
}
}
}
impl Node {
fn set_parent(&mut self, new_parent: Option<Weak<RefCell<Node>>>) {
match self {
Node::Reference { parent, .. }
| Node::Object { parent, .. }
| Node::Array { parent, .. }
| Node::String { parent, .. }
| Node::Number { parent, .. }
| Node::Bool { parent, .. }
| Node::Null { parent } => *parent = new_parent,
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_references() {
let json = serde_json::json!({
"a": {
"x": "$b.d"
},
"b": {
"c": "$a",
"d": "$e.f"
},
"e": {
"extends": "$a",
"f": "1"
}
});
assert_eq!(
resolve_references(json).unwrap(),
serde_json::json!({
"a": {
"x": "1"
},
"b": {
"c": {
"x": "1"
},
"d": "1"
},
"e": {
"extends": "$a",
"f": "1",
"x": "1"
},
})
)
}
#[test]
fn test_cycles() {
let json = serde_json::json!({
"a": {
"b": "$c.d"
},
"c": {
"d": "$a.b",
},
});
assert!(resolve_references(json).is_err());
}
}
+34 -508
View File
@@ -1,8 +1,9 @@
use anyhow::{anyhow, Context, Result};
use super::resolution::resolve_references;
use anyhow::{Context, Result};
use gpui::{fonts, AssetSource, FontCache};
use parking_lot::Mutex;
use serde_json::{Map, Value};
use std::{collections::HashMap, fmt, mem, sync::Arc};
use std::{collections::HashMap, sync::Arc};
use super::Theme;
@@ -13,30 +14,6 @@ pub struct ThemeRegistry {
font_cache: Arc<FontCache>,
}
#[derive(Default)]
struct KeyPathReferenceSet {
references: Vec<KeyPathReference>,
reference_ids_by_source: Vec<usize>,
reference_ids_by_target: Vec<usize>,
dependencies: Vec<(usize, usize)>,
dependency_counts: Vec<usize>,
}
#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
struct KeyPathReference {
target: KeyPath,
source: KeyPath,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct KeyPath(Vec<Key>);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Key {
Array(usize),
Object(String),
}
impl ThemeRegistry {
pub fn new(source: impl AssetSource, font_cache: Arc<FontCache>) -> Arc<Self> {
Arc::new(Self {
@@ -111,41 +88,15 @@ impl ThemeRegistry {
}
}
let mut theme_data = Value::Object(theme_data);
// Find all of the key path references in the object, and then sort them according
// to their dependencies.
if evaluate_references {
let mut key_path = KeyPath::default();
let mut references = KeyPathReferenceSet::default();
for (key, value) in theme_data.iter() {
key_path.0.push(Key::Object(key.clone()));
find_references(value, &mut key_path, &mut references);
key_path.0.pop();
}
let sorted_references = references
.top_sort()
.map_err(|key_paths| anyhow!("cycle for key paths: {:?}", key_paths))?;
// Now update objects to include the fields of objects they extend
for KeyPathReference { source, target } in sorted_references {
if let Some(source) = value_at(&mut theme_data, &source).cloned() {
let target = value_at(&mut theme_data, &target).unwrap();
if let Value::Object(target_object) = target.take() {
if let Value::Object(mut source_object) = source {
deep_merge_json(&mut source_object, target_object);
*target = Value::Object(source_object);
} else {
Err(anyhow!("extended key path {} is not an object", source))?;
}
} else {
*target = source;
}
} else {
Err(anyhow!("invalid key path '{}'", source))?;
}
}
theme_data = resolve_references(theme_data)?;
}
let result = Arc::new(Value::Object(theme_data));
let result = Arc::new(theme_data);
self.theme_data
.lock()
.insert(name.to_string(), result.clone());
@@ -154,293 +105,6 @@ impl ThemeRegistry {
}
}
impl KeyPathReferenceSet {
fn insert(&mut self, reference: KeyPathReference) {
let id = self.references.len();
let source_ix = self
.reference_ids_by_source
.binary_search_by_key(&&reference.source, |id| &self.references[*id].source)
.unwrap_or_else(|i| i);
let target_ix = self
.reference_ids_by_target
.binary_search_by_key(&&reference.target, |id| &self.references[*id].target)
.unwrap_or_else(|i| i);
self.populate_dependencies(id, &reference);
self.reference_ids_by_source.insert(source_ix, id);
self.reference_ids_by_target.insert(target_ix, id);
self.references.push(reference);
}
fn top_sort(mut self) -> Result<Vec<KeyPathReference>, Vec<KeyPath>> {
let mut results = Vec::with_capacity(self.references.len());
let mut root_ids = Vec::with_capacity(self.references.len());
// Find the initial set of references that have no dependencies.
for (id, dep_count) in self.dependency_counts.iter().enumerate() {
if *dep_count == 0 {
root_ids.push(id);
}
}
while results.len() < root_ids.len() {
// Just to guarantee a stable result when the inputs are randomized,
// sort references lexicographically in absence of any dependency relationship.
root_ids[results.len()..].sort_by_key(|id| &self.references[*id]);
let root_id = root_ids[results.len()];
let root = mem::take(&mut self.references[root_id]);
results.push(root);
// Remove this reference as a dependency from any of its dependent references.
if let Ok(dep_ix) = self
.dependencies
.binary_search_by_key(&root_id, |edge| edge.0)
{
let mut first_dep_ix = dep_ix;
let mut last_dep_ix = dep_ix + 1;
while first_dep_ix > 0 && self.dependencies[first_dep_ix - 1].0 == root_id {
first_dep_ix -= 1;
}
while last_dep_ix < self.dependencies.len()
&& self.dependencies[last_dep_ix].0 == root_id
{
last_dep_ix += 1;
}
// If any reference no longer has any dependencies, then then mark it as a root.
// Preserve the references' original order where possible.
for (_, successor_id) in self.dependencies.drain(first_dep_ix..last_dep_ix) {
self.dependency_counts[successor_id] -= 1;
if self.dependency_counts[successor_id] == 0 {
root_ids.push(successor_id);
}
}
}
}
// If any references never became roots, then there are reference cycles
// in the set. Return an error containing all of the key paths that are
// directly involved in cycles.
if results.len() < self.references.len() {
let mut cycle_ref_ids = (0..self.references.len())
.filter(|id| !root_ids.contains(id))
.collect::<Vec<_>>();
// Iteratively remove any references that have no dependencies,
// so that the error will only indicate which key paths are directly
// involved in the cycles.
let mut done = false;
while !done {
done = true;
cycle_ref_ids.retain(|id| {
if self.dependencies.iter().any(|dep| dep.0 == *id) {
true
} else {
done = false;
self.dependencies.retain(|dep| dep.1 != *id);
false
}
});
}
let mut cycle_key_paths = Vec::new();
for id in cycle_ref_ids {
let reference = &self.references[id];
cycle_key_paths.push(reference.target.clone());
cycle_key_paths.push(reference.source.clone());
}
cycle_key_paths.sort_unstable();
return Err(cycle_key_paths);
}
Ok(results)
}
fn populate_dependencies(&mut self, new_id: usize, new_reference: &KeyPathReference) {
self.dependency_counts.push(0);
// If an existing reference's source path starts with the new reference's
// target path, then insert this new reference before that existing reference.
for id in Self::reference_ids_for_key_path(
&new_reference.target.0,
&self.references,
&self.reference_ids_by_source,
KeyPathReference::source,
KeyPath::starts_with,
) {
Self::add_dependency(
(new_id, id),
&mut self.dependencies,
&mut self.dependency_counts,
);
}
// If an existing reference's target path starts with the new reference's
// source path, then insert this new reference after that existing reference.
for id in Self::reference_ids_for_key_path(
&new_reference.source.0,
&self.references,
&self.reference_ids_by_target,
KeyPathReference::target,
KeyPath::starts_with,
) {
Self::add_dependency(
(id, new_id),
&mut self.dependencies,
&mut self.dependency_counts,
);
}
// If an existing reference's source path is a prefix of the new reference's
// target path, then insert this new reference before that existing reference.
for prefix in new_reference.target.prefixes() {
for id in Self::reference_ids_for_key_path(
prefix,
&self.references,
&self.reference_ids_by_source,
KeyPathReference::source,
PartialEq::eq,
) {
Self::add_dependency(
(new_id, id),
&mut self.dependencies,
&mut self.dependency_counts,
);
}
}
// If an existing reference's target path is a prefix of the new reference's
// source path, then insert this new reference after that existing reference.
for prefix in new_reference.source.prefixes() {
for id in Self::reference_ids_for_key_path(
prefix,
&self.references,
&self.reference_ids_by_target,
KeyPathReference::target,
PartialEq::eq,
) {
Self::add_dependency(
(id, new_id),
&mut self.dependencies,
&mut self.dependency_counts,
);
}
}
}
// Find all existing references that satisfy a given predicate with respect
// to a given key path. Use a sorted array of reference ids in order to avoid
// performing unnecessary comparisons.
fn reference_ids_for_key_path<'a>(
key_path: &[Key],
references: &[KeyPathReference],
sorted_reference_ids: &'a [usize],
reference_attribute: impl Fn(&KeyPathReference) -> &KeyPath,
predicate: impl Fn(&KeyPath, &[Key]) -> bool,
) -> impl Iterator<Item = usize> + 'a {
let ix = sorted_reference_ids
.binary_search_by_key(&key_path, |id| &reference_attribute(&references[*id]).0)
.unwrap_or_else(|i| i);
let mut start_ix = ix;
while start_ix > 0 {
let reference_id = sorted_reference_ids[start_ix - 1];
let reference = &references[reference_id];
if !predicate(&reference_attribute(reference), key_path) {
break;
}
start_ix -= 1;
}
let mut end_ix = ix;
while end_ix < sorted_reference_ids.len() {
let reference_id = sorted_reference_ids[end_ix];
let reference = &references[reference_id];
if !predicate(&reference_attribute(reference), key_path) {
break;
}
end_ix += 1;
}
sorted_reference_ids[start_ix..end_ix].iter().copied()
}
fn add_dependency(
(predecessor, successor): (usize, usize),
dependencies: &mut Vec<(usize, usize)>,
dependency_counts: &mut Vec<usize>,
) {
let dependency = (predecessor, successor);
if let Err(i) = dependencies.binary_search(&dependency) {
dependencies.insert(i, dependency);
}
dependency_counts[successor] += 1;
}
}
impl KeyPathReference {
fn source(&self) -> &KeyPath {
&self.source
}
fn target(&self) -> &KeyPath {
&self.target
}
}
impl KeyPath {
fn new(string: &str) -> Self {
Self(
string
.split(".")
.map(|key| Key::Object(key.to_string()))
.collect(),
)
}
fn starts_with(&self, other: &[Key]) -> bool {
self.0.starts_with(&other)
}
fn prefixes(&self) -> impl Iterator<Item = &[Key]> {
(1..self.0.len()).map(move |end_ix| &self.0[0..end_ix])
}
}
impl PartialEq<[Key]> for KeyPath {
fn eq(&self, other: &[Key]) -> bool {
self.0.eq(other)
}
}
impl fmt::Debug for KeyPathReference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"KeyPathReference {{ {} <- {} }}",
self.target, self.source
)
}
}
impl fmt::Display for KeyPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, key) in self.0.iter().enumerate() {
match key {
Key::Array(index) => write!(f, "[{}]", index)?,
Key::Object(key) => {
if i > 0 {
".".fmt(f)?;
}
key.fmt(f)?;
}
}
}
Ok(())
}
}
fn deep_merge_json(base: &mut Map<String, Value>, extension: Map<String, Value>) {
for (key, extension_value) in extension {
if let Value::Object(extension_object) = extension_value {
@@ -455,69 +119,12 @@ fn deep_merge_json(base: &mut Map<String, Value>, extension: Map<String, Value>)
}
}
fn find_references(value: &Value, key_path: &mut KeyPath, references: &mut KeyPathReferenceSet) {
match value {
Value::Array(vec) => {
for (ix, value) in vec.iter().enumerate() {
key_path.0.push(Key::Array(ix));
find_references(value, key_path, references);
key_path.0.pop();
}
}
Value::Object(map) => {
for (key, value) in map.iter() {
if key == "extends" {
if let Some(source_path) = value.as_str().and_then(|s| s.strip_prefix("$")) {
references.insert(KeyPathReference {
source: KeyPath::new(source_path),
target: key_path.clone(),
});
}
} else {
key_path.0.push(Key::Object(key.to_string()));
find_references(value, key_path, references);
key_path.0.pop();
}
}
}
Value::String(string) => {
if let Some(source_path) = string.strip_prefix("$") {
references.insert(KeyPathReference {
source: KeyPath::new(source_path),
target: key_path.clone(),
});
}
}
_ => {}
}
}
fn value_at<'a>(object: &'a mut Map<String, Value>, key_path: &KeyPath) -> Option<&'a mut Value> {
let mut key_path = key_path.0.iter();
if let Some(Key::Object(first_key)) = key_path.next() {
let mut cur_value = object.get_mut(first_key);
for key in key_path {
if let Some(value) = cur_value {
match key {
Key::Array(ix) => cur_value = value.get_mut(ix),
Key::Object(key) => cur_value = value.get_mut(key),
}
} else {
return None;
}
}
cur_value
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{test::test_app_state, theme::DEFAULT_THEME_NAME};
use anyhow::anyhow;
use gpui::MutableAppContext;
use rand::{prelude::StdRng, Rng};
#[gpui::test]
fn test_bundled_themes(cx: &mut MutableAppContext) {
@@ -575,6 +182,7 @@ mod tests {
let registry = ThemeRegistry::new(assets, cx.font_cache().clone());
let theme_data = registry.load("light", true).unwrap();
assert_eq!(
theme_data.as_ref(),
&serde_json::json!({
@@ -619,120 +227,38 @@ mod tests {
);
}
#[test]
fn test_key_path_reference_set_simple() {
let input_references = build_refs(&[
("r", "a"),
("a.b.c", "d"),
("d.e", "f"),
("t.u", "v"),
("v.w", "x"),
("v.y", "x"),
("d.h", "i"),
("v.z", "x"),
("f.g", "d.h"),
]);
let expected_references = build_refs(&[
("d.h", "i"),
("f.g", "d.h"),
("d.e", "f"),
("a.b.c", "d"),
("r", "a"),
("v.w", "x"),
("v.y", "x"),
("v.z", "x"),
("t.u", "v"),
])
.collect::<Vec<_>>();
#[gpui::test]
fn test_nested_extension(cx: &mut MutableAppContext) {
let assets = TestAssets(&[(
"themes/theme.toml",
r##"
[a]
text = { extends = "$text.0" }
let mut reference_set = KeyPathReferenceSet::default();
for reference in input_references {
reference_set.insert(reference);
}
assert_eq!(reference_set.top_sort().unwrap(), expected_references);
}
[b]
extends = "$a"
text = { extends = "$text.1" }
#[test]
fn test_key_path_reference_set_with_cycles() {
let input_references = build_refs(&[
("x", "a.b"),
("y", "x.c"),
("a.b.c", "d.e"),
("d.e.f", "g.h"),
("g.h.i", "a"),
]);
let mut reference_set = KeyPathReferenceSet::default();
for reference in input_references {
reference_set.insert(reference);
}
[text]
0 = { color = "red" }
1 = { color = "blue" }
"##,
)]);
let registry = ThemeRegistry::new(assets, cx.font_cache().clone());
let theme_data = registry.load("theme", true).unwrap();
assert_eq!(
reference_set.top_sort().unwrap_err(),
&[
KeyPath::new("a"),
KeyPath::new("a.b.c"),
KeyPath::new("d.e"),
KeyPath::new("d.e.f"),
KeyPath::new("g.h"),
KeyPath::new("g.h.i"),
]
theme_data
.get("b")
.unwrap()
.get("text")
.unwrap()
.get("color")
.unwrap(),
"blue"
);
}
#[gpui::test(iterations = 20)]
async fn test_key_path_reference_set_random(mut rng: StdRng) {
let examples: &[&[_]] = &[
&[
("n.d.h", "i"),
("f.g", "n.d.h"),
("n.d.e", "f"),
("a.b.c", "n.d"),
("r", "a"),
("q.q.q", "r.s"),
("r.t", "q"),
("x.x", "r.r"),
("v.w", "x"),
("v.y", "x"),
("v.z", "x"),
("t.u", "v"),
],
&[
("w.x.y.z", "t.u.z"),
("x", "w.x"),
("a.b.c1", "x.b1.c"),
("a.b.c2", "x.b2.c"),
],
&[
("x.y", "m.n.n.o.q"),
("x.y.z", "m.n.n.o.p"),
("u.v.w", "x.y.z"),
("a.b.c.d", "u.v"),
("a.b.c.d.e", "u.v"),
("a.b.c.d.f", "u.v"),
("a.b.c.d.g", "u.v"),
],
];
for example in examples {
let expected_references = build_refs(example).collect::<Vec<_>>();
let mut input_references = expected_references.clone();
input_references.sort_by_key(|_| rng.gen_range(0..1000));
let mut reference_set = KeyPathReferenceSet::default();
for reference in input_references {
reference_set.insert(reference);
}
assert_eq!(reference_set.top_sort().unwrap(), expected_references);
}
}
fn build_refs<'a>(rows: &'a [(&str, &str)]) -> impl Iterator<Item = KeyPathReference> + 'a {
rows.iter().map(|(target, source)| KeyPathReference {
target: KeyPath::new(target),
source: KeyPath::new(source),
})
}
struct TestAssets(&'static [(&'static str, &'static str)]);
impl AssetSource for TestAssets {
+12 -8
View File
@@ -58,10 +58,14 @@ impl ThemeSelector {
cx: &mut ViewContext<Self>,
) -> Self {
let query_editor = cx.add_view(|cx| {
Editor::single_line(settings.clone(), cx).with_style({
let settings = settings.clone();
move |_| settings.borrow().theme.selector.input_editor.as_editor()
})
Editor::single_line(
settings.clone(),
{
let settings = settings.clone();
move |_| settings.borrow().theme.selector.input_editor.as_editor()
},
cx,
)
});
cx.subscribe(&query_editor, Self::on_query_editor_event)
@@ -214,7 +218,7 @@ impl ThemeSelector {
)
.boxed(),
)
.with_style(&settings.theme.selector.empty.container)
.with_style(settings.theme.selector.empty.container)
.named("empty matches");
}
@@ -259,9 +263,9 @@ impl ThemeSelector {
.boxed(),
)
.with_style(if index == self.selected_index {
&theme.selector.active_item.container
theme.selector.active_item.container
} else {
&theme.selector.item.container
theme.selector.item.container
});
container.boxed()
@@ -288,7 +292,7 @@ impl View for ThemeSelector {
.with_child(Flexible::new(1.0, self.render_matches(cx)).boxed())
.boxed(),
)
.with_style(&settings.theme.selector.container)
.with_style(settings.theme.selector.container)
.boxed(),
)
.with_max_width(600.0)
+112 -22
View File
@@ -1,22 +1,77 @@
use crate::rpc::Client;
use anyhow::{anyhow, Result};
use crate::{
http::{HttpClient, Method, Request, Url},
rpc::{Client, Status},
util::TryFutureExt,
};
use anyhow::{anyhow, Context, Result};
use futures::future;
use gpui::{executor, ImageData, Task};
use parking_lot::Mutex;
use std::{collections::HashMap, sync::Arc};
use postage::{oneshot, prelude::Stream, sink::Sink, watch};
use std::{
collections::HashMap,
sync::{Arc, Weak},
};
use zrpc::proto;
pub use proto::User;
#[derive(Debug)]
pub struct User {
pub id: u64,
pub github_login: String,
pub avatar: Option<Arc<ImageData>>,
}
pub struct UserStore {
users: Mutex<HashMap<u64, Arc<User>>>,
current_user: watch::Receiver<Option<Arc<User>>>,
rpc: Arc<Client>,
http: Arc<dyn HttpClient>,
_maintain_current_user: Task<()>,
}
impl UserStore {
pub fn new(rpc: Arc<Client>) -> Self {
Self {
pub fn new(
rpc: Arc<Client>,
http: Arc<dyn HttpClient>,
executor: &executor::Background,
) -> Arc<Self> {
let (mut current_user_tx, current_user_rx) = watch::channel();
let (mut this_tx, mut this_rx) = oneshot::channel::<Weak<Self>>();
let this = Arc::new(Self {
users: Default::default(),
rpc,
}
current_user: current_user_rx,
rpc: rpc.clone(),
http,
_maintain_current_user: executor.spawn(async move {
let this = if let Some(this) = this_rx.recv().await {
this
} else {
return;
};
let mut status = rpc.status();
while let Some(status) = status.recv().await {
match status {
Status::Connected { .. } => {
if let Some((this, user_id)) = this.upgrade().zip(rpc.user_id()) {
current_user_tx
.send(this.fetch_user(user_id).log_err().await)
.await
.ok();
}
}
Status::SignedOut => {
current_user_tx.send(None).await.ok();
}
_ => {}
}
}
}),
});
let weak = Arc::downgrade(&this);
executor
.spawn(async move { this_tx.send(weak).await })
.detach();
this
}
pub async fn load_users(&self, mut user_ids: Vec<u64>) -> Result<()> {
@@ -27,8 +82,15 @@ impl UserStore {
if !user_ids.is_empty() {
let response = self.rpc.request(proto::GetUsers { user_ids }).await?;
let new_users = future::join_all(
response
.users
.into_iter()
.map(|user| User::new(user, self.http.as_ref())),
)
.await;
let mut users = self.users.lock();
for user in response.users {
for user in new_users {
users.insert(user.id, Arc::new(user));
}
}
@@ -36,24 +98,52 @@ impl UserStore {
Ok(())
}
pub async fn get_user(&self, user_id: u64) -> Result<Arc<User>> {
pub async fn fetch_user(&self, user_id: u64) -> Result<Arc<User>> {
if let Some(user) = self.users.lock().get(&user_id).cloned() {
return Ok(user);
}
let response = self
.rpc
.request(proto::GetUsers {
user_ids: vec![user_id],
})
.await?;
self.load_users(vec![user_id]).await?;
self.users
.lock()
.get(&user_id)
.cloned()
.ok_or_else(|| anyhow!("server responded with no users"))
}
if let Some(user) = response.users.into_iter().next() {
let user = Arc::new(user);
self.users.lock().insert(user_id, user.clone());
Ok(user)
} else {
Err(anyhow!("server responded with no users"))
pub fn current_user(&self) -> Option<Arc<User>> {
self.current_user.borrow().clone()
}
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
self.current_user.clone()
}
}
impl User {
async fn new(message: proto::User, http: &dyn HttpClient) -> Self {
User {
id: message.id,
github_login: message.github_login,
avatar: fetch_avatar(http, &message.avatar_url).log_err().await,
}
}
}
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
let url = Url::parse(url).with_context(|| format!("failed to parse avatar url {:?}", url))?;
let mut request = Request::new(Method::Get, url);
request.middleware(surf::middleware::Redirect::default());
let mut response = http
.send(request)
.await
.map_err(|e| anyhow!("failed to send user avatar request: {}", e))?;
let bytes = response
.body_bytes()
.await
.map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?;
let format = image::guess_format(&bytes)?;
let image = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
Ok(ImageData::new(image))
}
+115 -17
View File
@@ -10,8 +10,9 @@ use crate::{
project_browser::ProjectBrowser,
rpc,
settings::Settings,
user,
worktree::{File, Worktree},
AppState,
AppState, Authenticate,
};
use anyhow::{anyhow, Result};
use gpui::{
@@ -20,7 +21,7 @@ use gpui::{
geometry::{rect::RectF, vector::vec2f},
json::to_string_pretty,
keymap::Binding,
platform::WindowOptions,
platform::{CursorStyle, WindowOptions},
AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext,
PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
WeakModelHandle,
@@ -28,9 +29,8 @@ use gpui::{
use log::error;
pub use pane::*;
pub use pane_group::*;
use postage::watch;
use postage::{prelude::Stream, watch};
use sidebar::{Side, Sidebar, ToggleSidebarItem};
use smol::prelude::*;
use std::{
collections::{hash_map::Entry, HashMap, HashSet},
future::Future,
@@ -341,6 +341,7 @@ pub struct Workspace {
pub settings: watch::Receiver<Settings>,
languages: Arc<LanguageRegistry>,
rpc: Arc<rpc::Client>,
user_store: Arc<user::UserStore>,
fs: Arc<dyn Fs>,
modal: Option<AnyViewHandle>,
center: PaneGroup,
@@ -354,6 +355,7 @@ pub struct Workspace {
(usize, Arc<Path>),
postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
>,
_observe_current_user: Task<()>,
}
impl Workspace {
@@ -387,6 +389,23 @@ impl Workspace {
);
right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into());
let mut current_user = app_state.user_store.watch_current_user().clone();
let mut connection_status = app_state.rpc.status().clone();
let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
current_user.recv().await;
connection_status.recv().await;
let mut stream =
Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
while stream.recv().await.is_some() {
cx.update(|cx| {
if let Some(this) = this.upgrade(&cx) {
this.update(cx, |_, cx| cx.notify());
}
})
}
});
Workspace {
modal: None,
center: PaneGroup::new(pane.id()),
@@ -395,12 +414,14 @@ impl Workspace {
settings: app_state.settings.clone(),
languages: app_state.languages.clone(),
rpc: app_state.rpc.clone(),
user_store: app_state.user_store.clone(),
fs: app_state.fs.clone(),
left_sidebar,
right_sidebar,
worktrees: Default::default(),
items: Default::default(),
loading_items: Default::default(),
_observe_current_user,
}
}
@@ -625,7 +646,7 @@ impl Workspace {
if let Some(load_result) = watch.borrow().as_ref() {
break load_result.clone();
}
watch.next().await;
watch.recv().await;
};
this.update(&mut cx, |this, cx| {
@@ -936,6 +957,66 @@ impl Workspace {
pub fn active_pane(&self) -> &ViewHandle<Pane> {
&self.active_pane
}
fn render_connection_status(&self) -> Option<ElementBox> {
let theme = &self.settings.borrow().theme;
match &*self.rpc.status().borrow() {
rpc::Status::ConnectionError
| rpc::Status::ConnectionLost
| rpc::Status::Reauthenticating
| rpc::Status::Reconnecting { .. }
| rpc::Status::ReconnectionError { .. } => Some(
Container::new(
Align::new(
ConstrainedBox::new(
Svg::new("icons/offline-14.svg")
.with_color(theme.workspace.titlebar.icon_color)
.boxed(),
)
.with_width(theme.workspace.titlebar.offline_icon.width)
.boxed(),
)
.boxed(),
)
.with_style(theme.workspace.titlebar.offline_icon.container)
.boxed(),
),
_ => None,
}
}
fn render_avatar(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &self.settings.borrow().theme;
let avatar = if let Some(avatar) = self
.user_store
.current_user()
.and_then(|user| user.avatar.clone())
{
Image::new(avatar)
.with_style(theme.workspace.titlebar.avatar)
.boxed()
} else {
MouseEventHandler::new::<Authenticate, _, _, _>(0, cx, |_, _| {
Svg::new("icons/signed-out-12.svg")
.with_color(theme.workspace.titlebar.icon_color)
.boxed()
})
.on_click(|cx| cx.dispatch_action(Authenticate))
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
};
ConstrainedBox::new(
Align::new(
ConstrainedBox::new(avatar)
.with_width(theme.workspace.titlebar.avatar_width)
.boxed(),
)
.boxed(),
)
.with_width(theme.workspace.right_sidebar.width)
.boxed()
}
}
impl Entity for Workspace {
@@ -955,15 +1036,30 @@ impl View for Workspace {
.with_child(
ConstrainedBox::new(
Container::new(
Align::new(
Label::new(
"zed".into(),
theme.workspace.titlebar.label.clone()
).boxed()
)
.boxed()
Stack::new()
.with_child(
Align::new(
Label::new(
"zed".into(),
theme.workspace.titlebar.title.clone(),
)
.boxed(),
)
.boxed(),
)
.with_child(
Align::new(
Flex::row()
.with_children(self.render_connection_status())
.with_child(self.render_avatar(cx))
.boxed(),
)
.right()
.boxed(),
)
.boxed(),
)
.with_style(&theme.workspace.titlebar.container)
.with_style(theme.workspace.titlebar.container)
.boxed(),
)
.with_height(32.)
@@ -1380,7 +1476,7 @@ mod tests {
});
cx.simulate_new_path_selection(|parent_dir| {
assert_eq!(parent_dir, dir.path());
Some(parent_dir.join("the-new-name"))
Some(parent_dir.join("the-new-name.rs"))
});
cx.read(|cx| {
assert!(editor.is_dirty(cx));
@@ -1393,8 +1489,10 @@ mod tests {
.await;
cx.read(|cx| {
assert!(!editor.is_dirty(cx));
assert_eq!(editor.title(cx), "the-new-name");
assert_eq!(editor.title(cx), "the-new-name.rs");
});
// The language is assigned based on the path
editor.read_with(&cx, |editor, cx| assert!(editor.language(cx).is_some()));
// Edit the file and save it again. This time, there is no filename prompt.
editor.update(&mut cx, |editor, cx| {
@@ -1408,7 +1506,7 @@ mod tests {
editor
.condition(&cx, |editor, cx| !editor.is_dirty(cx))
.await;
cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name"));
cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name.rs"));
// Open the same newly-created file in another pane item. The new editor should reuse
// the same buffer.
@@ -1416,7 +1514,7 @@ mod tests {
workspace.open_new_file(&OpenNew(app_state.clone()), cx);
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
assert!(workspace
.open_entry((tree.id(), Path::new("the-new-name").into()), cx)
.open_entry((tree.id(), Path::new("the-new-name.rs").into()), cx)
.is_none());
});
let editor2 = workspace.update(&mut cx, |workspace, cx| {
+48 -37
View File
@@ -1,6 +1,14 @@
use super::{ItemViewHandle, SplitDirection};
use crate::settings::Settings;
use gpui::{Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle, action, color::Color, elements::*, geometry::{rect::RectF, vector::vec2f}, keymap::Binding, platform::CursorStyle};
use gpui::{
action,
color::Color,
elements::*,
geometry::{rect::RectF, vector::vec2f},
keymap::Binding,
platform::CursorStyle,
Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle,
};
use postage::watch;
use std::{cmp, path::Path, sync::Arc};
@@ -177,13 +185,13 @@ impl Pane {
theme.workspace.tab.label.text.font_size,
);
let mut row = Flex::row();
for (ix, item) in self.items.iter().enumerate() {
let is_active = ix == self.active_item;
enum Tabs {}
let tabs = MouseEventHandler::new::<Tabs, _, _, _>(0, cx, |mouse_state, cx| {
let mut row = Flex::row();
for (ix, item) in self.items.iter().enumerate() {
let is_active = ix == self.active_item;
enum Tab {}
row.add_child(
MouseEventHandler::new::<Tab, _, _, _>(item.id(), cx, |mouse_state, cx| {
row.add_child({
let mut title = item.title(cx);
if title.len() > MAX_TAB_TITLE_LEN {
let mut truncated_len = MAX_TAB_TITLE_LEN;
@@ -256,7 +264,7 @@ impl Pane {
)
.boxed(),
)
.with_style(&ContainerStyle {
.with_style(ContainerStyle {
margin: Margin {
left: style.spacing,
right: style.spacing,
@@ -268,7 +276,7 @@ impl Pane {
)
.with_child(
Align::new(
ConstrainedBox::new(if is_active || mouse_state.hovered {
ConstrainedBox::new(if mouse_state.hovered {
let item_id = item.id();
enum TabCloseButton {}
let icon = Svg::new("icons/x.svg");
@@ -283,7 +291,9 @@ impl Pane {
icon.with_color(style.icon_close).boxed()
}
},
).with_cursor_style(CursorStyle::PointingHand)
)
.with_padding(Padding::uniform(4.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(move |cx| {
cx.dispatch_action(CloseItem(item_id))
})
@@ -298,7 +308,7 @@ impl Pane {
)
.boxed(),
)
.with_style(&style.container)
.with_style(style.container)
.boxed(),
)
.on_mouse_down(move |cx| {
@@ -307,36 +317,37 @@ impl Pane {
})
.boxed()
})
.boxed(),
)
}
}
// 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.
let mut border = Border::bottom(1.0, Color::default());
border.color = theme.workspace.tab.container.border.color;
// 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.
let mut border = Border::bottom(1.0, Color::default());
border.color = theme.workspace.tab.container.border.color;
row.add_child(
ConstrainedBox::new(
Container::new(Empty::new().boxed())
.with_border(border)
.boxed(),
)
.with_min_width(20.)
.named("fixed-filler"),
);
row.add_child(
ConstrainedBox::new(
Container::new(Empty::new().boxed())
.with_border(border)
.boxed(),
)
.with_min_width(20.)
.named("fixed-filler"),
);
row.add_child(
Expanded::new(
0.0,
Container::new(Empty::new().boxed())
.with_border(border)
.boxed(),
)
.named("filler"),
);
row.add_child(
Expanded::new(
0.0,
Container::new(Empty::new().boxed())
.with_border(border)
.boxed(),
)
.named("filler"),
);
ConstrainedBox::new(row.boxed())
row.boxed()
});
ConstrainedBox::new(tabs.boxed())
.with_height(line_height + 16.)
.named("tabs")
}
+38 -28
View File
@@ -75,38 +75,48 @@ impl Sidebar {
);
let theme = self.theme(settings);
Container::new(
Flex::column()
.with_children(self.items.iter().enumerate().map(|(item_index, item)| {
let theme = if Some(item_index) == self.active_item_ix {
&theme.active_icon
} else {
&theme.icon
};
enum SidebarButton {}
MouseEventHandler::new::<SidebarButton, _, _, _>(item.view.id(), cx, |_, _| {
ConstrainedBox::new(
Align::new(
ConstrainedBox::new(
Container::new(
Flex::column()
.with_children(self.items.iter().enumerate().map(|(item_index, item)| {
let theme = if Some(item_index) == self.active_item_ix {
&theme.active_icon
} else {
&theme.icon
};
enum SidebarButton {}
MouseEventHandler::new::<SidebarButton, _, _, _>(
item.view.id(),
cx,
|_, _| {
ConstrainedBox::new(
Svg::new(item.icon_path).with_color(theme.color).boxed(),
Align::new(
ConstrainedBox::new(
Svg::new(item.icon_path)
.with_color(theme.color)
.boxed(),
)
.with_height(theme.height)
.boxed(),
)
.boxed(),
)
.with_height(theme.height)
.boxed(),
)
.boxed(),
.with_height(line_height + 16.0)
.boxed()
},
)
.with_height(line_height + 16.0)
.with_cursor_style(CursorStyle::PointingHand)
.on_mouse_down(move |cx| {
cx.dispatch_action(ToggleSidebarItem(ToggleArg { side, item_index }))
})
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_mouse_down(move |cx| {
cx.dispatch_action(ToggleSidebarItem(ToggleArg { side, item_index }))
})
.boxed()
}))
.boxed(),
}))
.boxed(),
)
.with_style(theme.container)
.boxed(),
)
.with_style(&theme.container)
.with_width(theme.width)
.boxed()
}
@@ -155,7 +165,7 @@ impl Sidebar {
let side = self.side;
MouseEventHandler::new::<Self, _, _, _>(self.side.id(), &mut cx, |_, _| {
Container::new(Empty::new().boxed())
.with_style(&self.theme(settings).resize_handle)
.with_style(self.theme(settings).resize_handle)
.boxed()
})
.with_padding(Padding {
+7
View File
@@ -268,6 +268,13 @@ impl Worktree {
}
}
pub fn languages(&self) -> &Arc<LanguageRegistry> {
match self {
Worktree::Local(worktree) => &worktree.languages,
Worktree::Remote(worktree) => &worktree.languages,
}
}
pub fn snapshot(&self) -> Snapshot {
match self {
Worktree::Local(worktree) => worktree.snapshot(),
+7
View File
@@ -151,6 +151,7 @@ message GetUsersResponse {
message SendChannelMessage {
uint64 channel_id = 1;
string body = 2;
Nonce nonce = 3;
}
message SendChannelMessageResponse {
@@ -296,6 +297,11 @@ message Range {
uint64 end = 2;
}
message Nonce {
uint64 upper_half = 1;
uint64 lower_half = 2;
}
message Channel {
uint64 id = 1;
string name = 2;
@@ -306,4 +312,5 @@ message ChannelMessage {
string body = 2;
uint64 timestamp = 3;
uint64 sender_id = 4;
Nonce nonce = 5;
}
+2 -2
View File
@@ -2,7 +2,7 @@ use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSock
use futures::{channel::mpsc, SinkExt as _, Stream, StreamExt as _};
use std::{io, task::Poll};
pub struct Conn {
pub struct Connection {
pub(crate) tx:
Box<dyn 'static + Send + Unpin + futures::Sink<WebSocketMessage, Error = WebSocketError>>,
pub(crate) rx: Box<
@@ -13,7 +13,7 @@ pub struct Conn {
>,
}
impl Conn {
impl Connection {
pub fn new<S>(stream: S) -> Self
where
S: 'static
+1 -1
View File
@@ -2,5 +2,5 @@ pub mod auth;
mod conn;
mod peer;
pub mod proto;
pub use conn::Conn;
pub use conn::Connection;
pub use peer::*;
+20 -20
View File
@@ -1,5 +1,5 @@
use super::proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, RequestMessage};
use super::Conn;
use super::Connection;
use anyhow::{anyhow, Context, Result};
use async_lock::{Mutex, RwLock};
use futures::FutureExt as _;
@@ -79,12 +79,12 @@ impl<T: RequestMessage> TypedEnvelope<T> {
}
pub struct Peer {
connections: RwLock<HashMap<ConnectionId, Connection>>,
connections: RwLock<HashMap<ConnectionId, ConnectionState>>,
next_connection_id: AtomicU32,
}
#[derive(Clone)]
struct Connection {
struct ConnectionState {
outgoing_tx: mpsc::Sender<proto::Envelope>,
next_message_id: Arc<AtomicU32>,
response_channels: Arc<Mutex<HashMap<u32, mpsc::Sender<proto::Envelope>>>>,
@@ -100,7 +100,7 @@ impl Peer {
pub async fn add_connection(
self: &Arc<Self>,
conn: Conn,
connection: Connection,
) -> (
ConnectionId,
impl Future<Output = anyhow::Result<()>> + Send,
@@ -112,16 +112,16 @@ impl Peer {
);
let (mut incoming_tx, incoming_rx) = mpsc::channel(64);
let (outgoing_tx, mut outgoing_rx) = mpsc::channel(64);
let connection = Connection {
let connection_state = ConnectionState {
outgoing_tx,
next_message_id: Default::default(),
response_channels: Default::default(),
};
let mut writer = MessageStream::new(conn.tx);
let mut reader = MessageStream::new(conn.rx);
let mut writer = MessageStream::new(connection.tx);
let mut reader = MessageStream::new(connection.rx);
let this = self.clone();
let response_channels = connection.response_channels.clone();
let response_channels = connection_state.response_channels.clone();
let handle_io = async move {
loop {
let read_message = reader.read_message().fuse();
@@ -179,7 +179,7 @@ impl Peer {
self.connections
.write()
.await
.insert(connection_id, connection);
.insert(connection_id, connection_state);
(connection_id, handle_io, incoming_rx)
}
@@ -218,7 +218,7 @@ impl Peer {
let this = self.clone();
let (tx, mut rx) = mpsc::channel(1);
async move {
let mut connection = this.connection(receiver_id).await?;
let mut connection = this.connection_state(receiver_id).await?;
let message_id = connection
.next_message_id
.fetch_add(1, atomic::Ordering::SeqCst);
@@ -252,7 +252,7 @@ impl Peer {
) -> impl Future<Output = Result<()>> {
let this = self.clone();
async move {
let mut connection = this.connection(receiver_id).await?;
let mut connection = this.connection_state(receiver_id).await?;
let message_id = connection
.next_message_id
.fetch_add(1, atomic::Ordering::SeqCst);
@@ -272,7 +272,7 @@ impl Peer {
) -> impl Future<Output = Result<()>> {
let this = self.clone();
async move {
let mut connection = this.connection(receiver_id).await?;
let mut connection = this.connection_state(receiver_id).await?;
let message_id = connection
.next_message_id
.fetch_add(1, atomic::Ordering::SeqCst);
@@ -291,7 +291,7 @@ impl Peer {
) -> impl Future<Output = Result<()>> {
let this = self.clone();
async move {
let mut connection = this.connection(receipt.sender_id).await?;
let mut connection = this.connection_state(receipt.sender_id).await?;
let message_id = connection
.next_message_id
.fetch_add(1, atomic::Ordering::SeqCst);
@@ -310,7 +310,7 @@ impl Peer {
) -> impl Future<Output = Result<()>> {
let this = self.clone();
async move {
let mut connection = this.connection(receipt.sender_id).await?;
let mut connection = this.connection_state(receipt.sender_id).await?;
let message_id = connection
.next_message_id
.fetch_add(1, atomic::Ordering::SeqCst);
@@ -322,10 +322,10 @@ impl Peer {
}
}
fn connection(
fn connection_state(
self: &Arc<Self>,
connection_id: ConnectionId,
) -> impl Future<Output = Result<Connection>> {
) -> impl Future<Output = Result<ConnectionState>> {
let this = self.clone();
async move {
let connections = this.connections.read().await;
@@ -352,12 +352,12 @@ mod tests {
let client1 = Peer::new();
let client2 = Peer::new();
let (client1_to_server_conn, server_to_client_1_conn, _) = Conn::in_memory();
let (client1_to_server_conn, server_to_client_1_conn, _) = Connection::in_memory();
let (client1_conn_id, io_task1, _) =
client1.add_connection(client1_to_server_conn).await;
let (_, io_task2, incoming1) = server.add_connection(server_to_client_1_conn).await;
let (client2_to_server_conn, server_to_client_2_conn, _) = Conn::in_memory();
let (client2_to_server_conn, server_to_client_2_conn, _) = Connection::in_memory();
let (client2_conn_id, io_task3, _) =
client2.add_connection(client2_to_server_conn).await;
let (_, io_task4, incoming2) = server.add_connection(server_to_client_2_conn).await;
@@ -486,7 +486,7 @@ mod tests {
#[test]
fn test_disconnect() {
smol::block_on(async move {
let (client_conn, mut server_conn, _) = Conn::in_memory();
let (client_conn, mut server_conn, _) = Connection::in_memory();
let client = Peer::new();
let (connection_id, io_handler, mut incoming) =
@@ -520,7 +520,7 @@ mod tests {
#[test]
fn test_io_error() {
smol::block_on(async move {
let (client_conn, server_conn, _) = Conn::in_memory();
let (client_conn, server_conn, _) = Connection::in_memory();
drop(server_conn);
let client = Peer::new();
+19
View File
@@ -248,3 +248,22 @@ impl From<SystemTime> for Timestamp {
}
}
}
impl From<u128> for Nonce {
fn from(nonce: u128) -> Self {
let upper_half = (nonce >> 64) as u64;
let lower_half = nonce as u64;
Self {
upper_half,
lower_half,
}
}
}
impl From<Nonce> for u128 {
fn from(nonce: Nonce) -> Self {
let upper_half = (nonce.upper_half as u128) << 64;
let lower_half = nonce.lower_half as u128;
upper_half | lower_half
}
}