Detailed changes
@@ -8,6 +8,9 @@
"type": "lldb",
"request": "launch",
"name": "Debug executable 'Zed'",
+ "env": {
+ "ZED_SERVER_URL": "http://localhost:8080"
+ },
"cargo": {
"args": [
"build",
@@ -162,6 +162,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+[[package]]
+name = "arrayvec"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd"
+
[[package]]
name = "ascii"
version = "1.0.0"
@@ -626,7 +632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
dependencies = [
"arrayref",
- "arrayvec",
+ "arrayvec 0.5.2",
"constant_time_eq",
]
@@ -637,7 +643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3"
dependencies = [
"arrayref",
- "arrayvec",
+ "arrayvec 0.5.2",
"cc",
"cfg-if 0.1.10",
"constant_time_eq",
@@ -2145,6 +2151,7 @@ name = "gpui"
version = "0.1.0"
dependencies = [
"anyhow",
+ "arrayvec 0.7.1",
"async-task",
"backtrace",
"bindgen",
@@ -2160,6 +2167,7 @@ dependencies = [
"font-kit",
"foreign-types",
"gpui_macros",
+ "lazy_static",
"log",
"metal",
"num_cpus",
@@ -2179,6 +2187,7 @@ dependencies = [
"simplelog",
"smallvec",
"smol",
+ "time 0.3.2",
"tiny-skia",
"tree-sitter",
"usvg",
@@ -2632,7 +2641,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e30b1df631d23875f230ed3ddd1a88c231f269a04b2044eb6ca87e763b5f4c42"
dependencies = [
- "arrayvec",
+ "arrayvec 0.5.2",
]
[[package]]
@@ -2665,7 +2674,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
dependencies = [
- "arrayvec",
+ "arrayvec 0.5.2",
"bitflags 1.2.1",
"cfg-if 1.0.0",
"ryu",
@@ -2728,6 +2737,16 @@ dependencies = [
"vcpkg",
]
+[[package]]
+name = "lipsum"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ee7271c76a89032dcc7e595c0a739a9c5514eab483deb0e82981fe2098c56a"
+dependencies = [
+ "rand 0.8.3",
+ "rand_chacha 0.3.0",
+]
+
[[package]]
name = "lock_api"
version = "0.4.2"
@@ -4021,9 +4040,9 @@ dependencies = [
[[package]]
name = "rust-embed"
-version = "5.9.0"
+version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fe1fe6aac5d6bb9e1ffd81002340363272a7648234ec7bdfac5ee202cb65523"
+checksum = "1be44a6694859b7cfc955699935944a6844aa9fe416aeda5d40829e3e38dfee6"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
@@ -4032,9 +4051,9 @@ dependencies = [
[[package]]
name = "rust-embed-impl"
-version = "5.9.0"
+version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ed91c41c42ef7bf687384439c312e75e0da9c149b0390889b94de3c7d9d9e66"
+checksum = "f567ca01565c50c67b29e535f5f67b8ea8aeadaeed16a88f10792ab57438b957"
dependencies = [
"proc-macro2",
"quote",
@@ -4045,10 +4064,12 @@ dependencies = [
[[package]]
name = "rust-embed-utils"
-version = "5.1.0"
+version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a512219132473ab0a77b52077059f1c47ce4af7fbdc94503e9862a34422876d"
+checksum = "6116e7ab9ea963f60f2f20291d8fcf6c7273192cdd7273b3c80729a9605c97b2"
dependencies = [
+ "glob 0.3.0",
+ "sha2",
"walkdir",
]
@@ -4699,6 +4720,7 @@ dependencies = [
"sqlx-rt 0.5.5",
"stringprep",
"thiserror",
+ "time 0.2.25",
"url",
"webpki",
"webpki-roots",
@@ -5133,6 +5155,15 @@ dependencies = [
"winapi 0.3.9",
]
+[[package]]
+name = "time"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0a10c9a9fb3a5dce8c2239ed670f1a2569fcf42da035f5face1b19860d52b0"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "time-macros"
version = "0.1.1"
@@ -5163,7 +5194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bf81f2900d2e235220e6f31ec9f63ade6a7f59090c556d74fe949bb3b15e9fe"
dependencies = [
"arrayref",
- "arrayvec",
+ "arrayvec 0.5.2",
"bytemuck",
"cfg-if 1.0.0",
"png 0.16.8",
@@ -5791,7 +5822,7 @@ name = "zed"
version = "0.1.0"
dependencies = [
"anyhow",
- "arrayvec",
+ "arrayvec 0.7.1",
"async-trait",
"async-tungstenite",
"cargo-bundle",
@@ -5817,12 +5848,14 @@ dependencies = [
"seahash",
"serde 1.0.125",
"serde_json 1.0.64",
+ "serde_path_to_error",
"similar",
"simplelog",
"smallvec",
"smol",
"surf",
"tempdir",
+ "time 0.3.2",
"tiny_http",
"toml 0.5.8",
"tree-sitter",
@@ -5852,6 +5885,7 @@ dependencies = [
"http-auth-basic",
"jwt-simple",
"lazy_static",
+ "lipsum",
"oauth2",
"oauth2-surf",
"parking_lot",
@@ -5866,6 +5900,7 @@ dependencies = [
"surf",
"tide",
"tide-compress",
+ "time 0.2.25",
"toml 0.5.8",
"zed",
"zrpc",
@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=./script/node_modules \
RUN --mount=type=cache,target=./script/node_modules \
--mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=./target \
- cargo build --release --bin zed-server
+ cargo build --release --package zed-server --bin zed-server
# Copy server binary out of cached directory
RUN --mount=type=cache,target=./target \
@@ -4,7 +4,7 @@ FROM rust 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.5
+ cargo install sqlx-cli --root=/app --target-dir=/app/target --version 0.5.7
FROM debian:buster-slim as runtime
RUN apt-get update; \
@@ -5,11 +5,13 @@ name = "gpui"
version = "0.1.0"
[dependencies]
+arrayvec = "0.7.1"
async-task = "4.0.3"
backtrace = "0.3"
ctor = "0.1"
etagere = "0.2"
gpui_macros = { path = "../gpui_macros" }
+lazy_static = "1.4.0"
log = "0.4"
num_cpus = "1.13"
ordered-float = "2.1.1"
@@ -25,6 +27,7 @@ serde = { version = "1.0.125", features = ["derive"] }
serde_json = "1.0.64"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
+time = { version = "0.3" }
tiny-skia = "0.5"
tree-sitter = "0.19"
usvg = "0.14"
@@ -1,6 +1,7 @@
use gpui::{
color::Color,
fonts::{Properties, Weight},
+ text_layout::RunStyle,
DebugContext, Element as _, Quad,
};
use log::LevelFilter;
@@ -12,7 +13,7 @@ fn main() {
gpui::App::new(()).unwrap().run(|cx| {
cx.platform().activate(true);
- cx.add_window(|_| TextView);
+ cx.add_window(Default::default(), |_| TextView);
});
}
@@ -28,7 +29,7 @@ impl gpui::View for TextView {
"View"
}
- fn render(&self, _: &gpui::RenderContext<Self>) -> gpui::ElementBox {
+ fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox {
TextElement.boxed()
}
}
@@ -46,56 +47,57 @@ impl gpui::Element for TextElement {
(constraint.max, ())
}
- fn after_layout(
- &mut self,
- _: pathfinder_geometry::vector::Vector2F,
- _: &mut Self::LayoutState,
- _: &mut gpui::AfterLayoutContext,
- ) {
- }
-
fn paint(
&mut self,
bounds: RectF,
+ visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut gpui::PaintContext,
) -> Self::PaintState {
let font_size = 12.;
let family = cx.font_cache.load_family(&["SF Pro Display"]).unwrap();
- let normal = cx
- .font_cache
- .select_font(family, &Default::default())
- .unwrap();
- let bold = cx
- .font_cache
- .select_font(
- family,
- &Properties {
- weight: Weight::BOLD,
- ..Default::default()
- },
- )
- .unwrap();
+ let normal = RunStyle {
+ font_id: cx
+ .font_cache
+ .select_font(family, &Default::default())
+ .unwrap(),
+ color: Color::default(),
+ underline: false,
+ };
+ let bold = RunStyle {
+ font_id: cx
+ .font_cache
+ .select_font(
+ family,
+ &Properties {
+ weight: Weight::BOLD,
+ ..Default::default()
+ },
+ )
+ .unwrap(),
+ color: Color::default(),
+ underline: false,
+ };
let text = "Hello world!";
let line = cx.text_layout_cache.layout_str(
text,
font_size,
&[
- (1, normal, Color::default()),
- (1, bold, Color::default()),
- (1, normal, Color::default()),
- (1, bold, Color::default()),
- (text.len() - 4, normal, Color::default()),
+ (1, normal.clone()),
+ (1, bold.clone()),
+ (1, normal.clone()),
+ (1, bold.clone()),
+ (text.len() - 4, normal.clone()),
],
);
cx.scene.push_quad(Quad {
- bounds: bounds,
+ bounds,
background: Some(Color::white()),
..Default::default()
});
- line.paint(bounds.origin(), bounds, cx);
+ line.paint(bounds.origin(), visible_bounds, bounds.height(), cx);
}
fn dispatch_event(
@@ -2,35 +2,37 @@ use crate::{
elements::ElementBox,
executor,
keymap::{self, Keystroke},
- platform::{self, Platform, PromptLevel, WindowOptions},
+ platform::{self, CursorStyle, Platform, PromptLevel, WindowOptions},
presenter::Presenter,
util::{post_inc, timeout},
- AssetCache, AssetSource, ClipboardItem, EventContext, FontCache, PathPromptOptions,
- TextLayoutCache,
+ AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
};
use anyhow::{anyhow, Result};
use async_task::Task;
use keymap::MatchResult;
-use parking_lot::{Mutex, RwLock};
-use pathfinder_geometry::{rect::RectF, vector::vec2f};
+use parking_lot::Mutex;
use platform::Event;
use postage::{mpsc, sink::Sink as _, stream::Stream as _};
use smol::prelude::*;
use std::{
any::{type_name, Any, TypeId},
cell::RefCell,
- collections::{hash_map::Entry, HashMap, HashSet, VecDeque},
+ collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque},
fmt::{self, Debug},
hash::{Hash, Hasher},
marker::PhantomData,
+ mem,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
rc::{self, Rc},
- sync::{Arc, Weak},
+ sync::{
+ atomic::{AtomicUsize, Ordering::SeqCst},
+ Arc, Weak,
+ },
time::Duration,
};
-pub trait Entity: 'static + Send + Sync {
+pub trait Entity: 'static {
type Event;
fn release(&mut self, _: &mut MutableAppContext) {}
@@ -38,7 +40,7 @@ pub trait Entity: 'static + Send + Sync {
pub trait View: Entity + Sized {
fn ui_name() -> &'static str;
- fn render(&self, cx: &RenderContext<'_, Self>) -> ElementBox;
+ fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox;
fn on_focus(&mut self, _: &mut ViewContext<Self>) {}
fn on_blur(&mut self, _: &mut ViewContext<Self>) {}
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
@@ -70,6 +72,11 @@ pub trait UpdateModel {
F: FnOnce(&mut T, &mut ModelContext<T>) -> S;
}
+pub trait UpgradeModelHandle {
+ fn upgrade_model_handle<T: Entity>(&self, handle: WeakModelHandle<T>)
+ -> Option<ModelHandle<T>>;
+}
+
pub trait ReadView {
fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T;
}
@@ -88,6 +95,83 @@ pub trait UpdateView {
F: FnOnce(&mut T, &mut ViewContext<T>) -> S;
}
+pub trait Action: 'static + AnyAction {
+ type Argument: 'static + Clone;
+}
+
+pub trait AnyAction {
+ fn id(&self) -> TypeId;
+ fn name(&self) -> &'static str;
+ fn as_any(&self) -> &dyn Any;
+ fn boxed_clone(&self) -> Box<dyn AnyAction>;
+ fn boxed_clone_as_any(&self) -> Box<dyn Any>;
+}
+
+#[macro_export]
+macro_rules! action {
+ ($name:ident, $arg:ty) => {
+ #[derive(Clone)]
+ pub struct $name(pub $arg);
+
+ impl $crate::Action for $name {
+ type Argument = $arg;
+ }
+
+ impl $crate::AnyAction for $name {
+ fn id(&self) -> std::any::TypeId {
+ std::any::TypeId::of::<$name>()
+ }
+
+ fn name(&self) -> &'static str {
+ stringify!($name)
+ }
+
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+
+ fn boxed_clone(&self) -> Box<dyn $crate::AnyAction> {
+ Box::new(self.clone())
+ }
+
+ fn boxed_clone_as_any(&self) -> Box<dyn std::any::Any> {
+ Box::new(self.clone())
+ }
+ }
+ };
+
+ ($name:ident) => {
+ #[derive(Clone, Debug, Eq, PartialEq)]
+ pub struct $name;
+
+ impl $crate::Action for $name {
+ type Argument = ();
+ }
+
+ impl $crate::AnyAction for $name {
+ fn id(&self) -> std::any::TypeId {
+ std::any::TypeId::of::<$name>()
+ }
+
+ fn name(&self) -> &'static str {
+ stringify!($name)
+ }
+
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+
+ fn boxed_clone(&self) -> Box<dyn $crate::AnyAction> {
+ Box::new(self.clone())
+ }
+
+ fn boxed_clone_as_any(&self) -> Box<dyn std::any::Any> {
+ Box::new(self.clone())
+ }
+ }
+ };
+}
+
pub struct Menu<'a> {
pub name: &'a str,
pub items: Vec<MenuItem<'a>>,
@@ -97,8 +181,7 @@ pub enum MenuItem<'a> {
Action {
name: &'a str,
keystroke: Option<&'a str>,
- action: &'a str,
- arg: Option<Box<dyn Any + 'static>>,
+ action: Box<dyn AnyAction>,
},
Separator,
}
@@ -132,19 +215,19 @@ impl App {
))));
let cx = app.0.clone();
- foreground_platform.on_menu_command(Box::new(move |command, arg| {
+ foreground_platform.on_menu_command(Box::new(move |action| {
let mut cx = cx.borrow_mut();
if let Some(key_window_id) = cx.cx.platform.key_window_id() {
if let Some((presenter, _)) = cx.presenters_and_platform_windows.get(&key_window_id)
{
let presenter = presenter.clone();
let path = presenter.borrow().dispatch_path(cx.as_ref());
- cx.dispatch_action_any(key_window_id, &path, command, arg.unwrap_or(&()));
+ cx.dispatch_action_any(key_window_id, &path, action);
} else {
- cx.dispatch_global_action_any(command, arg.unwrap_or(&()));
+ cx.dispatch_global_action_any(action);
}
} else {
- cx.dispatch_global_action_any(command, arg.unwrap_or(&()));
+ cx.dispatch_global_action_any(action);
}
}));
@@ -211,10 +294,16 @@ impl App {
let platform = self.0.borrow().foreground_platform.clone();
platform.run(Box::new(move || {
let mut cx = self.0.borrow_mut();
- on_finish_launching(&mut *cx);
+ let cx = &mut *cx;
+ crate::views::init(cx);
+ on_finish_launching(cx);
}))
}
+ pub fn platform(&self) -> Arc<dyn Platform> {
+ self.0.borrow().platform()
+ }
+
pub fn font_cache(&self) -> Arc<FontCache> {
self.0.borrow().cx.font_cache.clone()
}
@@ -254,23 +343,19 @@ impl TestAppContext {
cx
}
- pub fn dispatch_action<T: 'static + Any>(
+ pub fn dispatch_action<A: Action>(
&self,
window_id: usize,
responder_chain: Vec<usize>,
- name: &str,
- arg: T,
+ action: A,
) {
- self.cx.borrow_mut().dispatch_action_any(
- window_id,
- &responder_chain,
- name,
- Box::new(arg).as_ref(),
- );
+ self.cx
+ .borrow_mut()
+ .dispatch_action_any(window_id, &responder_chain, &action);
}
- pub fn dispatch_global_action<T: 'static + Any>(&self, name: &str, arg: T) {
- self.cx.borrow_mut().dispatch_global_action(name, arg);
+ pub fn dispatch_global_action<A: Action>(&self, action: A) {
+ self.cx.borrow_mut().dispatch_global_action(action);
}
pub fn dispatch_keystroke(
@@ -300,7 +385,9 @@ impl TestAppContext {
T: View,
F: FnOnce(&mut ViewContext<T>) -> T,
{
- self.cx.borrow_mut().add_window(build_root_view)
+ self.cx
+ .borrow_mut()
+ .add_window(Default::default(), build_root_view)
}
pub fn window_ids(&self) -> Vec<usize> {
@@ -457,6 +544,15 @@ impl UpdateModel for AsyncAppContext {
}
}
+impl UpgradeModelHandle for AsyncAppContext {
+ fn upgrade_model_handle<T: Entity>(
+ &self,
+ handle: WeakModelHandle<T>,
+ ) -> Option<ModelHandle<T>> {
+ self.0.borrow_mut().upgrade_model_handle(handle)
+ }
+}
+
impl ReadModelWith for AsyncAppContext {
fn read_model_with<E: Entity, F: FnOnce(&E, &AppContext) -> T, T>(
&self,
@@ -548,23 +644,25 @@ impl ReadViewWith for TestAppContext {
}
type ActionCallback =
- dyn FnMut(&mut dyn AnyView, &dyn Any, &mut MutableAppContext, usize, usize) -> bool;
+ dyn FnMut(&mut dyn AnyView, &dyn AnyAction, &mut MutableAppContext, usize, usize) -> bool;
+type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext);
-type GlobalActionCallback = dyn FnMut(&dyn Any, &mut MutableAppContext);
+type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
+type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
pub struct MutableAppContext {
weak_self: Option<rc::Weak<RefCell<Self>>>,
foreground_platform: Rc<dyn platform::ForegroundPlatform>,
assets: Arc<AssetCache>,
cx: AppContext,
- actions: HashMap<TypeId, HashMap<String, Vec<Box<ActionCallback>>>>,
- global_actions: HashMap<String, Vec<Box<GlobalActionCallback>>>,
+ actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
+ global_actions: HashMap<TypeId, Vec<Box<GlobalActionCallback>>>,
keystroke_matcher: keymap::Matcher,
next_entity_id: usize,
next_window_id: usize,
- subscriptions: HashMap<usize, Vec<Subscription>>,
- model_observations: HashMap<usize, Vec<ModelObservation>>,
- view_observations: HashMap<usize, Vec<ViewObservation>>,
+ next_subscription_id: usize,
+ subscriptions: Arc<Mutex<HashMap<usize, BTreeMap<usize, SubscriptionCallback>>>>,
+ observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ObservationCallback>>>>,
presenters_and_platform_windows:
HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
debug_elements_callbacks: HashMap<usize, Box<dyn Fn(&AppContext) -> crate::json::Value>>,
@@ -572,6 +670,7 @@ pub struct MutableAppContext {
pending_effects: VecDeque<Effect>,
pending_flushes: usize,
flushing_effects: bool,
+ next_cursor_style_handle_id: Arc<AtomicUsize>,
}
impl MutableAppContext {
@@ -591,7 +690,7 @@ impl MutableAppContext {
models: Default::default(),
views: Default::default(),
windows: Default::default(),
- values: Default::default(),
+ element_states: Default::default(),
ref_counts: Arc::new(Mutex::new(RefCounts::default())),
background,
font_cache,
@@ -602,15 +701,16 @@ impl MutableAppContext {
keystroke_matcher: keymap::Matcher::default(),
next_entity_id: 0,
next_window_id: 0,
- subscriptions: HashMap::new(),
- model_observations: HashMap::new(),
- view_observations: HashMap::new(),
+ next_subscription_id: 0,
+ subscriptions: Default::default(),
+ observations: Default::default(),
presenters_and_platform_windows: HashMap::new(),
debug_elements_callbacks: HashMap::new(),
foreground,
pending_effects: VecDeque::new(),
pending_flushes: 0,
flushing_effects: false,
+ next_cursor_style_handle_id: Default::default(),
}
}
@@ -648,69 +748,53 @@ impl MutableAppContext {
.map(|debug_elements| debug_elements(&self.cx))
}
- pub fn add_action<S, V, T, F>(&mut self, name: S, mut handler: F)
+ pub fn add_action<A, V, F>(&mut self, mut handler: F)
where
- S: Into<String>,
+ A: Action,
V: View,
- T: Any,
- F: 'static + FnMut(&mut V, &T, &mut ViewContext<V>),
+ F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>),
{
- let name = name.into();
- let name_clone = name.clone();
let handler = Box::new(
move |view: &mut dyn AnyView,
- arg: &dyn Any,
+ action: &dyn AnyAction,
cx: &mut MutableAppContext,
window_id: usize,
view_id: usize| {
- match arg.downcast_ref() {
- Some(arg) => {
- let mut cx = ViewContext::new(cx, window_id, view_id);
- handler(
- view.as_any_mut()
- .downcast_mut()
- .expect("downcast is type safe"),
- arg,
- &mut cx,
- );
- cx.halt_action_dispatch
- }
- None => {
- log::error!("Could not downcast argument for action {}", name_clone);
- false
- }
- }
+ let action = action.as_any().downcast_ref().unwrap();
+ let mut cx = ViewContext::new(cx, window_id, view_id);
+ handler(
+ view.as_any_mut()
+ .downcast_mut()
+ .expect("downcast is type safe"),
+ action,
+ &mut cx,
+ );
+ cx.halt_action_dispatch
},
);
self.actions
.entry(TypeId::of::<V>())
.or_default()
- .entry(name)
+ .entry(TypeId::of::<A>())
.or_default()
.push(handler);
}
- pub fn add_global_action<S, T, F>(&mut self, name: S, mut handler: F)
+ pub fn add_global_action<A, F>(&mut self, mut handler: F)
where
- S: Into<String>,
- T: 'static + Any,
- F: 'static + FnMut(&T, &mut MutableAppContext),
+ A: Action,
+ F: 'static + FnMut(&A, &mut MutableAppContext),
{
- let name = name.into();
- let name_clone = name.clone();
- let handler = Box::new(move |arg: &dyn Any, cx: &mut MutableAppContext| {
- if let Some(arg) = arg.downcast_ref() {
- handler(arg, cx);
- } else {
- log::error!(
- "Could not downcast argument for global action {}",
- name_clone
- );
- }
+ let handler = Box::new(move |action: &dyn AnyAction, cx: &mut MutableAppContext| {
+ let action = action.as_any().downcast_ref().unwrap();
+ handler(action, cx);
});
- self.global_actions.entry(name).or_default().push(handler);
+ self.global_actions
+ .entry(TypeId::of::<A>())
+ .or_default()
+ .push(handler);
}
pub fn window_ids(&self) -> impl Iterator<Item = usize> + '_ {
@@ -732,12 +816,49 @@ impl MutableAppContext {
self.cx.focused_view_id(window_id)
}
- pub fn render_view(&self, window_id: usize, view_id: usize) -> Result<ElementBox> {
- self.cx.render_view(window_id, view_id)
+ pub fn render_view(
+ &mut self,
+ window_id: usize,
+ view_id: usize,
+ titlebar_height: f32,
+ refreshing: bool,
+ ) -> Result<ElementBox> {
+ let mut view = self
+ .cx
+ .views
+ .remove(&(window_id, view_id))
+ .ok_or(anyhow!("view not found"))?;
+ let element = view.render(window_id, view_id, titlebar_height, refreshing, self);
+ self.cx.views.insert((window_id, view_id), view);
+ Ok(element)
}
- pub fn render_views(&self, window_id: usize) -> HashMap<usize, ElementBox> {
- self.cx.render_views(window_id)
+ pub fn render_views(
+ &mut self,
+ window_id: usize,
+ titlebar_height: f32,
+ ) -> HashMap<usize, ElementBox> {
+ let view_ids = self
+ .views
+ .keys()
+ .filter_map(|(win_id, view_id)| {
+ if *win_id == window_id {
+ Some(*view_id)
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+ view_ids
+ .into_iter()
+ .map(|view_id| {
+ (
+ view_id,
+ self.render_view(window_id, view_id, titlebar_height, false)
+ .unwrap(),
+ )
+ })
+ .collect()
}
pub fn update<T, F: FnOnce() -> T>(&mut self, callback: F) -> T {
@@ -808,37 +929,112 @@ impl MutableAppContext {
);
}
+ pub fn subscribe<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
+ where
+ E: Entity,
+ E::Event: 'static,
+ H: Handle<E>,
+ F: 'static + FnMut(H, &E::Event, &mut Self),
+ {
+ self.subscribe_internal(handle, move |handle, event, cx| {
+ callback(handle, event, cx);
+ true
+ })
+ }
+
+ fn observe<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
+ where
+ E: Entity,
+ E::Event: 'static,
+ H: Handle<E>,
+ F: 'static + FnMut(H, &mut Self),
+ {
+ self.observe_internal(handle, move |handle, cx| {
+ callback(handle, cx);
+ true
+ })
+ }
+
+ pub fn subscribe_internal<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
+ where
+ E: Entity,
+ E::Event: 'static,
+ H: Handle<E>,
+ F: 'static + FnMut(H, &E::Event, &mut Self) -> bool,
+ {
+ let id = post_inc(&mut self.next_subscription_id);
+ let emitter = handle.downgrade();
+ self.subscriptions
+ .lock()
+ .entry(handle.id())
+ .or_default()
+ .insert(
+ id,
+ Box::new(move |payload, cx| {
+ if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) {
+ let payload = payload.downcast_ref().expect("downcast is type safe");
+ callback(emitter, payload, cx)
+ } else {
+ false
+ }
+ }),
+ );
+ Subscription::Subscription {
+ id,
+ entity_id: handle.id(),
+ subscriptions: Some(Arc::downgrade(&self.subscriptions)),
+ }
+ }
+
+ fn observe_internal<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
+ where
+ E: Entity,
+ E::Event: 'static,
+ H: Handle<E>,
+ F: 'static + FnMut(H, &mut Self) -> bool,
+ {
+ let id = post_inc(&mut self.next_subscription_id);
+ let observed = handle.downgrade();
+ self.observations
+ .lock()
+ .entry(handle.id())
+ .or_default()
+ .insert(
+ id,
+ Box::new(move |cx| {
+ if let Some(observed) = H::upgrade_from(&observed, cx) {
+ callback(observed, cx)
+ } else {
+ false
+ }
+ }),
+ );
+ Subscription::Observation {
+ id,
+ entity_id: handle.id(),
+ observations: Some(Arc::downgrade(&self.observations)),
+ }
+ }
+
pub(crate) fn notify_view(&mut self, window_id: usize, view_id: usize) {
self.pending_effects
.push_back(Effect::ViewNotification { window_id, view_id });
}
- pub(crate) fn notify_all_views(&mut self) {
- let notifications = self
- .views
- .keys()
- .copied()
- .map(|(window_id, view_id)| Effect::ViewNotification { window_id, view_id })
- .collect::<Vec<_>>();
- self.pending_effects.extend(notifications);
- }
-
- pub fn dispatch_action<T: 'static + Any>(
+ pub fn dispatch_action<A: Action>(
&mut self,
window_id: usize,
responder_chain: Vec<usize>,
- name: &str,
- arg: T,
+ action: &A,
) {
- self.dispatch_action_any(window_id, &responder_chain, name, Box::new(arg).as_ref());
+ self.dispatch_action_any(window_id, &responder_chain, action);
}
pub(crate) fn dispatch_action_any(
&mut self,
window_id: usize,
path: &[usize],
- name: &str,
- arg: &dyn Any,
+ action: &dyn AnyAction,
) -> bool {
self.pending_flushes += 1;
let mut halted_dispatch = false;
@@ -850,10 +1046,11 @@ impl MutableAppContext {
if let Some((name, mut handlers)) = self
.actions
.get_mut(&type_id)
- .and_then(|h| h.remove_entry(name))
+ .and_then(|h| h.remove_entry(&action.id()))
{
for handler in handlers.iter_mut().rev() {
- let halt_dispatch = handler(view.as_mut(), arg, self, window_id, *view_id);
+ let halt_dispatch =
+ handler(view.as_mut(), action, self, window_id, *view_id);
if halt_dispatch {
halted_dispatch = true;
break;
@@ -874,22 +1071,22 @@ impl MutableAppContext {
}
if !halted_dispatch {
- self.dispatch_global_action_any(name, arg);
+ self.dispatch_global_action_any(action);
}
self.flush_effects();
halted_dispatch
}
- pub fn dispatch_global_action<T: 'static + Any>(&mut self, name: &str, arg: T) {
- self.dispatch_global_action_any(name, Box::new(arg).as_ref());
+ pub fn dispatch_global_action<A: Action>(&mut self, action: A) {
+ self.dispatch_global_action_any(&action);
}
- fn dispatch_global_action_any(&mut self, name: &str, arg: &dyn Any) {
- if let Some((name, mut handlers)) = self.global_actions.remove_entry(name) {
+ fn dispatch_global_action_any(&mut self, action: &dyn AnyAction) {
+ if let Some((name, mut handlers)) = self.global_actions.remove_entry(&action.id()) {
self.pending_flushes += 1;
for handler in handlers.iter_mut().rev() {
- handler(arg, self);
+ handler(action, self);
}
self.global_actions.insert(name, handlers);
self.flush_effects();
@@ -928,13 +1125,9 @@ impl MutableAppContext {
{
MatchResult::None => {}
MatchResult::Pending => pending = true,
- MatchResult::Action { name, arg } => {
- if self.dispatch_action_any(
- window_id,
- &responder_chain[0..=i],
- &name,
- arg.as_ref().map(|arg| arg.as_ref()).unwrap_or(&()),
- ) {
+ MatchResult::Action(action) => {
+ if self.dispatch_action_any(window_id, &responder_chain[0..=i], action.as_ref())
+ {
return Ok(true);
}
}
@@ -959,7 +1152,11 @@ impl MutableAppContext {
handle
}
- pub fn add_window<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>)
+ pub fn add_window<T, F>(
+ &mut self,
+ window_options: WindowOptions,
+ build_root_view: F,
+ ) -> (usize, ViewHandle<T>)
where
T: View,
F: FnOnce(&mut ViewContext<T>) -> T,
@@ -976,7 +1173,7 @@ impl MutableAppContext {
invalidation: None,
},
);
- self.open_platform_window(window_id);
+ self.open_platform_window(window_id, window_options);
root_view.update(self, |view, cx| {
view.on_focus(cx);
cx.notify();
@@ -992,23 +1189,14 @@ impl MutableAppContext {
self.remove_dropped_entities();
}
- fn open_platform_window(&mut self, window_id: usize) {
- let mut window = self.cx.platform.open_window(
- window_id,
- WindowOptions {
- bounds: RectF::new(vec2f(0., 0.), vec2f(1024., 768.)),
- title: "Zed".into(),
- },
- self.foreground.clone(),
- );
- let text_layout_cache = TextLayoutCache::new(self.cx.platform.fonts());
- let presenter = Rc::new(RefCell::new(Presenter::new(
- window_id,
- self.cx.font_cache.clone(),
- text_layout_cache,
- self.assets.clone(),
- self,
- )));
+ fn open_platform_window(&mut self, window_id: usize, window_options: WindowOptions) {
+ let mut window =
+ self.cx
+ .platform
+ .open_window(window_id, window_options, self.foreground.clone());
+ let presenter = Rc::new(RefCell::new(
+ self.build_presenter(window_id, window.titlebar_height()),
+ ));
{
let mut app = self.upgrade();
@@ -1035,16 +1223,8 @@ impl MutableAppContext {
{
let mut app = self.upgrade();
- let presenter = presenter.clone();
- window.on_resize(Box::new(move |window| {
- app.update(|cx| {
- let scene = presenter.borrow_mut().build_scene(
- window.size(),
- window.scale_factor(),
- cx,
- );
- window.present_scene(scene);
- })
+ window.on_resize(Box::new(move || {
+ app.update(|cx| cx.resize_window(window_id))
}));
}
@@ -1063,6 +1243,34 @@ impl MutableAppContext {
});
}
+ pub fn build_presenter(&mut self, window_id: usize, titlebar_height: f32) -> Presenter {
+ Presenter::new(
+ window_id,
+ titlebar_height,
+ self.cx.font_cache.clone(),
+ TextLayoutCache::new(self.cx.platform.fonts()),
+ self.assets.clone(),
+ self,
+ )
+ }
+
+ pub fn build_render_context<V: View>(
+ &mut self,
+ window_id: usize,
+ view_id: usize,
+ titlebar_height: f32,
+ refreshing: bool,
+ ) -> RenderContext<V> {
+ RenderContext {
+ app: self,
+ titlebar_height,
+ refreshing,
+ window_id,
+ view_id,
+ view_type: PhantomData,
+ }
+ }
+
pub fn add_view<T, F>(&mut self, window_id: usize, build_view: F) -> ViewHandle<T>
where
T: View,
@@ -1102,24 +1310,39 @@ impl MutableAppContext {
handle
}
+ pub fn element_state<Tag: 'static, T: 'static + Default>(
+ &mut self,
+ id: ElementStateId,
+ ) -> ElementStateHandle<T> {
+ let key = (TypeId::of::<Tag>(), id);
+ self.cx
+ .element_states
+ .entry(key)
+ .or_insert_with(|| Box::new(T::default()));
+ ElementStateHandle::new(TypeId::of::<Tag>(), id, &self.cx.ref_counts)
+ }
+
fn remove_dropped_entities(&mut self) {
loop {
- let (dropped_models, dropped_views, dropped_values) =
+ let (dropped_models, dropped_views, dropped_element_states) =
self.cx.ref_counts.lock().take_dropped();
- if dropped_models.is_empty() && dropped_views.is_empty() && dropped_values.is_empty() {
+ if dropped_models.is_empty()
+ && dropped_views.is_empty()
+ && dropped_element_states.is_empty()
+ {
break;
}
for model_id in dropped_models {
- self.subscriptions.remove(&model_id);
- self.model_observations.remove(&model_id);
+ self.subscriptions.lock().remove(&model_id);
+ self.observations.lock().remove(&model_id);
let mut model = self.cx.models.remove(&model_id).unwrap();
model.release(self);
}
for (window_id, view_id) in dropped_views {
- self.subscriptions.remove(&view_id);
- self.model_observations.remove(&view_id);
+ self.subscriptions.lock().remove(&view_id);
+ self.observations.lock().remove(&view_id);
let mut view = self.cx.views.remove(&(window_id, view_id)).unwrap();
view.release(self);
let change_focus_to = self.cx.windows.get_mut(&window_id).and_then(|window| {
@@ -1140,9 +1363,8 @@ impl MutableAppContext {
}
}
- let mut values = self.cx.values.write();
- for key in dropped_values {
- values.remove(&key);
+ for key in dropped_element_states {
+ self.cx.element_states.remove(&key);
}
}
}
@@ -1153,6 +1375,7 @@ impl MutableAppContext {
if !self.flushing_effects && self.pending_flushes == 0 {
self.flushing_effects = true;
+ let mut refreshing = false;
loop {
if let Some(effect) = self.pending_effects.pop_front() {
match effect {
@@ -1166,15 +1389,31 @@ impl MutableAppContext {
Effect::Focus { window_id, view_id } => {
self.focus(window_id, view_id);
}
+ Effect::ResizeWindow { window_id } => {
+ if let Some(window) = self.cx.windows.get_mut(&window_id) {
+ window
+ .invalidation
+ .get_or_insert(WindowInvalidation::default());
+ }
+ }
+ Effect::RefreshWindows => {
+ refreshing = true;
+ }
}
self.remove_dropped_entities();
} else {
self.remove_dropped_entities();
- self.update_windows();
+ if refreshing {
+ self.perform_window_refresh();
+ } else {
+ self.update_windows();
+ }
if self.pending_effects.is_empty() {
self.flushing_effects = false;
break;
+ } else {
+ refreshing = false;
}
}
}
@@ -1195,8 +1434,9 @@ impl MutableAppContext {
{
{
let mut presenter = presenter.borrow_mut();
- presenter.invalidate(invalidation, self.as_ref());
- let scene = presenter.build_scene(window.size(), window.scale_factor(), self);
+ presenter.invalidate(invalidation, self);
+ let scene =
+ presenter.build_scene(window.size(), window.scale_factor(), false, self);
window.present_scene(scene);
}
self.presenters_and_platform_windows
@@ -1205,134 +1445,101 @@ impl MutableAppContext {
}
}
- fn emit_event(&mut self, entity_id: usize, payload: Box<dyn Any>) {
- if let Some(subscriptions) = self.subscriptions.remove(&entity_id) {
- for mut subscription in subscriptions {
- let alive = match &mut subscription {
- Subscription::FromModel { model_id, callback } => {
- if let Some(mut model) = self.cx.models.remove(model_id) {
- callback(model.as_any_mut(), payload.as_ref(), self, *model_id);
- self.cx.models.insert(*model_id, model);
- true
- } else {
- false
- }
- }
- Subscription::FromView {
- window_id,
- view_id,
- callback,
- } => {
- if let Some(mut view) = self.cx.views.remove(&(*window_id, *view_id)) {
- callback(
- view.as_any_mut(),
- payload.as_ref(),
- self,
- *window_id,
- *view_id,
- );
- self.cx.views.insert((*window_id, *view_id), view);
- true
- } else {
- false
- }
- }
- };
+ fn resize_window(&mut self, window_id: usize) {
+ self.pending_effects
+ .push_back(Effect::ResizeWindow { window_id });
+ }
+
+ pub fn refresh_windows(&mut self) {
+ self.pending_effects.push_back(Effect::RefreshWindows);
+ }
+
+ fn perform_window_refresh(&mut self) {
+ let mut presenters = mem::take(&mut self.presenters_and_platform_windows);
+ for (window_id, (presenter, window)) in &mut presenters {
+ let invalidation = self
+ .cx
+ .windows
+ .get_mut(&window_id)
+ .unwrap()
+ .invalidation
+ .take();
+ let mut presenter = presenter.borrow_mut();
+ presenter.refresh(invalidation, self);
+ let scene = presenter.build_scene(window.size(), window.scale_factor(), true, self);
+ window.present_scene(scene);
+ }
+ self.presenters_and_platform_windows = presenters;
+ }
+ pub fn set_cursor_style(&mut self, style: CursorStyle) -> CursorStyleHandle {
+ self.platform.set_cursor_style(style);
+ let id = self.next_cursor_style_handle_id.fetch_add(1, SeqCst);
+ CursorStyleHandle {
+ id,
+ next_cursor_style_handle_id: self.next_cursor_style_handle_id.clone(),
+ platform: self.platform(),
+ }
+ }
+
+ fn emit_event(&mut self, entity_id: usize, payload: Box<dyn Any>) {
+ let callbacks = self.subscriptions.lock().remove(&entity_id);
+ if let Some(callbacks) = callbacks {
+ for (id, mut callback) in callbacks {
+ let alive = callback(payload.as_ref(), self);
if alive {
self.subscriptions
+ .lock()
.entry(entity_id)
.or_default()
- .push(subscription);
+ .insert(id, callback);
}
}
}
}
fn notify_model_observers(&mut self, observed_id: usize) {
- if let Some(observations) = self.model_observations.remove(&observed_id) {
+ let callbacks = self.observations.lock().remove(&observed_id);
+ if let Some(callbacks) = callbacks {
if self.cx.models.contains_key(&observed_id) {
- for mut observation in observations {
- let alive = match &mut observation {
- ModelObservation::FromModel { model_id, callback } => {
- if let Some(mut model) = self.cx.models.remove(model_id) {
- callback(model.as_any_mut(), observed_id, self, *model_id);
- self.cx.models.insert(*model_id, model);
- true
- } else {
- false
- }
- }
- ModelObservation::FromView {
- window_id,
- view_id,
- callback,
- } => {
- if let Some(mut view) = self.cx.views.remove(&(*window_id, *view_id)) {
- callback(
- view.as_any_mut(),
- observed_id,
- self,
- *window_id,
- *view_id,
- );
- self.cx.views.insert((*window_id, *view_id), view);
- true
- } else {
- false
- }
- }
- };
-
+ for (id, mut callback) in callbacks {
+ let alive = callback(self);
if alive {
- self.model_observations
+ self.observations
+ .lock()
.entry(observed_id)
.or_default()
- .push(observation);
+ .insert(id, callback);
}
}
}
}
}
- fn notify_view_observers(&mut self, window_id: usize, view_id: usize) {
- if let Some(window) = self.cx.windows.get_mut(&window_id) {
+ fn notify_view_observers(&mut self, observed_window_id: usize, observed_view_id: usize) {
+ if let Some(window) = self.cx.windows.get_mut(&observed_window_id) {
window
.invalidation
.get_or_insert_with(Default::default)
.updated
- .insert(view_id);
+ .insert(observed_view_id);
}
- if let Some(observations) = self.view_observations.remove(&view_id) {
- if self.cx.views.contains_key(&(window_id, view_id)) {
- for mut observation in observations {
- let alive = if let Some(mut view) = self
- .cx
- .views
- .remove(&(observation.window_id, observation.view_id))
- {
- (observation.callback)(
- view.as_any_mut(),
- view_id,
- window_id,
- self,
- observation.window_id,
- observation.view_id,
- );
- self.cx
- .views
- .insert((observation.window_id, observation.view_id), view);
- true
- } else {
- false
- };
-
+ let callbacks = self.observations.lock().remove(&observed_view_id);
+ if let Some(callbacks) = callbacks {
+ if self
+ .cx
+ .views
+ .contains_key(&(observed_window_id, observed_view_id))
+ {
+ for (id, mut callback) in callbacks {
+ let alive = callback(self);
if alive {
- self.view_observations
- .entry(view_id)
+ self.observations
+ .lock()
+ .entry(observed_view_id)
.or_default()
- .push(observation);
+ .insert(id, callback);
}
}
}
@@ -5,11 +5,15 @@ mod container;
mod empty;
mod event_handler;
mod flex;
+mod hook;
mod label;
mod line_box;
+mod list;
mod mouse_event_handler;
+mod overlay;
mod stack;
mod svg;
+mod text;
mod uniform_list;
pub use crate::presenter::ChildView;
@@ -20,26 +24,35 @@ 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},
- json, AfterLayoutContext, DebugContext, Event, EventContext, LayoutContext, PaintContext,
- SizeConstraint,
+ json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
use core::panic;
use json::ToJson;
-use std::{any::Any, borrow::Cow, mem};
+use std::{
+ any::Any,
+ borrow::Cow,
+ cell::RefCell,
+ mem,
+ ops::{Deref, DerefMut},
+ rc::Rc,
+};
trait AnyElement {
fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F;
- fn after_layout(&mut self, _: &mut AfterLayoutContext) {}
- fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext);
+ fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext);
fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool;
fn debug(&self, cx: &DebugContext) -> serde_json::Value;
@@ -57,16 +70,10 @@ pub trait Element {
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState);
- fn after_layout(
- &mut self,
- size: Vector2F,
- layout: &mut Self::LayoutState,
- cx: &mut AfterLayoutContext,
- );
-
fn paint(
&mut self,
bounds: RectF,
+ visible_bounds: RectF,
layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState;
@@ -96,20 +103,20 @@ pub trait Element {
where
Self: 'static + Sized,
{
- ElementBox {
+ ElementBox(ElementRc {
name: None,
- element: Box::new(Lifecycle::Init { element: self }),
- }
+ element: Rc::new(RefCell::new(Lifecycle::Init { element: self })),
+ })
}
fn named(self, name: impl Into<Cow<'static, str>>) -> ElementBox
where
Self: 'static + Sized,
{
- ElementBox {
+ ElementBox(ElementRc {
name: Some(name.into()),
- element: Box::new(Lifecycle::Init { element: self }),
- }
+ element: Rc::new(RefCell::new(Lifecycle::Init { element: self })),
+ })
}
}
@@ -132,9 +139,12 @@ pub enum Lifecycle<T: Element> {
paint: T::PaintState,
},
}
-pub struct ElementBox {
+pub struct ElementBox(ElementRc);
+
+#[derive(Clone)]
+pub struct ElementRc {
name: Option<Cow<'static, str>>,
- element: Box<dyn AnyElement>,
+ element: Rc<RefCell<dyn AnyElement>>,
}
impl<T: Element> AnyElement for Lifecycle<T> {
@@ -161,40 +171,49 @@ impl<T: Element> AnyElement for Lifecycle<T> {
result
}
- fn after_layout(&mut self, cx: &mut AfterLayoutContext) {
- if let Lifecycle::PostLayout {
- element,
- size,
- layout,
- ..
- } = self
- {
- element.after_layout(*size, layout, cx);
- } else {
- panic!("invalid element lifecycle state");
- }
- }
-
- fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext) {
- *self = if let Lifecycle::PostLayout {
- mut element,
- constraint,
- size,
- mut layout,
- } = mem::take(self)
- {
- let bounds = RectF::new(origin, size);
- let paint = element.paint(bounds, &mut layout, cx);
+ fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext) {
+ *self = match mem::take(self) {
+ Lifecycle::PostLayout {
+ mut element,
+ constraint,
+ size,
+ mut layout,
+ } => {
+ let bounds = RectF::new(origin, size);
+ let visible_bounds = visible_bounds
+ .intersection(bounds)
+ .unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default()));
+ let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
+ Lifecycle::PostPaint {
+ element,
+ constraint,
+ bounds,
+ layout,
+ paint,
+ }
+ }
Lifecycle::PostPaint {
- element,
+ mut element,
constraint,
bounds,
- layout,
- paint,
+ mut layout,
+ ..
+ } => {
+ let bounds = RectF::new(origin, bounds.size());
+ let visible_bounds = visible_bounds
+ .intersection(bounds)
+ .unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default()));
+ let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
+ Lifecycle::PostPaint {
+ element,
+ constraint,
+ bounds,
+ layout,
+ paint,
+ }
}
- } else {
- panic!("invalid element lifecycle state");
- };
+ _ => panic!("invalid element lifecycle state"),
+ }
}
fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool {
@@ -264,32 +283,51 @@ impl<T: Element> Default for Lifecycle<T> {
}
impl ElementBox {
- pub fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F {
- self.element.layout(constraint, cx)
+ pub fn metadata<T: 'static>(&self) -> Option<&T> {
+ let element = unsafe { &*self.0.element.as_ptr() };
+ element.metadata().and_then(|m| m.downcast_ref())
}
+}
- pub fn after_layout(&mut self, cx: &mut AfterLayoutContext) {
- self.element.after_layout(cx);
+impl Into<ElementRc> for ElementBox {
+ fn into(self) -> ElementRc {
+ self.0
}
+}
+
+impl Deref for ElementBox {
+ type Target = ElementRc;
- pub fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext) {
- self.element.paint(origin, cx);
+ fn deref(&self) -> &Self::Target {
+ &self.0
}
+}
- pub fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool {
- self.element.dispatch_event(event, cx)
+impl DerefMut for ElementBox {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
}
+}
- pub fn size(&self) -> Vector2F {
- self.element.size()
+impl ElementRc {
+ pub fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F {
+ self.element.borrow_mut().layout(constraint, cx)
+ }
+
+ pub fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext) {
+ self.element.borrow_mut().paint(origin, visible_bounds, cx);
+ }
+
+ pub fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool {
+ self.element.borrow_mut().dispatch_event(event, cx)
}
- pub fn metadata(&self) -> Option<&dyn Any> {
- self.element.metadata()
+ pub fn size(&self) -> Vector2F {
+ self.element.borrow().size()
}
pub fn debug(&self, cx: &DebugContext) -> json::Value {
- let mut value = self.element.debug(cx);
+ let mut value = self.element.borrow().debug(cx);
if let Some(name) = &self.name {
if let json::Value::Object(map) = &mut value {
@@ -302,6 +340,15 @@ impl ElementBox {
value
}
+
+ pub fn with_metadata<T, F, R>(&self, f: F) -> R
+ where
+ T: 'static,
+ F: FnOnce(Option<&T>) -> R,
+ {
+ let element = self.element.borrow();
+ f(element.metadata().and_then(|m| m.downcast_ref()))
+ }
}
pub trait ParentElement<'a>: Extend<ElementBox> + Sized {
@@ -1,9 +1,10 @@
use crate::{
- json, AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext,
- LayoutContext, PaintContext, SizeConstraint,
+ geometry::{rect::RectF, vector::Vector2F},
+ json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
+ SizeConstraint,
};
use json::ToJson;
-use pathfinder_geometry::vector::Vector2F;
+
use serde_json::json;
pub struct Align {
@@ -51,18 +52,10 @@ impl Element for Align {
(size, ())
}
- fn after_layout(
- &mut self,
- _: Vector2F,
- _: &mut Self::LayoutState,
- cx: &mut AfterLayoutContext,
- ) {
- self.child.after_layout(cx);
- }
-
fn paint(
&mut self,
- bounds: pathfinder_geometry::rect::RectF,
+ bounds: RectF,
+ visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
@@ -72,8 +65,11 @@ impl Element for Align {
let child_center = self.child.size() / 2.;
let child_target = child_center + child_center * self.alignment;
- self.child
- .paint(bounds.origin() - (child_target - my_target), cx);
+ self.child.paint(
+ bounds.origin() - (child_target - my_target),
+ visible_bounds,
+ cx,
+ );
}
fn dispatch_event(
@@ -9,13 +9,11 @@ use pathfinder_geometry::{
vector::{vec2f, Vector2F},
};
-pub struct Canvas<F>(F)
-where
- F: FnMut(RectF, &mut PaintContext);
+pub struct Canvas<F>(F);
impl<F> Canvas<F>
where
- F: FnMut(RectF, &mut PaintContext),
+ F: FnMut(RectF, RectF, &mut PaintContext),
{
pub fn new(f: F) -> Self {
Self(f)
@@ -24,7 +22,7 @@ where
impl<F> Element for Canvas<F>
where
- F: FnMut(RectF, &mut PaintContext),
+ F: FnMut(RectF, RectF, &mut PaintContext),
{
type LayoutState = ();
type PaintState = ();
@@ -50,18 +48,11 @@ where
fn paint(
&mut self,
bounds: RectF,
+ visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
- self.0(bounds, cx)
- }
-
- fn after_layout(
- &mut self,
- _: Vector2F,
- _: &mut Self::LayoutState,
- _: &mut crate::AfterLayoutContext,
- ) {
+ self.0(bounds, visible_bounds, cx)
}
fn dispatch_event(
@@ -3,8 +3,8 @@ use serde_json::json;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
- json, AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext,
- LayoutContext, PaintContext, SizeConstraint,
+ json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
+ SizeConstraint,
};
pub struct ConstrainedBox {
@@ -67,22 +67,14 @@ impl Element for ConstrainedBox {
(size, ())
}
- fn after_layout(
- &mut self,
- _: Vector2F,
- _: &mut Self::LayoutState,
- cx: &mut AfterLayoutContext,
- ) {
- self.child.after_layout(cx);
- }
-
fn paint(
&mut self,
bounds: RectF,
+ visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
- self.child.paint(bounds.origin(), cx);
+ self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
@@ -10,8 +10,7 @@ use crate::{
},
json::ToJson,
scene::{self, Border, Quad},
- AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
- SizeConstraint,
+ Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
#[derive(Clone, Debug, Default, Deserialize)]
@@ -167,18 +166,10 @@ impl Element for Container {
(child_size + size_buffer, ())
}
- fn after_layout(
- &mut self,
- _: Vector2F,
- _: &mut Self::LayoutState,
- cx: &mut AfterLayoutContext,
- ) {
- self.child.after_layout(cx);
- }
-
fn paint(
&mut self,
bounds: RectF,
+ visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
@@ -208,7 +199,7 @@ impl Element for Container {
self.style.border.left_width(),
self.style.border.top_width(),
);
- self.child.paint(child_origin, cx);
+ self.child.paint(child_origin, visible_bounds, cx);
}
fn dispatch_event(
@@ -251,15 +242,11 @@ impl ToJson for ContainerStyle {
}
}
-#[derive(Clone, Debug, Default, Deserialize)]
+#[derive(Clone, Debug, Default)]
pub struct Margin {
- #[serde(default)]
top: f32,
- #[serde(default)]
left: f32,
- #[serde(default)]
bottom: f32,
- #[serde(default)]
right: f32,
}
@@ -282,18 +269,85 @@ impl ToJson for Margin {
}
}
-#[derive(Clone, Debug, Default, Deserialize)]
+#[derive(Clone, Debug, Default)]
pub struct Padding {
- #[serde(default)]
top: f32,
- #[serde(default)]
left: f32,
- #[serde(default)]
bottom: f32,
- #[serde(default)]
right: f32,
}
+impl<'de> Deserialize<'de> for Padding {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let spacing = Spacing::deserialize(deserializer)?;
+ Ok(match spacing {
+ Spacing::Uniform(size) => Padding {
+ top: size,
+ left: size,
+ bottom: size,
+ right: size,
+ },
+ Spacing::Specific {
+ top,
+ left,
+ bottom,
+ right,
+ } => Padding {
+ top,
+ left,
+ bottom,
+ right,
+ },
+ })
+ }
+}
+
+impl<'de> Deserialize<'de> for Margin {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let spacing = Spacing::deserialize(deserializer)?;
+ Ok(match spacing {
+ Spacing::Uniform(size) => Margin {
+ top: size,
+ left: size,
+ bottom: size,
+ right: size,
+ },
+ Spacing::Specific {
+ top,
+ left,
+ bottom,
+ right,
+ } => Margin {
+ top,
+ left,
+ bottom,
+ right,
+ },
+ })
+ }
+}
+#[derive(Deserialize)]
+#[serde(untagged)]
+enum Spacing {
+ Uniform(f32),
+ Specific {
+ #[serde(default)]
+ top: f32,
+ #[serde(default)]
+ left: f32,
+ #[serde(default)]
+ bottom: f32,
+ #[serde(default)]
+ right: f32,
+ },
+}
+
impl ToJson for Padding {
fn to_json(&self) -> serde_json::Value {
let mut value = json!({});
@@ -6,9 +6,7 @@ use crate::{
json::{json, ToJson},
DebugContext,
};
-use crate::{
- AfterLayoutContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
-};
+use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint};
pub struct Empty;
@@ -41,12 +39,10 @@ impl Element for Empty {
(vec2f(x, y), ())
}
- fn after_layout(&mut self, _: Vector2F, _: &mut Self::LayoutState, _: &mut AfterLayoutContext) {
- }
-
fn paint(
&mut self,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut PaintContext,
) -> Self::PaintState {
@@ -2,8 +2,8 @@ use pathfinder_geometry::rect::RectF;
use serde_json::json;
use crate::{
- geometry::vector::Vector2F, AfterLayoutContext, DebugContext, Element, ElementBox, Event,
- EventContext, LayoutContext, PaintContext, SizeConstraint,
+ geometry::vector::Vector2F, DebugContext, Element, ElementBox, Event, EventContext,
+ LayoutContext, PaintContext, SizeConstraint,
};
pub struct EventHandler {
@@ -41,22 +41,14 @@ impl Element for EventHandler {
(size, ())
}
- fn after_layout(
- &mut self,
- _: Vector2F,
- _: &mut Self::LayoutState,
- cx: &mut AfterLayoutContext,
- ) {
- self.child.after_layout(cx);
- }
-
fn paint(
&mut self,
bounds: RectF,
+ visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
- self.child.paint(bounds.origin(), cx);
+ self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
@@ -2,8 +2,8 @@ use std::{any::Any, f32::INFINITY};
use crate::{
json::{self, ToJson, Value},
- AfterLayoutContext, Axis, DebugContext, Element, ElementBox, Event, EventContext,
- LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
+ Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
+ SizeConstraint, Vector2FExt,
};
use pathfinder_geometry::{
rect::RectF,
@@ -32,11 +32,46 @@ impl Flex {
Self::new(Axis::Vertical)
}
- fn child_flex<'b>(child: &ElementBox) -> Option<f32> {
- child
- .metadata()
- .and_then(|d| d.downcast_ref::<FlexParentData>())
- .map(|data| data.flex)
+ fn layout_flex_children(
+ &mut self,
+ expanded: bool,
+ constraint: SizeConstraint,
+ remaining_space: &mut f32,
+ remaining_flex: &mut f32,
+ cross_axis_max: &mut f32,
+ cx: &mut LayoutContext,
+ ) {
+ let cross_axis = self.axis.invert();
+ for child in &mut self.children {
+ if let Some(metadata) = child.metadata::<FlexParentData>() {
+ if metadata.expanded != expanded {
+ continue;
+ }
+
+ let flex = metadata.flex;
+ let child_max = if *remaining_flex == 0.0 {
+ *remaining_space
+ } else {
+ let space_per_flex = *remaining_space / *remaining_flex;
+ space_per_flex * flex
+ };
+ let child_min = if expanded { child_max } else { 0. };
+ let child_constraint = match self.axis {
+ Axis::Horizontal => SizeConstraint::new(
+ vec2f(child_min, constraint.min.y()),
+ vec2f(child_max, constraint.max.y()),
+ ),
+ Axis::Vertical => SizeConstraint::new(
+ vec2f(constraint.min.x(), child_min),
+ vec2f(constraint.max.x(), child_max),
+ ),
+ };
+ let child_size = child.layout(child_constraint, cx);
+ *remaining_space -= child_size.along(self.axis);
+ *remaining_flex -= flex;
+ *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
+ }
+ }
}
}
@@ -47,7 +82,7 @@ impl Extend<ElementBox> for Flex {
}
impl Element for Flex {
- type LayoutState = ();
+ type LayoutState = bool;
type PaintState = ();
fn layout(
@@ -55,14 +90,14 @@ impl Element for Flex {
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
- let mut total_flex = 0.0;
+ let mut total_flex = None;
let mut fixed_space = 0.0;
let cross_axis = self.axis.invert();
let mut cross_axis_max: f32 = 0.0;
for child in &mut self.children {
- if let Some(flex) = Self::child_flex(&child) {
- total_flex += flex;
+ if let Some(metadata) = child.metadata::<FlexParentData>() {
+ *total_flex.get_or_insert(0.) += metadata.flex;
} else {
let child_constraint = match self.axis {
Axis::Horizontal => SizeConstraint::new(
@@ -80,37 +115,28 @@ impl Element for Flex {
}
}
- let mut size = if total_flex > 0.0 {
+ let mut size = if let Some(mut remaining_flex) = total_flex {
if constraint.max_along(self.axis).is_infinite() {
panic!("flex contains flexible children but has an infinite constraint along the flex axis");
}
let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
- let mut remaining_flex = total_flex;
- for child in &mut self.children {
- if let Some(flex) = Self::child_flex(&child) {
- let child_max = if remaining_flex == 0.0 {
- remaining_space
- } else {
- let space_per_flex = remaining_space / remaining_flex;
- space_per_flex * flex
- };
- let child_constraint = match self.axis {
- Axis::Horizontal => SizeConstraint::new(
- vec2f(0.0, constraint.min.y()),
- vec2f(child_max, constraint.max.y()),
- ),
- Axis::Vertical => SizeConstraint::new(
- vec2f(constraint.min.x(), 0.0),
- vec2f(constraint.max.x(), child_max),
- ),
- };
- let child_size = child.layout(child_constraint, cx);
- remaining_space -= child_size.along(self.axis);
- remaining_flex -= flex;
- cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
- }
- }
+ self.layout_flex_children(
+ false,
+ constraint,
+ &mut remaining_space,
+ &mut remaining_flex,
+ &mut cross_axis_max,
+ cx,
+ );
+ self.layout_flex_children(
+ true,
+ constraint,
+ &mut remaining_space,
+ &mut remaining_flex,
+ &mut cross_axis_max,
+ cx,
+ );
match self.axis {
Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max),
@@ -126,39 +152,44 @@ impl Element for Flex {
if constraint.min.x().is_finite() {
size.set_x(size.x().max(constraint.min.x()));
}
-
if constraint.min.y().is_finite() {
size.set_y(size.y().max(constraint.min.y()));
}
- (size, ())
- }
-
- fn after_layout(
- &mut self,
- _: Vector2F,
- _: &mut Self::LayoutState,
- cx: &mut AfterLayoutContext,
- ) {
- for child in &mut self.children {
- child.after_layout(cx);
+ let mut overflowing = false;
+ if size.x() > constraint.max.x() {
+ size.set_x(constraint.max.x());
+ overflowing = true;
+ }
+ if size.y() > constraint.max.y() {
+ size.set_y(constraint.max.y());
+ overflowing = true;
}
+
+ (size, overflowing)
}
fn paint(
&mut self,
bounds: RectF,
- _: &mut Self::LayoutState,
+ visible_bounds: RectF,
+ overflowing: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
+ if *overflowing {
+ cx.scene.push_layer(Some(bounds));
+ }
let mut child_origin = bounds.origin();
for child in &mut self.children {
- child.paint(child_origin, cx);
+ child.paint(child_origin, visible_bounds, cx);
match self.axis {
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
}
}
+ if *overflowing {
+ cx.scene.pop_layer();
+ }
}
fn dispatch_event(
@@ -194,6 +225,7 @@ impl Element for Flex {
struct FlexParentData {
flex: f32,
+ expanded: bool,
}
pub struct Expanded {
@@ -204,7 +236,10 @@ pub struct Expanded {
impl Expanded {
pub fn new(flex: f32, child: ElementBox) -> Self {
Expanded {
- metadata: FlexParentData { flex },
+ metadata: FlexParentData {
+ flex,
+ expanded: true,
+ },
child,
}
}
@@ -223,22 +258,84 @@ impl Element for Expanded {
(size, ())
}
- fn after_layout(
+ fn paint(
&mut self,
- _: Vector2F,
+ bounds: RectF,
+ visible_bounds: RectF,
_: &mut Self::LayoutState,
- cx: &mut AfterLayoutContext,
- ) {
- self.child.after_layout(cx);
+ cx: &mut PaintContext,
+ ) -> Self::PaintState {
+ self.child.paint(bounds.origin(), visible_bounds, cx)
+ }
+
+ fn dispatch_event(
+ &mut self,
+ event: &Event,
+ _: RectF,
+ _: &mut Self::LayoutState,
+ _: &mut Self::PaintState,
+ cx: &mut EventContext,
+ ) -> bool {
+ self.child.dispatch_event(event, cx)
+ }
+
+ fn metadata(&self) -> Option<&dyn Any> {
+ Some(&self.metadata)
+ }
+
+ fn debug(
+ &self,
+ _: RectF,
+ _: &Self::LayoutState,
+ _: &Self::PaintState,
+ cx: &DebugContext,
+ ) -> Value {
+ json!({
+ "type": "Expanded",
+ "flex": self.metadata.flex,
+ "child": self.child.debug(cx)
+ })
+ }
+}
+
+pub struct Flexible {
+ metadata: FlexParentData,
+ child: ElementBox,
+}
+
+impl Flexible {
+ pub fn new(flex: f32, child: ElementBox) -> Self {
+ Flexible {
+ metadata: FlexParentData {
+ flex,
+ expanded: false,
+ },
+ child,
+ }
+ }
+}
+
+impl Element for Flexible {
+ type LayoutState = ();
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ constraint: SizeConstraint,
+ cx: &mut LayoutContext,
+ ) -> (Vector2F, Self::LayoutState) {
+ let size = self.child.layout(constraint, cx);
+ (size, ())
}
fn paint(
&mut self,
bounds: RectF,
+ visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
- self.child.paint(bounds.origin(), cx)
+ self.child.paint(bounds.origin(), visible_bounds, cx)
}
fn dispatch_event(
@@ -264,7 +361,7 @@ impl Element for Expanded {
cx: &DebugContext,
) -> Value {
json!({
- "type": "Expanded",
+ "type": "Flexible",
"flex": self.metadata.flex,
"child": self.child.debug(cx)
})
@@ -0,0 +1,79 @@
+use crate::{
+ geometry::{rect::RectF, vector::Vector2F},
+ json::json,
+ DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
+ SizeConstraint,
+};
+
+pub struct Hook {
+ child: ElementBox,
+ after_layout: Option<Box<dyn FnMut(Vector2F, &mut LayoutContext)>>,
+}
+
+impl Hook {
+ pub fn new(child: ElementBox) -> Self {
+ Self {
+ child,
+ after_layout: None,
+ }
+ }
+
+ pub fn on_after_layout(
+ mut self,
+ f: impl 'static + FnMut(Vector2F, &mut LayoutContext),
+ ) -> Self {
+ self.after_layout = Some(Box::new(f));
+ self
+ }
+}
+
+impl Element for Hook {
+ type LayoutState = ();
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ constraint: SizeConstraint,
+ cx: &mut LayoutContext,
+ ) -> (Vector2F, Self::LayoutState) {
+ let size = self.child.layout(constraint, cx);
+ if let Some(handler) = self.after_layout.as_mut() {
+ handler(size, cx);
+ }
+ (size, ())
+ }
+
+ fn paint(
+ &mut self,
+ bounds: RectF,
+ visible_bounds: RectF,
+ _: &mut Self::LayoutState,
+ cx: &mut PaintContext,
+ ) {
+ self.child.paint(bounds.origin(), visible_bounds, cx);
+ }
+
+ fn dispatch_event(
+ &mut self,
+ event: &Event,
+ _: RectF,
+ _: &mut Self::LayoutState,
+ _: &mut Self::PaintState,
+ cx: &mut EventContext,
+ ) -> bool {
+ self.child.dispatch_event(event, cx)
+ }
+
+ fn debug(
+ &self,
+ _: RectF,
+ _: &Self::LayoutState,
+ _: &Self::PaintState,
+ cx: &DebugContext,
+ ) -> serde_json::Value {
+ json!({
+ "type": "Hooks",
+ "child": self.child.debug(cx),
+ })
+ }
+}
@@ -1,15 +1,12 @@
use crate::{
- color::Color,
- font_cache::FamilyId,
- fonts::{FontId, TextStyle},
+ fonts::TextStyle,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{ToJson, Value},
- text_layout::Line,
- AfterLayoutContext, DebugContext, Element, Event, EventContext, FontCache, LayoutContext,
- PaintContext, SizeConstraint,
+ text_layout::{Line, RunStyle},
+ DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
use serde::Deserialize;
use serde_json::json;
@@ -17,85 +14,85 @@ use smallvec::{smallvec, SmallVec};
pub struct Label {
text: String,
- family_id: FamilyId,
- font_size: f32,
style: LabelStyle,
highlight_indices: Vec<usize>,
}
-#[derive(Clone, Debug, Default, Deserialize)]
+#[derive(Clone, Debug, Deserialize)]
pub struct LabelStyle {
pub text: TextStyle,
pub highlight_text: Option<TextStyle>,
}
+impl From<TextStyle> for LabelStyle {
+ fn from(text: TextStyle) -> Self {
+ LabelStyle {
+ text,
+ highlight_text: None,
+ }
+ }
+}
+
impl Label {
- pub fn new(text: String, family_id: FamilyId, font_size: f32) -> Self {
+ pub fn new(text: String, style: impl Into<LabelStyle>) -> Self {
Self {
text,
- family_id,
- font_size,
highlight_indices: Default::default(),
- style: Default::default(),
+ style: style.into(),
}
}
- pub fn with_style(mut self, style: &LabelStyle) -> Self {
- self.style = style.clone();
- self
- }
-
- pub fn with_default_color(mut self, color: Color) -> Self {
- self.style.text.color = color;
- self
- }
-
pub fn with_highlights(mut self, indices: Vec<usize>) -> Self {
self.highlight_indices = indices;
self
}
- fn compute_runs(
- &self,
- font_cache: &FontCache,
- font_id: FontId,
- ) -> SmallVec<[(usize, FontId, Color); 8]> {
+ fn compute_runs(&self) -> SmallVec<[(usize, RunStyle); 8]> {
+ let font_id = self.style.text.font_id;
if self.highlight_indices.is_empty() {
- return smallvec![(self.text.len(), font_id, self.style.text.color)];
+ return smallvec![(
+ self.text.len(),
+ RunStyle {
+ font_id,
+ color: self.style.text.color,
+ underline: self.style.text.underline,
+ }
+ )];
}
let highlight_font_id = self
.style
.highlight_text
.as_ref()
- .and_then(|style| {
- font_cache
- .select_font(self.family_id, &style.font_properties)
- .ok()
- })
- .unwrap_or(font_id);
+ .map_or(font_id, |style| style.font_id);
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
let mut runs = SmallVec::new();
+ let highlight_style = self
+ .style
+ .highlight_text
+ .as_ref()
+ .unwrap_or(&self.style.text);
for (char_ix, c) in self.text.char_indices() {
let mut font_id = font_id;
let mut color = self.style.text.color;
+ let mut underline = self.style.text.underline;
if let Some(highlight_ix) = highlight_indices.peek() {
if char_ix == *highlight_ix {
font_id = highlight_font_id;
- color = self
- .style
- .highlight_text
- .as_ref()
- .unwrap_or(&self.style.text)
- .color;
+ color = highlight_style.color;
+ underline = highlight_style.underline;
highlight_indices.next();
}
}
- let push_new_run = if let Some((last_len, last_font_id, last_color)) = runs.last_mut() {
- if font_id == *last_font_id && color == *last_color {
+ let last_run: Option<&mut (usize, RunStyle)> = runs.last_mut();
+ let push_new_run = if let Some((last_len, last_style)) = last_run {
+ if font_id == last_style.font_id
+ && color == last_style.color
+ && underline == last_style.underline
+ {
*last_len += c.len_utf8();
false
} else {
@@ -106,7 +103,14 @@ impl Label {
};
if push_new_run {
- runs.push((c.len_utf8(), font_id, color));
+ runs.push((
+ c.len_utf8(),
+ RunStyle {
+ font_id,
+ color,
+ underline,
+ },
+ ));
}
}
@@ -123,37 +127,31 @@ impl Element for Label {
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
- let font_id = cx
- .font_cache
- .select_font(self.family_id, &self.style.text.font_properties)
- .unwrap();
- let runs = self.compute_runs(&cx.font_cache, font_id);
- let line =
- cx.text_layout_cache
- .layout_str(self.text.as_str(), self.font_size, runs.as_slice());
+ let runs = self.compute_runs();
+ let line = cx.text_layout_cache.layout_str(
+ self.text.as_str(),
+ self.style.text.font_size,
+ runs.as_slice(),
+ );
let size = vec2f(
line.width().max(constraint.min.x()).min(constraint.max.x()),
- cx.font_cache.line_height(font_id, self.font_size).ceil(),
+ cx.font_cache
+ .line_height(self.style.text.font_id, self.style.text.font_size)
+ .ceil(),
);
(size, line)
}
- fn after_layout(&mut self, _: Vector2F, _: &mut Self::LayoutState, _: &mut AfterLayoutContext) {
- }
-
fn paint(
&mut self,
bounds: RectF,
+ visible_bounds: RectF,
line: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
- line.paint(
- bounds.origin(),
- RectF::new(vec2f(0., 0.), bounds.size()),
- cx,
- )
+ line.paint(bounds.origin(), visible_bounds, bounds.size().y(), cx)
}
fn dispatch_event(
@@ -172,15 +170,13 @@ impl Element for Label {
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
- cx: &DebugContext,
+ _: &DebugContext,
) -> Value {
json!({
"type": "Label",
"bounds": bounds.to_json(),
"text": &self.text,
"highlight_indices": self.highlight_indices,
- "font_family": cx.font_cache.family_name(self.family_id).unwrap(),
- "font_size": self.font_size,
"style": self.style.to_json(),
})
}
@@ -200,52 +196,65 @@ impl ToJson for LabelStyle {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::color::Color;
use crate::fonts::{Properties as FontProperties, Weight};
#[crate::test(self)]
fn test_layout_label_with_highlights(cx: &mut crate::MutableAppContext) {
- let menlo = cx.font_cache().load_family(&["Menlo"]).unwrap();
- let menlo_regular = cx
- .font_cache()
- .select_font(menlo, &FontProperties::new())
- .unwrap();
- let menlo_bold = cx
- .font_cache()
- .select_font(menlo, FontProperties::new().weight(Weight::BOLD))
- .unwrap();
- let black = Color::black();
- let red = Color::new(255, 0, 0, 255);
-
- let label = Label::new(".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), menlo, 12.0)
- .with_style(&LabelStyle {
- text: TextStyle {
- color: black,
- font_properties: Default::default(),
- },
- highlight_text: Some(TextStyle {
- color: red,
- font_properties: *FontProperties::new().weight(Weight::BOLD),
- }),
- })
- .with_highlights(vec![
- ".α".len(),
- ".αβ".len(),
- ".αβγδ".len(),
- ".αβγδε.ⓐ".len(),
- ".αβγδε.ⓐⓑ".len(),
- ]);
-
- let runs = label.compute_runs(cx.font_cache().as_ref(), menlo_regular);
+ let default_style = TextStyle::new(
+ "Menlo",
+ 12.,
+ Default::default(),
+ false,
+ Color::black(),
+ cx.font_cache(),
+ )
+ .unwrap();
+ let highlight_style = TextStyle::new(
+ "Menlo",
+ 12.,
+ *FontProperties::new().weight(Weight::BOLD),
+ false,
+ Color::new(255, 0, 0, 255),
+ cx.font_cache(),
+ )
+ .unwrap();
+ let label = Label::new(
+ ".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(),
+ LabelStyle {
+ text: default_style.clone(),
+ highlight_text: Some(highlight_style.clone()),
+ },
+ )
+ .with_highlights(vec![
+ ".α".len(),
+ ".αβ".len(),
+ ".αβγδ".len(),
+ ".αβγδε.ⓐ".len(),
+ ".αβγδε.ⓐⓑ".len(),
+ ]);
+
+ let default_run_style = RunStyle {
+ font_id: default_style.font_id,
+ color: default_style.color,
+ underline: default_style.underline,
+ };
+ let highlight_run_style = RunStyle {
+ font_id: highlight_style.font_id,
+ color: highlight_style.color,
+ underline: highlight_style.underline,
+ };
+ let runs = label.compute_runs();
assert_eq!(
runs.as_slice(),
&[
- (".α".len(), menlo_regular, black),
- ("βγ".len(), menlo_bold, red),
- ("δ".len(), menlo_regular, black),
- ("ε".len(), menlo_bold, red),
- (".ⓐ".len(), menlo_regular, black),
- ("ⓑⓒ".len(), menlo_bold, red),
- ("ⓓⓔ.abcde.".len(), menlo_regular, black),
+ (".α".len(), default_run_style),
+ ("βγ".len(), highlight_run_style),
+ ("δ".len(), default_run_style),
+ ("ε".len(), highlight_run_style),
+ (".ⓐ".len(), default_run_style),
+ ("ⓑⓒ".len(), highlight_run_style),
+ ("ⓓⓔ.abcde.".len(), default_run_style),
]
);
}
@@ -1,30 +1,22 @@
use crate::{
- font_cache::FamilyId,
- fonts::Properties,
+ fonts::TextStyle,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{json, ToJson},
- AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext,
- PaintContext, SizeConstraint,
+ DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
+ SizeConstraint,
};
pub struct LineBox {
child: ElementBox,
- family_id: FamilyId,
- font_size: f32,
- font_properties: Properties,
+ style: TextStyle,
}
impl LineBox {
- pub fn new(family_id: FamilyId, font_size: f32, child: ElementBox) -> Self {
- Self {
- child,
- family_id,
- font_size,
- font_properties: Properties::default(),
- }
+ pub fn new(child: ElementBox, style: TextStyle) -> Self {
+ Self { child, style }
}
}
@@ -37,52 +29,41 @@ impl Element for LineBox {
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
- match cx
+ let line_height = cx
.font_cache
- .select_font(self.family_id, &self.font_properties)
- {
- Ok(font_id) => {
- let line_height = cx.font_cache.line_height(font_id, self.font_size);
- let character_height = cx.font_cache.ascent(font_id, self.font_size)
- + cx.font_cache.descent(font_id, self.font_size);
- let child_max = vec2f(constraint.max.x(), character_height);
- let child_size = self.child.layout(
- SizeConstraint::new(constraint.min.min(child_max), child_max),
- cx,
- );
- let size = vec2f(child_size.x(), line_height);
- (size, (line_height - character_height) / 2.)
- }
- Err(error) => {
- log::error!("can't find font for LineBox: {}", error);
- (constraint.min, 0.0)
- }
- }
- }
-
- fn after_layout(
- &mut self,
- _: Vector2F,
- _: &mut Self::LayoutState,
- cx: &mut AfterLayoutContext,
- ) {
- self.child.after_layout(cx);
+ .line_height(self.style.font_id, self.style.font_size);
+ let character_height = cx
+ .font_cache
+ .ascent(self.style.font_id, self.style.font_size)
+ + cx.font_cache
+ .descent(self.style.font_id, self.style.font_size);
+ let child_max = vec2f(constraint.max.x(), character_height);
+ let child_size = self.child.layout(
+ SizeConstraint::new(constraint.min.min(child_max), child_max),
+ cx,
+ );
+ let size = vec2f(child_size.x(), line_height);
+ (size, (line_height - character_height) / 2.)
}
fn paint(
&mut self,
- bounds: pathfinder_geometry::rect::RectF,
+ bounds: RectF,
+ visible_bounds: RectF,
padding_top: &mut f32,
cx: &mut PaintContext,
) -> Self::PaintState {
- self.child
- .paint(bounds.origin() + vec2f(0., *padding_top), cx);
+ self.child.paint(
+ bounds.origin() + vec2f(0., *padding_top),
+ visible_bounds,
+ cx,
+ );
}
fn dispatch_event(
&mut self,
event: &Event,
- _: pathfinder_geometry::rect::RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
@@ -99,9 +80,7 @@ impl Element for LineBox {
) -> serde_json::Value {
json!({
"bounds": bounds.to_json(),
- "font_family": cx.font_cache.family_name(self.family_id).unwrap(),
- "font_size": self.font_size,
- "font_properties": self.font_properties.to_json(),
+ "style": self.style.to_json(),
"child": self.child.debug(cx),
})
}
@@ -0,0 +1,864 @@
+use crate::{
+ geometry::{
+ rect::RectF,
+ vector::{vec2f, Vector2F},
+ },
+ json::json,
+ sum_tree::{self, Bias, SumTree},
+ DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
+ SizeConstraint,
+};
+use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
+
+pub struct List {
+ state: ListState,
+}
+
+#[derive(Clone)]
+pub struct ListState(Rc<RefCell<StateInner>>);
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum Orientation {
+ Top,
+ Bottom,
+}
+
+struct StateInner {
+ last_layout_width: Option<f32>,
+ render_item: Box<dyn FnMut(usize, &mut LayoutContext) -> ElementBox>,
+ rendered_range: Range<usize>,
+ items: SumTree<ListItem>,
+ logical_scroll_top: Option<ListOffset>,
+ orientation: Orientation,
+ overdraw: f32,
+ scroll_handler: Option<Box<dyn FnMut(Range<usize>, &mut EventContext)>>,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct ListOffset {
+ item_ix: usize,
+ offset_in_item: f32,
+}
+
+#[derive(Clone)]
+enum ListItem {
+ Unrendered,
+ Rendered(ElementRc),
+ Removed(f32),
+}
+
+impl std::fmt::Debug for ListItem {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Unrendered => write!(f, "Unrendered"),
+ Self::Rendered(_) => f.debug_tuple("Rendered").finish(),
+ Self::Removed(height) => f.debug_tuple("Removed").field(height).finish(),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+struct ListItemSummary {
+ count: usize,
+ rendered_count: usize,
+ unrendered_count: usize,
+ height: f32,
+}
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct Count(usize);
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct RenderedCount(usize);
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct UnrenderedCount(usize);
+
+#[derive(Clone, Debug, Default)]
+struct Height(f32);
+
+impl List {
+ pub fn new(state: ListState) -> Self {
+ Self { state }
+ }
+}
+
+impl Element for List {
+ type LayoutState = ListOffset;
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ constraint: SizeConstraint,
+ cx: &mut LayoutContext,
+ ) -> (Vector2F, Self::LayoutState) {
+ let state = &mut *self.state.0.borrow_mut();
+ let size = constraint.max;
+ let mut item_constraint = constraint;
+ item_constraint.min.set_y(0.);
+ item_constraint.max.set_y(f32::INFINITY);
+
+ if cx.refreshing || state.last_layout_width != Some(size.x()) {
+ state.rendered_range = 0..0;
+ state.items = SumTree::from_iter(
+ (0..state.items.summary().count).map(|_| ListItem::Unrendered),
+ &(),
+ )
+ }
+
+ let old_items = state.items.clone();
+ let mut new_items = SumTree::new();
+ let mut rendered_items = VecDeque::new();
+ let mut rendered_height = 0.;
+ let mut scroll_top = state
+ .logical_scroll_top
+ .unwrap_or_else(|| match state.orientation {
+ Orientation::Top => ListOffset {
+ item_ix: 0,
+ offset_in_item: 0.,
+ },
+ Orientation::Bottom => ListOffset {
+ item_ix: state.items.summary().count,
+ offset_in_item: 0.,
+ },
+ });
+
+ // Render items after the scroll top, including those in the trailing overdraw.
+ let mut cursor = old_items.cursor::<Count, ()>();
+ cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+ for (ix, item) in cursor.by_ref().enumerate() {
+ if rendered_height - scroll_top.offset_in_item >= size.y() + state.overdraw {
+ break;
+ }
+
+ let element = state.render_item(scroll_top.item_ix + ix, item, item_constraint, cx);
+ rendered_height += element.size().y();
+ rendered_items.push_back(ListItem::Rendered(element));
+ }
+
+ // Prepare to start walking upward from the item at the scroll top.
+ cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+
+ // If the rendered items do not fill the visible region, then adjust
+ // the scroll top upward.
+ if rendered_height - scroll_top.offset_in_item < size.y() {
+ while rendered_height < size.y() {
+ cursor.prev(&());
+ if let Some(item) = cursor.item() {
+ let element =
+ state.render_item(cursor.seek_start().0, item, item_constraint, cx);
+ rendered_height += element.size().y();
+ rendered_items.push_front(ListItem::Rendered(element));
+ } else {
+ break;
+ }
+ }
+
+ scroll_top = ListOffset {
+ item_ix: cursor.seek_start().0,
+ offset_in_item: rendered_height - size.y(),
+ };
+
+ match state.orientation {
+ Orientation::Top => {
+ scroll_top.offset_in_item = scroll_top.offset_in_item.max(0.);
+ state.logical_scroll_top = Some(scroll_top);
+ }
+ Orientation::Bottom => {
+ scroll_top = ListOffset {
+ item_ix: cursor.seek_start().0,
+ offset_in_item: rendered_height - size.y(),
+ };
+ state.logical_scroll_top = None;
+ }
+ };
+ }
+
+ // Render items in the leading overdraw.
+ let mut leading_overdraw = scroll_top.offset_in_item;
+ while leading_overdraw < state.overdraw {
+ cursor.prev(&());
+ if let Some(item) = cursor.item() {
+ let element = state.render_item(cursor.seek_start().0, item, item_constraint, cx);
+ leading_overdraw += element.size().y();
+ rendered_items.push_front(ListItem::Rendered(element));
+ } else {
+ break;
+ }
+ }
+
+ let new_rendered_range =
+ cursor.seek_start().0..(cursor.seek_start().0 + rendered_items.len());
+
+ let mut cursor = old_items.cursor::<Count, ()>();
+
+ if state.rendered_range.start < new_rendered_range.start {
+ new_items.push_tree(
+ cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
+ &(),
+ );
+ let remove_to = state.rendered_range.end.min(new_rendered_range.start);
+ while cursor.seek_start().0 < remove_to {
+ new_items.push(cursor.item().unwrap().remove(), &());
+ cursor.next(&());
+ }
+ }
+ new_items.push_tree(
+ cursor.slice(&Count(new_rendered_range.start), Bias::Right, &()),
+ &(),
+ );
+
+ new_items.extend(rendered_items, &());
+ cursor.seek(&Count(new_rendered_range.end), Bias::Right, &());
+
+ if new_rendered_range.end < state.rendered_range.start {
+ new_items.push_tree(
+ cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
+ &(),
+ );
+ }
+ while cursor.seek_start().0 < state.rendered_range.end {
+ new_items.push(cursor.item().unwrap().remove(), &());
+ cursor.next(&());
+ }
+
+ new_items.push_tree(cursor.suffix(&()), &());
+
+ state.items = new_items;
+ state.rendered_range = new_rendered_range;
+ state.last_layout_width = Some(size.x());
+ (size, scroll_top)
+ }
+
+ fn paint(
+ &mut self,
+ bounds: RectF,
+ visible_bounds: RectF,
+ scroll_top: &mut ListOffset,
+ cx: &mut PaintContext,
+ ) {
+ cx.scene.push_layer(Some(bounds));
+
+ let state = &mut *self.state.0.borrow_mut();
+ for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
+ element.paint(origin, visible_bounds, cx);
+ }
+
+ cx.scene.pop_layer();
+ }
+
+ fn dispatch_event(
+ &mut self,
+ event: &Event,
+ bounds: RectF,
+ scroll_top: &mut ListOffset,
+ _: &mut (),
+ cx: &mut EventContext,
+ ) -> bool {
+ let mut handled = false;
+
+ let mut state = self.state.0.borrow_mut();
+ for (mut element, _) in state.visible_elements(bounds, scroll_top) {
+ handled = element.dispatch_event(event, cx) || handled;
+ }
+
+ match event {
+ Event::ScrollWheel {
+ position,
+ delta,
+ precise,
+ } => {
+ if bounds.contains_point(*position) {
+ if state.scroll(scroll_top, bounds.height(), *delta, *precise, cx) {
+ handled = true;
+ }
+ }
+ }
+ _ => {}
+ }
+
+ handled
+ }
+
+ fn debug(
+ &self,
+ bounds: RectF,
+ scroll_top: &Self::LayoutState,
+ _: &(),
+ cx: &DebugContext,
+ ) -> serde_json::Value {
+ let state = self.state.0.borrow_mut();
+ let visible_elements = state
+ .visible_elements(bounds, scroll_top)
+ .map(|e| e.0.debug(cx))
+ .collect::<Vec<_>>();
+ let visible_range = scroll_top.item_ix..(scroll_top.item_ix + visible_elements.len());
+ json!({
+ "visible_range": visible_range,
+ "visible_elements": visible_elements,
+ "scroll_top": state.logical_scroll_top.map(|top| (top.item_ix, top.offset_in_item)),
+ })
+ }
+}
+
+impl ListState {
+ pub fn new<F>(
+ element_count: usize,
+ orientation: Orientation,
+ overdraw: f32,
+ render_item: F,
+ ) -> Self
+ where
+ F: 'static + FnMut(usize, &mut LayoutContext) -> ElementBox,
+ {
+ let mut items = SumTree::new();
+ items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
+ Self(Rc::new(RefCell::new(StateInner {
+ last_layout_width: None,
+ render_item: Box::new(render_item),
+ rendered_range: 0..0,
+ items,
+ logical_scroll_top: None,
+ orientation,
+ overdraw,
+ scroll_handler: None,
+ })))
+ }
+
+ pub fn reset(&self, element_count: usize) {
+ let state = &mut *self.0.borrow_mut();
+ state.rendered_range = 0..0;
+ state.logical_scroll_top = None;
+ state.items = SumTree::new();
+ state
+ .items
+ .extend((0..element_count).map(|_| ListItem::Unrendered), &());
+ }
+
+ pub fn splice(&self, old_range: Range<usize>, count: usize) {
+ let state = &mut *self.0.borrow_mut();
+
+ if let Some(ListOffset {
+ item_ix,
+ offset_in_item,
+ }) = state.logical_scroll_top.as_mut()
+ {
+ if old_range.contains(item_ix) {
+ *item_ix = old_range.start;
+ *offset_in_item = 0.;
+ } else if old_range.end <= *item_ix {
+ *item_ix = *item_ix - (old_range.end - old_range.start) + count;
+ }
+ }
+
+ let new_end = old_range.start + count;
+ if old_range.start < state.rendered_range.start {
+ state.rendered_range.start =
+ new_end + state.rendered_range.start.saturating_sub(old_range.end);
+ }
+ if old_range.start < state.rendered_range.end {
+ state.rendered_range.end =
+ new_end + state.rendered_range.end.saturating_sub(old_range.end);
+ }
+
+ let mut old_heights = state.items.cursor::<Count, ()>();
+ let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
+ old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
+
+ new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
+ new_heights.push_tree(old_heights.suffix(&()), &());
+ drop(old_heights);
+ state.items = new_heights;
+ }
+
+ pub fn set_scroll_handler(
+ &mut self,
+ handler: impl FnMut(Range<usize>, &mut EventContext) + 'static,
+ ) {
+ self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
+ }
+}
+
+impl StateInner {
+ fn render_item(
+ &mut self,
+ ix: usize,
+ existing_item: &ListItem,
+ constraint: SizeConstraint,
+ cx: &mut LayoutContext,
+ ) -> ElementRc {
+ if let ListItem::Rendered(element) = existing_item {
+ element.clone()
+ } else {
+ let mut element = (self.render_item)(ix, cx);
+ element.layout(constraint, cx);
+ element.into()
+ }
+ }
+
+ fn visible_range(&self, height: f32, scroll_top: &ListOffset) -> Range<usize> {
+ let mut cursor = self.items.cursor::<Count, Height>();
+ cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+ let start_y = cursor.sum_start().0 + scroll_top.offset_in_item;
+ let mut cursor = cursor.swap_dimensions();
+ cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
+ scroll_top.item_ix..cursor.sum_start().0 + 1
+ }
+
+ fn visible_elements<'a>(
+ &'a self,
+ bounds: RectF,
+ scroll_top: &ListOffset,
+ ) -> impl Iterator<Item = (ElementRc, Vector2F)> + 'a {
+ let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
+ let mut cursor = self.items.cursor::<Count, ()>();
+ cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+ std::iter::from_fn(move || {
+ while let Some(item) = cursor.item() {
+ if item_origin.y() > bounds.max_y() {
+ break;
+ }
+
+ if let ListItem::Rendered(element) = item {
+ let result = (element.clone(), item_origin);
+ item_origin.set_y(item_origin.y() + element.size().y());
+ cursor.next(&());
+ return Some(result);
+ }
+
+ cursor.next(&());
+ }
+
+ None
+ })
+ }
+
+ fn scroll(
+ &mut self,
+ scroll_top: &ListOffset,
+ height: f32,
+ mut delta: Vector2F,
+ precise: bool,
+ cx: &mut EventContext,
+ ) -> bool {
+ if !precise {
+ delta *= 20.;
+ }
+
+ let scroll_max = (self.items.summary().height - height).max(0.);
+ let new_scroll_top = (self.scroll_top(scroll_top) - delta.y())
+ .max(0.)
+ .min(scroll_max);
+
+ if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max {
+ self.logical_scroll_top = None;
+ } else {
+ let mut cursor = self.items.cursor::<Height, Count>();
+ cursor.seek(&Height(new_scroll_top), Bias::Right, &());
+ let item_ix = cursor.sum_start().0;
+ let offset_in_item = new_scroll_top - cursor.seek_start().0;
+ self.logical_scroll_top = Some(ListOffset {
+ item_ix,
+ offset_in_item,
+ });
+ }
+
+ if self.scroll_handler.is_some() {
+ let visible_range = self.visible_range(height, scroll_top);
+ self.scroll_handler.as_mut().unwrap()(visible_range, cx);
+ }
+ cx.notify();
+
+ true
+ }
+
+ fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
+ let mut cursor = self.items.cursor::<Count, Height>();
+ cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
+ cursor.sum_start().0 + logical_scroll_top.offset_in_item
+ }
+}
+
+impl ListItem {
+ fn remove(&self) -> Self {
+ match self {
+ ListItem::Unrendered => ListItem::Unrendered,
+ ListItem::Rendered(element) => ListItem::Removed(element.size().y()),
+ ListItem::Removed(height) => ListItem::Removed(*height),
+ }
+ }
+}
+
+impl sum_tree::Item for ListItem {
+ type Summary = ListItemSummary;
+
+ fn summary(&self) -> Self::Summary {
+ match self {
+ ListItem::Unrendered => ListItemSummary {
+ count: 1,
+ rendered_count: 0,
+ unrendered_count: 1,
+ height: 0.,
+ },
+ ListItem::Rendered(element) => ListItemSummary {
+ count: 1,
+ rendered_count: 1,
+ unrendered_count: 0,
+ height: element.size().y(),
+ },
+ ListItem::Removed(height) => ListItemSummary {
+ count: 1,
+ rendered_count: 0,
+ unrendered_count: 1,
+ height: *height,
+ },
+ }
+ }
+}
+
+impl sum_tree::Summary for ListItemSummary {
+ type Context = ();
+
+ fn add_summary(&mut self, summary: &Self, _: &()) {
+ self.count += summary.count;
+ self.rendered_count += summary.rendered_count;
+ self.unrendered_count += summary.unrendered_count;
+ self.height += summary.height;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for ListItemSummary {
+ fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+ sum_tree::Summary::add_summary(self, summary, &());
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
+ fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+ self.0 += summary.count;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
+ fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+ self.0 += summary.rendered_count;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
+ fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+ self.0 += summary.unrendered_count;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
+ fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+ self.0 += summary.height;
+ }
+}
+
+impl<'a> sum_tree::SeekDimension<'a, ListItemSummary> for Height {
+ fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
+ self.0.partial_cmp(&other.0).unwrap()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::geometry::vector::vec2f;
+ use rand::prelude::*;
+ use std::env;
+
+ #[crate::test(self)]
+ fn test_layout(cx: &mut crate::MutableAppContext) {
+ let mut presenter = cx.build_presenter(0, 0.);
+ let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
+
+ let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)]));
+ let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, {
+ let elements = elements.clone();
+ move |ix, _| {
+ let (id, height) = elements.borrow()[ix];
+ TestElement::new(id, height).boxed()
+ }
+ });
+
+ let mut list = List::new(state.clone());
+ let (size, _) = list.layout(constraint, &mut presenter.build_layout_context(false, cx));
+ assert_eq!(size, vec2f(100., 40.));
+ assert_eq!(
+ state.0.borrow().items.summary(),
+ ListItemSummary {
+ count: 3,
+ rendered_count: 3,
+ unrendered_count: 0,
+ height: 150.
+ }
+ );
+
+ state.0.borrow_mut().scroll(
+ &ListOffset {
+ item_ix: 0,
+ offset_in_item: 0.,
+ },
+ 40.,
+ vec2f(0., 54.),
+ true,
+ &mut presenter.build_event_context(cx),
+ );
+ let (_, logical_scroll_top) =
+ list.layout(constraint, &mut presenter.build_layout_context(false, cx));
+ assert_eq!(
+ logical_scroll_top,
+ ListOffset {
+ item_ix: 2,
+ offset_in_item: 4.
+ }
+ );
+ assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 54.);
+
+ elements.borrow_mut().splice(1..2, vec![(3, 40.), (4, 50.)]);
+ elements.borrow_mut().push((5, 60.));
+ state.splice(1..2, 2);
+ state.splice(4..4, 1);
+ assert_eq!(
+ state.0.borrow().items.summary(),
+ ListItemSummary {
+ count: 5,
+ rendered_count: 2,
+ unrendered_count: 3,
+ height: 120.
+ }
+ );
+
+ let (size, logical_scroll_top) =
+ list.layout(constraint, &mut presenter.build_layout_context(false, cx));
+ assert_eq!(size, vec2f(100., 40.));
+ assert_eq!(
+ state.0.borrow().items.summary(),
+ ListItemSummary {
+ count: 5,
+ rendered_count: 5,
+ unrendered_count: 0,
+ height: 270.
+ }
+ );
+ assert_eq!(
+ logical_scroll_top,
+ ListOffset {
+ item_ix: 3,
+ offset_in_item: 4.
+ }
+ );
+ assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.);
+ }
+
+ #[crate::test(self, iterations = 10000, 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"))
+ .unwrap_or(10);
+
+ let mut presenter = cx.build_presenter(0, 0.);
+ let mut next_id = 0;
+ let elements = Rc::new(RefCell::new(
+ (0..rng.gen_range(0..=20))
+ .map(|_| {
+ let id = next_id;
+ next_id += 1;
+ (id, rng.gen_range(0..=200) as f32 / 2.0)
+ })
+ .collect::<Vec<_>>(),
+ ));
+ let orientation = *[Orientation::Top, Orientation::Bottom]
+ .choose(&mut rng)
+ .unwrap();
+ let overdraw = rng.gen_range(1..=100) as f32;
+ let state = ListState::new(elements.borrow().len(), orientation, overdraw, {
+ let elements = elements.clone();
+ move |ix, _| {
+ let (id, height) = elements.borrow()[ix];
+ TestElement::new(id, height).boxed()
+ }
+ });
+
+ let mut width = rng.gen_range(0..=2000) as f32 / 2.;
+ let mut height = rng.gen_range(0..=2000) as f32 / 2.;
+ log::info!("orientation: {:?}", orientation);
+ log::info!("overdraw: {}", overdraw);
+ log::info!("elements: {:?}", elements.borrow());
+ log::info!("size: ({:?}, {:?})", width, height);
+ log::info!("==================");
+
+ let mut last_logical_scroll_top = None;
+ for _ in 0..operations {
+ match rng.gen_range(0..=100) {
+ 0..=29 if last_logical_scroll_top.is_some() => {
+ let delta = vec2f(0., rng.gen_range(-overdraw..=overdraw));
+ log::info!(
+ "Scrolling by {:?}, previous scroll top: {:?}",
+ delta,
+ last_logical_scroll_top.unwrap()
+ );
+ state.0.borrow_mut().scroll(
+ last_logical_scroll_top.as_ref().unwrap(),
+ height,
+ delta,
+ true,
+ &mut presenter.build_event_context(cx),
+ );
+ }
+ 30..=34 => {
+ width = rng.gen_range(0..=2000) as f32 / 2.;
+ log::info!("changing width: {:?}", width);
+ }
+ 35..=54 => {
+ height = rng.gen_range(0..=1000) as f32 / 2.;
+ log::info!("changing height: {:?}", height);
+ }
+ _ => {
+ let mut elements = elements.borrow_mut();
+ let end_ix = rng.gen_range(0..=elements.len());
+ let start_ix = rng.gen_range(0..=end_ix);
+ let new_elements = (0..rng.gen_range(0..10))
+ .map(|_| {
+ let id = next_id;
+ next_id += 1;
+ (id, rng.gen_range(0..=200) as f32 / 2.)
+ })
+ .collect::<Vec<_>>();
+ log::info!("splice({:?}, {:?})", start_ix..end_ix, new_elements);
+ state.splice(start_ix..end_ix, new_elements.len());
+ elements.splice(start_ix..end_ix, new_elements);
+ for (ix, item) in state.0.borrow().items.cursor::<(), ()>().enumerate() {
+ if let ListItem::Rendered(element) = item {
+ let (expected_id, _) = elements[ix];
+ element.with_metadata(|metadata: Option<&usize>| {
+ assert_eq!(*metadata.unwrap(), expected_id);
+ });
+ }
+ }
+ }
+ }
+
+ let mut list = List::new(state.clone());
+ let (size, logical_scroll_top) = list.layout(
+ SizeConstraint::new(vec2f(0., 0.), vec2f(width, height)),
+ &mut presenter.build_layout_context(false, cx),
+ );
+ assert_eq!(size, vec2f(width, height));
+ last_logical_scroll_top = Some(logical_scroll_top);
+
+ let state = state.0.borrow();
+ log::info!("items {:?}", state.items.items(&()));
+
+ let scroll_top = state.scroll_top(&logical_scroll_top);
+ let rendered_top = (scroll_top - overdraw).max(0.);
+ let rendered_bottom = scroll_top + height + overdraw;
+ let mut item_top = 0.;
+
+ log::info!(
+ "rendered top {:?}, rendered bottom {:?}, scroll top {:?}",
+ rendered_top,
+ rendered_bottom,
+ scroll_top,
+ );
+
+ let mut first_rendered_element_top = None;
+ let mut last_rendered_element_bottom = None;
+ assert_eq!(state.items.summary().count, elements.borrow().len());
+ for (ix, item) in state.items.cursor::<(), ()>().enumerate() {
+ match item {
+ ListItem::Unrendered => {
+ let item_bottom = item_top;
+ assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
+ item_top = item_bottom;
+ }
+ ListItem::Removed(height) => {
+ let (id, expected_height) = elements.borrow()[ix];
+ assert_eq!(
+ *height, expected_height,
+ "element {} height didn't match",
+ id
+ );
+ let item_bottom = item_top + height;
+ assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
+ item_top = item_bottom;
+ }
+ ListItem::Rendered(element) => {
+ let (expected_id, expected_height) = elements.borrow()[ix];
+ element.with_metadata(|metadata: Option<&usize>| {
+ assert_eq!(*metadata.unwrap(), expected_id);
+ });
+ assert_eq!(element.size().y(), expected_height);
+ let item_bottom = item_top + element.size().y();
+ first_rendered_element_top.get_or_insert(item_top);
+ last_rendered_element_bottom = Some(item_bottom);
+ assert!(item_bottom > rendered_top || item_top < rendered_bottom);
+ item_top = item_bottom;
+ }
+ }
+ }
+
+ match orientation {
+ Orientation::Top => {
+ if let Some(first_rendered_element_top) = first_rendered_element_top {
+ assert!(first_rendered_element_top <= scroll_top);
+ }
+ }
+ Orientation::Bottom => {
+ if let Some(last_rendered_element_bottom) = last_rendered_element_bottom {
+ assert!(last_rendered_element_bottom >= scroll_top + height);
+ }
+ }
+ }
+ }
+ }
+
+ struct TestElement {
+ id: usize,
+ size: Vector2F,
+ }
+
+ impl TestElement {
+ fn new(id: usize, height: f32) -> Self {
+ Self {
+ id,
+ size: vec2f(100., height),
+ }
+ }
+ }
+
+ impl Element for TestElement {
+ type LayoutState = ();
+ type PaintState = ();
+
+ fn layout(&mut self, _: SizeConstraint, _: &mut LayoutContext) -> (Vector2F, ()) {
+ (self.size, ())
+ }
+
+ fn paint(&mut self, _: RectF, _: RectF, _: &mut (), _: &mut PaintContext) {
+ todo!()
+ }
+
+ fn dispatch_event(
+ &mut self,
+ _: &Event,
+ _: RectF,
+ _: &mut (),
+ _: &mut (),
+ _: &mut EventContext,
+ ) -> bool {
+ todo!()
+ }
+
+ fn debug(&self, _: RectF, _: &(), _: &(), _: &DebugContext) -> serde_json::Value {
+ self.id.into()
+ }
+
+ fn metadata(&self) -> Option<&dyn std::any::Any> {
+ Some(&self.id)
+ }
+ }
+}
@@ -1,42 +1,69 @@
+use std::ops::DerefMut;
+
use crate::{
geometry::{rect::RectF, vector::Vector2F},
- AfterLayoutContext, AppContext, DebugContext, Element, ElementBox, Event, EventContext,
- LayoutContext, PaintContext, SizeConstraint, ValueHandle,
+ platform::CursorStyle,
+ CursorStyleHandle, DebugContext, Element, ElementBox, ElementStateHandle, ElementStateId,
+ Event, EventContext, LayoutContext, MutableAppContext, PaintContext, SizeConstraint,
};
use serde_json::json;
pub struct MouseEventHandler {
- state: ValueHandle<MouseState>,
+ state: ElementStateHandle<MouseState>,
child: ElementBox,
+ cursor_style: Option<CursorStyle>,
+ mouse_down_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
click_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
+ drag_handler: Option<Box<dyn FnMut(Vector2F, &mut EventContext)>>,
}
-#[derive(Clone, Copy, Debug, Default)]
+#[derive(Default)]
pub struct MouseState {
pub hovered: bool,
pub clicked: bool,
+ prev_drag_position: Option<Vector2F>,
+ cursor_style_handle: Option<CursorStyleHandle>,
}
impl MouseEventHandler {
- pub fn new<Tag, F>(id: usize, cx: &AppContext, render_child: F) -> Self
+ pub fn new<Tag, F, C, Id>(id: Id, cx: &mut C, render_child: F) -> Self
where
Tag: 'static,
- F: FnOnce(MouseState) -> ElementBox,
+ F: FnOnce(&MouseState, &mut C) -> ElementBox,
+ C: DerefMut<Target = MutableAppContext>,
+ Id: Into<ElementStateId>,
{
- let state_handle = cx.value::<Tag, _>(id);
- let state = state_handle.read(cx.as_ref(), |state| *state);
- let child = render_child(state);
+ let state_handle = cx.element_state::<Tag, _>(id.into());
+ let child = state_handle.update(cx, |state, cx| render_child(state, cx));
Self {
state: state_handle,
child,
+ cursor_style: None,
+ mouse_down_handler: None,
click_handler: None,
+ drag_handler: None,
}
}
+ pub fn with_cursor_style(mut self, cursor: CursorStyle) -> Self {
+ self.cursor_style = Some(cursor);
+ self
+ }
+
+ pub fn on_mouse_down(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self {
+ self.mouse_down_handler = Some(Box::new(handler));
+ self
+ }
+
pub fn on_click(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self {
self.click_handler = Some(Box::new(handler));
self
}
+
+ pub fn on_drag(mut self, handler: impl FnMut(Vector2F, &mut EventContext) + 'static) -> Self {
+ self.drag_handler = Some(Box::new(handler));
+ self
+ }
}
impl Element for MouseEventHandler {
@@ -51,22 +78,14 @@ impl Element for MouseEventHandler {
(self.child.layout(constraint, cx), ())
}
- fn after_layout(
- &mut self,
- _: Vector2F,
- _: &mut Self::LayoutState,
- cx: &mut AfterLayoutContext,
- ) {
- self.child.after_layout(cx);
- }
-
fn paint(
&mut self,
bounds: RectF,
+ visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
- self.child.paint(bounds.origin(), cx);
+ self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
@@ -77,33 +96,58 @@ impl Element for MouseEventHandler {
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
+ let cursor_style = self.cursor_style;
+ let mouse_down_handler = self.mouse_down_handler.as_mut();
let click_handler = self.click_handler.as_mut();
+ let drag_handler = self.drag_handler.as_mut();
let handled_in_child = self.child.dispatch_event(event, cx);
self.state.update(cx, |state, cx| match event {
- Event::MouseMoved { position } => {
- let mouse_in = bounds.contains_point(*position);
- if state.hovered != mouse_in {
- state.hovered = mouse_in;
- cx.notify();
- true
- } else {
- handled_in_child
+ Event::MouseMoved {
+ position,
+ left_mouse_down,
+ } => {
+ if !left_mouse_down {
+ let mouse_in = bounds.contains_point(*position);
+ if state.hovered != mouse_in {
+ state.hovered = mouse_in;
+ if let Some(cursor_style) = cursor_style {
+ if !state.clicked {
+ if state.hovered {
+ state.cursor_style_handle =
+ Some(cx.set_cursor_style(cursor_style));
+ } else {
+ state.cursor_style_handle = None;
+ }
+ }
+ }
+ cx.notify();
+ return true;
+ }
}
+ handled_in_child
}
Event::LeftMouseDown { position, .. } => {
if !handled_in_child && bounds.contains_point(*position) {
state.clicked = true;
+ state.prev_drag_position = Some(*position);
cx.notify();
+ if let Some(handler) = mouse_down_handler {
+ handler(cx);
+ }
true
} else {
handled_in_child
}
}
Event::LeftMouseUp { position, .. } => {
+ state.prev_drag_position = None;
if !handled_in_child && state.clicked {
state.clicked = false;
+ if !state.hovered {
+ state.cursor_style_handle = None;
+ }
cx.notify();
if let Some(handler) = click_handler {
if bounds.contains_point(*position) {
@@ -115,6 +159,20 @@ impl Element for MouseEventHandler {
handled_in_child
}
}
+ Event::LeftMouseDragged { position, .. } => {
+ if !handled_in_child && state.clicked {
+ let prev_drag_position = state.prev_drag_position.replace(*position);
+ if let Some((handler, prev_position)) = drag_handler.zip(prev_drag_position) {
+ let delta = *position - prev_position;
+ if !delta.is_zero() {
+ (handler)(delta, cx);
+ }
+ }
+ true
+ } else {
+ handled_in_child
+ }
+ }
_ => handled_in_child,
})
}
@@ -0,0 +1,63 @@
+use crate::{
+ geometry::{rect::RectF, vector::Vector2F},
+ DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
+ SizeConstraint,
+};
+
+pub struct Overlay {
+ child: ElementBox,
+}
+
+impl Overlay {
+ pub fn new(child: ElementBox) -> Self {
+ Self { child }
+ }
+}
+
+impl Element for Overlay {
+ type LayoutState = Vector2F;
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ constraint: SizeConstraint,
+ cx: &mut LayoutContext,
+ ) -> (Vector2F, Self::LayoutState) {
+ let size = self.child.layout(constraint, cx);
+ (Vector2F::zero(), size)
+ }
+
+ fn paint(
+ &mut self,
+ bounds: RectF,
+ _: RectF,
+ size: &mut Self::LayoutState,
+ cx: &mut PaintContext,
+ ) {
+ let bounds = RectF::new(bounds.origin(), *size);
+ cx.scene.push_stacking_context(None);
+ self.child.paint(bounds.origin(), bounds, cx);
+ cx.scene.pop_stacking_context();
+ }
+
+ fn dispatch_event(
+ &mut self,
+ event: &Event,
+ _: RectF,
+ _: &mut Self::LayoutState,
+ _: &mut Self::PaintState,
+ cx: &mut EventContext,
+ ) -> bool {
+ self.child.dispatch_event(event, cx)
+ }
+
+ fn debug(
+ &self,
+ _: RectF,
+ _: &Self::LayoutState,
+ _: &Self::PaintState,
+ cx: &DebugContext,
+ ) -> serde_json::Value {
+ self.child.debug(cx)
+ }
+}
@@ -1,8 +1,8 @@
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::{self, json, ToJson},
- AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext,
- PaintContext, SizeConstraint,
+ DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
+ SizeConstraint,
};
pub struct Stack {
@@ -33,26 +33,16 @@ impl Element for Stack {
(size, ())
}
- fn after_layout(
- &mut self,
- _: Vector2F,
- _: &mut Self::LayoutState,
- cx: &mut AfterLayoutContext,
- ) {
- for child in &mut self.children {
- child.after_layout(cx);
- }
- }
-
fn paint(
&mut self,
bounds: RectF,
+ visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
for child in &mut self.children {
cx.scene.push_layer(None);
- child.paint(bounds.origin(), cx);
+ child.paint(bounds.origin(), visible_bounds, cx);
cx.scene.pop_layer();
}
}
@@ -8,8 +8,7 @@ use crate::{
rect::RectF,
vector::{vec2f, Vector2F},
},
- scene, AfterLayoutContext, DebugContext, Element, Event, EventContext, LayoutContext,
- PaintContext, SizeConstraint,
+ scene, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
pub struct Svg {
@@ -66,10 +65,13 @@ impl Element for Svg {
}
}
- fn after_layout(&mut self, _: Vector2F, _: &mut Self::LayoutState, _: &mut AfterLayoutContext) {
- }
-
- fn paint(&mut self, bounds: RectF, svg: &mut Self::LayoutState, cx: &mut PaintContext) {
+ fn paint(
+ &mut self,
+ bounds: RectF,
+ _visible_bounds: RectF,
+ svg: &mut Self::LayoutState,
+ cx: &mut PaintContext,
+ ) {
if let Some(svg) = svg.clone() {
cx.scene.push_icon(scene::Icon {
bounds,
@@ -0,0 +1,131 @@
+use crate::{
+ color::Color,
+ fonts::TextStyle,
+ geometry::{
+ rect::RectF,
+ vector::{vec2f, Vector2F},
+ },
+ json::{ToJson, Value},
+ text_layout::{Line, ShapedBoundary},
+ DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
+};
+use serde_json::json;
+
+pub struct Text {
+ text: String,
+ style: TextStyle,
+}
+
+pub struct LayoutState {
+ lines: Vec<(Line, Vec<ShapedBoundary>)>,
+ line_height: f32,
+}
+
+impl Text {
+ pub fn new(text: String, style: TextStyle) -> Self {
+ Self { text, style }
+ }
+
+ pub fn with_default_color(mut self, color: Color) -> Self {
+ self.style.color = color;
+ self
+ }
+}
+
+impl Element for Text {
+ type LayoutState = LayoutState;
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ constraint: SizeConstraint,
+ cx: &mut LayoutContext,
+ ) -> (Vector2F, Self::LayoutState) {
+ let font_id = self.style.font_id;
+ let line_height = cx.font_cache.line_height(font_id, self.style.font_size);
+
+ let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
+ let mut lines = Vec::new();
+ let mut line_count = 0;
+ let mut max_line_width = 0_f32;
+ for line in self.text.lines() {
+ let shaped_line = cx.text_layout_cache.layout_str(
+ line,
+ self.style.font_size,
+ &[(line.len(), self.style.to_run())],
+ );
+ let wrap_boundaries = wrapper
+ .wrap_shaped_line(line, &shaped_line, constraint.max.x())
+ .collect::<Vec<_>>();
+
+ max_line_width = max_line_width.max(shaped_line.width());
+ line_count += wrap_boundaries.len() + 1;
+ lines.push((shaped_line, wrap_boundaries));
+ }
+
+ let size = vec2f(
+ max_line_width
+ .ceil()
+ .max(constraint.min.x())
+ .min(constraint.max.x()),
+ (line_height * line_count as f32).ceil(),
+ );
+ (size, LayoutState { lines, line_height })
+ }
+
+ fn paint(
+ &mut self,
+ bounds: RectF,
+ visible_bounds: RectF,
+ layout: &mut Self::LayoutState,
+ cx: &mut PaintContext,
+ ) -> Self::PaintState {
+ let mut origin = bounds.origin();
+ for (line, wrap_boundaries) in &layout.lines {
+ let wrapped_line_boundaries = RectF::new(
+ origin,
+ vec2f(
+ bounds.width(),
+ (wrap_boundaries.len() + 1) as f32 * layout.line_height,
+ ),
+ );
+
+ if wrapped_line_boundaries.intersects(visible_bounds) {
+ line.paint_wrapped(
+ origin,
+ visible_bounds,
+ layout.line_height,
+ wrap_boundaries.iter().copied(),
+ cx,
+ );
+ }
+ origin.set_y(wrapped_line_boundaries.max_y());
+ }
+ }
+
+ 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,
+ ) -> Value {
+ json!({
+ "type": "Text",
+ "bounds": bounds.to_json(),
+ "text": &self.text,
+ "style": self.style.to_json(),
+ })
+ }
+}
@@ -1,13 +1,11 @@
-use super::{
- AfterLayoutContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
-};
+use super::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint};
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{self, json},
- AppContext, ElementBox,
+ ElementBox, MutableAppContext,
};
use json::ToJson;
use parking_lot::Mutex;
@@ -40,7 +38,7 @@ pub struct LayoutState {
pub struct UniformList<F>
where
- F: Fn(Range<usize>, &mut Vec<ElementBox>, &AppContext),
+ F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut MutableAppContext),
{
state: UniformListState,
item_count: usize,
@@ -49,7 +47,7 @@ where
impl<F> UniformList<F>
where
- F: Fn(Range<usize>, &mut Vec<ElementBox>, &AppContext),
+ F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut MutableAppContext),
{
pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self {
Self {
@@ -62,13 +60,13 @@ where
fn scroll(
&self,
_: Vector2F,
- delta: Vector2F,
+ mut delta: Vector2F,
precise: bool,
scroll_max: f32,
cx: &mut EventContext,
) -> bool {
if !precise {
- todo!("still need to handle non-precise scroll events from a mouse wheel");
+ delta *= 20.;
}
let mut state = self.state.0.lock();
@@ -104,7 +102,7 @@ where
impl<F> Element for UniformList<F>
where
- F: Fn(Range<usize>, &mut Vec<ElementBox>, &AppContext),
+ F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut MutableAppContext),
{
type LayoutState = LayoutState;
type PaintState = ();
@@ -152,6 +150,8 @@ where
for item in &mut items {
item.layout(item_constraint, cx);
}
+ } else {
+ size = constraint.min;
}
(
@@ -164,20 +164,10 @@ where
)
}
- fn after_layout(
- &mut self,
- _: Vector2F,
- layout: &mut Self::LayoutState,
- cx: &mut AfterLayoutContext,
- ) {
- for item in &mut layout.items {
- item.after_layout(cx);
- }
- }
-
fn paint(
&mut self,
bounds: RectF,
+ visible_bounds: RectF,
layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
@@ -187,7 +177,7 @@ where
bounds.origin() - vec2f(0.0, self.state.scroll_top() % layout.item_height);
for item in &mut layout.items {
- item.paint(item_origin, cx);
+ item.paint(item_origin, visible_bounds, cx);
item_origin += vec2f(0.0, layout.item_height);
}
@@ -122,9 +122,14 @@ impl Deterministic {
smol::pin!(future);
let unparker = self.parker.lock().unparker();
- let waker = waker_fn(move || {
- unparker.unpark();
- });
+ let woken = Arc::new(AtomicBool::new(false));
+ let waker = {
+ let woken = woken.clone();
+ waker_fn(move || {
+ woken.store(true, SeqCst);
+ unparker.unpark();
+ })
+ };
let mut cx = Context::from_waker(&waker);
let mut trace = Trace::default();
@@ -166,10 +171,11 @@ impl Deterministic {
&& state.scheduled_from_background.is_empty()
&& state.spawned_from_foreground.is_empty()
{
- if state.forbid_parking {
+ if state.forbid_parking && !woken.load(SeqCst) {
panic!("deterministic executor parked after a call to forbid_parking");
}
drop(state);
+ woken.store(false, SeqCst);
self.parker.lock().park();
}
@@ -2,10 +2,16 @@ use crate::{
fonts::{FontId, Metrics, Properties},
geometry::vector::{vec2f, Vector2F},
platform,
+ text_layout::LineWrapper,
};
use anyhow::{anyhow, Result};
+use ordered_float::OrderedFloat;
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
-use std::{collections::HashMap, sync::Arc};
+use std::{
+ collections::HashMap,
+ ops::{Deref, DerefMut},
+ sync::Arc,
+};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct FamilyId(usize);
@@ -22,6 +28,12 @@ pub struct FontCacheState {
families: Vec<Family>,
font_selections: HashMap<FamilyId, HashMap<Properties, FontId>>,
metrics: HashMap<FontId, Metrics>,
+ wrapper_pool: HashMap<(FontId, OrderedFloat<f32>), Vec<LineWrapper>>,
+}
+
+pub struct LineWrapperHandle {
+ wrapper: Option<LineWrapper>,
+ font_cache: Arc<FontCache>,
}
unsafe impl Send for FontCache {}
@@ -30,9 +42,10 @@ impl FontCache {
pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self {
Self(RwLock::new(FontCacheState {
fonts,
- families: Vec::new(),
- font_selections: HashMap::new(),
- metrics: HashMap::new(),
+ families: Default::default(),
+ font_selections: Default::default(),
+ metrics: Default::default(),
+ wrapper_pool: Default::default(),
}))
}
@@ -134,9 +147,13 @@ impl FontCache {
}
pub fn em_width(&self, font_id: FontId, font_size: f32) -> f32 {
- let state = self.0.read();
- let glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
- let bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap();
+ let glyph_id;
+ let bounds;
+ {
+ let state = self.0.read();
+ 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)
}
@@ -160,6 +177,47 @@ impl FontCache {
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 line_wrapper(self: &Arc<Self>, font_id: FontId, font_size: f32) -> LineWrapperHandle {
+ let mut state = self.0.write();
+ let wrappers = state
+ .wrapper_pool
+ .entry((font_id, OrderedFloat(font_size)))
+ .or_default();
+ let wrapper = wrappers
+ .pop()
+ .unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.fonts.clone()));
+ LineWrapperHandle {
+ wrapper: Some(wrapper),
+ font_cache: self.clone(),
+ }
+ }
+}
+
+impl Drop for LineWrapperHandle {
+ fn drop(&mut self) {
+ let mut state = self.font_cache.0.write();
+ let wrapper = self.wrapper.take().unwrap();
+ state
+ .wrapper_pool
+ .get_mut(&(wrapper.font_id, OrderedFloat(wrapper.font_size)))
+ .unwrap()
+ .push(wrapper);
+ }
+}
+
+impl Deref for LineWrapperHandle {
+ type Target = LineWrapper;
+
+ fn deref(&self) -> &Self::Target {
+ self.wrapper.as_ref().unwrap()
+ }
+}
+
+impl DerefMut for LineWrapperHandle {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.wrapper.as_mut().unwrap()
+ }
}
#[cfg(test)]
@@ -1,23 +1,38 @@
use crate::{
color::Color,
json::{json, ToJson},
+ text_layout::RunStyle,
+ FontCache,
};
+use anyhow::anyhow;
pub use font_kit::{
metrics::Metrics,
properties::{Properties, Stretch, Style, Weight},
};
use serde::{de, Deserialize};
use serde_json::Value;
+use std::{cell::RefCell, sync::Arc};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct FontId(pub usize);
pub type GlyphId = u32;
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
+#[derive(Clone, Debug)]
pub struct TextStyle {
pub color: Color,
+ pub font_family_name: Arc<str>,
+ pub font_id: FontId,
+ pub font_size: f32,
pub font_properties: Properties,
+ pub underline: bool,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct HighlightStyle {
+ pub color: Color,
+ pub font_properties: Properties,
+ pub underline: bool,
}
#[allow(non_camel_case_types)]
@@ -34,76 +49,160 @@ enum WeightJson {
black,
}
+thread_local! {
+ static FONT_CACHE: RefCell<Option<Arc<FontCache>>> = Default::default();
+}
+
#[derive(Deserialize)]
struct TextStyleJson {
color: Color,
+ family: String,
weight: Option<WeightJson>,
+ size: f32,
#[serde(default)]
italic: bool,
+ #[serde(default)]
+ underline: bool,
}
-impl<'de> Deserialize<'de> for TextStyle {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: serde::Deserializer<'de>,
- {
- let json = Value::deserialize(deserializer)?;
- if json.is_object() {
- let style_json: TextStyleJson =
- serde_json::from_value(json).map_err(de::Error::custom)?;
- Ok(style_json.into())
- } else {
- Ok(Self {
- color: serde_json::from_value(json).map_err(de::Error::custom)?,
- font_properties: Properties::new(),
- })
+#[derive(Deserialize)]
+struct HighlightStyleJson {
+ color: Color,
+ weight: Option<WeightJson>,
+ #[serde(default)]
+ italic: bool,
+ #[serde(default)]
+ underline: bool,
+}
+
+impl TextStyle {
+ pub fn new(
+ font_family_name: impl Into<Arc<str>>,
+ font_size: f32,
+ font_properties: Properties,
+ underline: bool,
+ color: Color,
+ 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)?;
+ Ok(Self {
+ color,
+ font_family_name,
+ font_id,
+ font_size,
+ font_properties,
+ underline,
+ })
+ }
+
+ pub fn to_run(&self) -> RunStyle {
+ RunStyle {
+ font_id: self.font_id,
+ color: self.color,
+ underline: self.underline,
+ }
+ }
+
+ fn from_json(json: TextStyleJson) -> anyhow::Result<Self> {
+ FONT_CACHE.with(|font_cache| {
+ if let Some(font_cache) = font_cache.borrow().as_ref() {
+ let font_properties = properties_from_json(json.weight, json.italic);
+ Self::new(
+ json.family,
+ json.size,
+ font_properties,
+ json.underline,
+ json.color,
+ font_cache,
+ )
+ } else {
+ Err(anyhow!(
+ "TextStyle can only be deserialized within a call to with_font_cache"
+ ))
+ }
+ })
+ }
+}
+
+impl HighlightStyle {
+ fn from_json(json: HighlightStyleJson) -> Self {
+ let font_properties = properties_from_json(json.weight, json.italic);
+ Self {
+ color: json.color,
+ font_properties,
+ underline: json.underline,
}
}
}
-impl From<Color> for TextStyle {
+impl From<Color> for HighlightStyle {
fn from(color: Color) -> Self {
Self {
color,
font_properties: Default::default(),
+ underline: false,
}
}
}
+impl<'de> Deserialize<'de> for TextStyle {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ Ok(Self::from_json(TextStyleJson::deserialize(deserializer)?)
+ .map_err(|e| de::Error::custom(e))?)
+ }
+}
+
impl ToJson for TextStyle {
fn to_json(&self) -> Value {
json!({
"color": self.color.to_json(),
+ "font_family": self.font_family_name.as_ref(),
"font_properties": self.font_properties.to_json(),
})
}
}
-impl Into<TextStyle> for TextStyleJson {
- fn into(self) -> TextStyle {
- let weight = match self.weight.unwrap_or(WeightJson::normal) {
- WeightJson::thin => Weight::THIN,
- WeightJson::extra_light => Weight::EXTRA_LIGHT,
- WeightJson::light => Weight::LIGHT,
- WeightJson::normal => Weight::NORMAL,
- WeightJson::medium => Weight::MEDIUM,
- WeightJson::semibold => Weight::SEMIBOLD,
- WeightJson::bold => Weight::BOLD,
- WeightJson::extra_bold => Weight::EXTRA_BOLD,
- WeightJson::black => Weight::BLACK,
- };
- let style = if self.italic {
- Style::Italic
+impl<'de> Deserialize<'de> for HighlightStyle {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let json = serde_json::Value::deserialize(deserializer)?;
+ if json.is_object() {
+ Ok(Self::from_json(
+ serde_json::from_value(json).map_err(de::Error::custom)?,
+ ))
} else {
- Style::Normal
- };
- TextStyle {
- color: self.color,
- font_properties: *Properties::new().weight(weight).style(style),
+ Ok(Self {
+ color: serde_json::from_value(json).map_err(de::Error::custom)?,
+ font_properties: Properties::new(),
+ underline: false,
+ })
}
}
}
+fn properties_from_json(weight: Option<WeightJson>, italic: bool) -> Properties {
+ let weight = match weight.unwrap_or(WeightJson::normal) {
+ WeightJson::thin => Weight::THIN,
+ WeightJson::extra_light => Weight::EXTRA_LIGHT,
+ WeightJson::light => Weight::LIGHT,
+ WeightJson::normal => Weight::NORMAL,
+ WeightJson::medium => Weight::MEDIUM,
+ WeightJson::semibold => Weight::SEMIBOLD,
+ WeightJson::bold => Weight::BOLD,
+ WeightJson::extra_bold => Weight::EXTRA_BOLD,
+ WeightJson::black => Weight::BLACK,
+ };
+ let style = if italic { Style::Italic } else { Style::Normal };
+ *Properties::new().weight(weight).style(style)
+}
+
impl ToJson for Properties {
fn to_json(&self) -> crate::json::Value {
json!({
@@ -155,3 +254,15 @@ impl ToJson for Stretch {
json!(self.0)
}
}
+
+pub fn with_font_cache<F, T>(font_cache: Arc<FontCache>, callback: F) -> T
+where
+ F: FnOnce() -> T,
+{
+ FONT_CACHE.with(|cache| {
+ *cache.borrow_mut() = Some(font_cache);
+ let result = callback();
+ cache.borrow_mut().take();
+ result
+ })
+}
@@ -2,9 +2,12 @@ use anyhow::anyhow;
use std::{
any::Any,
collections::{HashMap, HashSet},
+ fmt::Debug,
};
use tree_sitter::{Language, Node, Parser};
+use crate::{Action, AnyAction};
+
extern "C" {
fn tree_sitter_context_predicate() -> Language;
}
@@ -24,8 +27,7 @@ pub struct Keymap(Vec<Binding>);
pub struct Binding {
keystrokes: Vec<Keystroke>,
- action: String,
- action_arg: Option<Box<dyn ActionArg>>,
+ action: Box<dyn AnyAction>,
context: Option<ContextPredicate>,
}
@@ -70,10 +72,7 @@ where
pub enum MatchResult {
None,
Pending,
- Action {
- name: String,
- arg: Option<Box<dyn Any>>,
- },
+ Action(Box<dyn AnyAction>),
}
impl Matcher {
@@ -117,10 +116,7 @@ impl Matcher {
{
if binding.keystrokes.len() == pending.keystrokes.len() {
self.pending.remove(&view_id);
- return MatchResult::Action {
- name: binding.action.clone(),
- arg: binding.action_arg.as_ref().map(|arg| (*arg).boxed_clone()),
- };
+ return MatchResult::Action(binding.action.boxed_clone());
} else {
retain_pending = true;
pending.context = Some(cx.clone());
@@ -153,19 +149,26 @@ impl Keymap {
}
}
+pub mod menu {
+ use crate::action;
+
+ action!(SelectPrev);
+ action!(SelectNext);
+}
+
impl Default for Keymap {
fn default() -> Self {
Self(vec![
- Binding::new("up", "menu:select_prev", Some("menu")),
- Binding::new("ctrl-p", "menu:select_prev", Some("menu")),
- Binding::new("down", "menu:select_next", Some("menu")),
- Binding::new("ctrl-n", "menu:select_next", Some("menu")),
+ Binding::new("up", menu::SelectPrev, Some("menu")),
+ Binding::new("ctrl-p", menu::SelectPrev, Some("menu")),
+ Binding::new("down", menu::SelectNext, Some("menu")),
+ Binding::new("ctrl-n", menu::SelectNext, Some("menu")),
])
}
}
impl Binding {
- pub fn new<S: Into<String>>(keystrokes: &str, action: S, context: Option<&str>) -> Self {
+ pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
let context = if let Some(context) = context {
Some(ContextPredicate::parse(context).unwrap())
} else {
@@ -177,16 +180,10 @@ impl Binding {
.split_whitespace()
.map(|key| Keystroke::parse(key).unwrap())
.collect(),
- action: action.into(),
- action_arg: None,
+ action: Box::new(action),
context,
}
}
-
- pub fn with_arg<T: 'static + Any + Clone>(mut self, arg: T) -> Self {
- self.action_arg = Some(Box::new(arg));
- self
- }
}
impl Keystroke {
@@ -328,6 +325,8 @@ impl ContextPredicate {
#[cfg(test)]
mod tests {
+ use crate::action;
+
use super::*;
#[test]
@@ -417,15 +416,31 @@ mod tests {
#[test]
fn test_matcher() -> anyhow::Result<()> {
+ action!(A, &'static str);
+ action!(B);
+ action!(Ab);
+
+ impl PartialEq for A {
+ fn eq(&self, other: &Self) -> bool {
+ self.0 == other.0
+ }
+ }
+ impl Eq for A {}
+ impl Debug for A {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "A({:?})", &self.0)
+ }
+ }
+
#[derive(Clone, Debug, Eq, PartialEq)]
struct ActionArg {
a: &'static str,
}
let keymap = Keymap(vec![
- Binding::new("a", "a", Some("a")).with_arg(ActionArg { a: "b" }),
- Binding::new("b", "b", Some("a")),
- Binding::new("a b", "a_b", Some("a || b")),
+ Binding::new("a", A("x"), Some("a")),
+ Binding::new("b", B, Some("a")),
+ Binding::new("a b", Ab, Some("a || b")),
]);
let mut ctx_a = Context::default();
@@ -437,57 +452,40 @@ mod tests {
let mut matcher = Matcher::new(keymap);
// Basic match
- assert_eq!(
- matcher.test_keystroke("a", 1, &ctx_a),
- Some(("a".to_string(), Some(ActionArg { a: "b" })))
- );
+ assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x")));
// Multi-keystroke match
- assert_eq!(matcher.test_keystroke::<()>("a", 1, &ctx_b), None);
- assert_eq!(
- matcher.test_keystroke::<()>("b", 1, &ctx_b),
- Some(("a_b".to_string(), None))
- );
+ assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
+ assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab));
// Failed matches don't interfere with matching subsequent keys
- assert_eq!(matcher.test_keystroke::<()>("x", 1, &ctx_a), None);
- assert_eq!(
- matcher.test_keystroke("a", 1, &ctx_a),
- Some(("a".to_string(), Some(ActionArg { a: "b" })))
- );
+ assert_eq!(matcher.test_keystroke::<A>("x", 1, &ctx_a), None);
+ assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x")));
// Pending keystrokes are cleared when the context changes
- assert_eq!(matcher.test_keystroke::<()>("a", 1, &ctx_b), None);
- assert_eq!(
- matcher.test_keystroke::<()>("b", 1, &ctx_a),
- Some(("b".to_string(), None))
- );
+ assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
+ assert_eq!(matcher.test_keystroke("b", 1, &ctx_a), Some(B));
let mut ctx_c = Context::default();
ctx_c.set.insert("c".into());
// Pending keystrokes are maintained per-view
- assert_eq!(matcher.test_keystroke::<()>("a", 1, &ctx_b), None);
- assert_eq!(matcher.test_keystroke::<()>("a", 2, &ctx_c), None);
- assert_eq!(
- matcher.test_keystroke::<()>("b", 1, &ctx_b),
- Some(("a_b".to_string(), None))
- );
+ assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
+ assert_eq!(matcher.test_keystroke::<A>("a", 2, &ctx_c), None);
+ assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab));
Ok(())
}
impl Matcher {
- fn test_keystroke<A: Any + Clone>(
- &mut self,
- keystroke: &str,
- view_id: usize,
- cx: &Context,
- ) -> Option<(String, Option<A>)> {
- if let MatchResult::Action { name, arg } =
+ fn test_keystroke<A>(&mut self, keystroke: &str, view_id: usize, cx: &Context) -> Option<A>
+ where
+ A: Action + Debug + Eq,
+ {
+ if let MatchResult::Action(action) =
self.push_keystroke(Keystroke::parse(keystroke).unwrap(), view_id, cx)
{
- Some((name, arg.and_then(|arg| arg.downcast_ref::<A>().cloned())))
+ Some(*action.boxed_clone_as_any().downcast().unwrap())
} else {
None
}
@@ -1,11 +1,13 @@
mod app;
pub use app::*;
mod assets;
+pub mod sum_tree;
#[cfg(test)]
mod test;
pub use assets::*;
pub mod elements;
pub mod font_cache;
+pub mod views;
pub use font_cache::FontCache;
mod clipboard;
pub use clipboard::ClipboardItem;
@@ -17,7 +19,7 @@ pub use scene::{Border, Quad, Scene};
pub mod text_layout;
pub use text_layout::TextLayoutCache;
mod util;
-pub use elements::{Element, ElementBox};
+pub use elements::{Element, ElementBox, ElementRc};
pub mod executor;
pub use executor::Task;
pub mod color;
@@ -28,6 +30,5 @@ pub use gpui_macros::test;
pub use platform::FontSystem;
pub use platform::{Event, PathPromptOptions, Platform, PromptLevel};
pub use presenter::{
- AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext,
- SizeConstraint, Vector2FExt,
+ Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
};
@@ -8,16 +8,16 @@ pub mod current {
}
use crate::{
- color::Color,
executor,
fonts::{FontId, GlyphId, Metrics as FontMetrics, Properties as FontProperties},
geometry::{
rect::{RectF, RectI},
- vector::Vector2F,
+ vector::{vec2f, Vector2F},
},
- text_layout::LineLayout,
- ClipboardItem, Menu, Scene,
+ text_layout::{LineLayout, RunStyle},
+ AnyAction, ClipboardItem, Menu, Scene,
};
+use anyhow::Result;
use async_task::Runnable;
pub use event::Event;
use std::{
@@ -26,6 +26,7 @@ use std::{
rc::Rc,
sync::Arc,
};
+use time::UtcOffset;
pub trait Platform: Send + Sync {
fn dispatcher(&self) -> Arc<dyn Dispatcher>;
@@ -45,8 +46,12 @@ pub trait Platform: Send + Sync {
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
fn open_url(&self, url: &str);
- fn write_credentials(&self, url: &str, username: &str, password: &[u8]);
- fn read_credentials(&self, url: &str) -> Option<(String, Vec<u8>)>;
+ fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>;
+ fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>>;
+
+ fn set_cursor_style(&self, style: CursorStyle);
+
+ fn local_timezone(&self) -> UtcOffset;
}
pub(crate) trait ForegroundPlatform {
@@ -56,7 +61,7 @@ pub(crate) trait ForegroundPlatform {
fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>);
fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>);
- fn on_menu_command(&self, callback: Box<dyn FnMut(&str, Option<&dyn Any>)>);
+ fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn AnyAction)>);
fn set_menus(&self, menus: Vec<Menu>);
fn prompt_for_paths(
&self,
@@ -78,7 +83,7 @@ pub trait Dispatcher: Send + Sync {
pub trait Window: WindowContext {
fn as_any_mut(&mut self) -> &mut dyn Any;
fn on_event(&mut self, callback: Box<dyn FnMut(Event)>);
- fn on_resize(&mut self, callback: Box<dyn FnMut(&mut dyn WindowContext)>);
+ fn on_resize(&mut self, callback: Box<dyn FnMut()>);
fn on_close(&mut self, callback: Box<dyn FnOnce()>);
fn prompt(
&self,
@@ -92,12 +97,15 @@ pub trait Window: WindowContext {
pub trait WindowContext {
fn size(&self) -> Vector2F;
fn scale_factor(&self) -> f32;
+ fn titlebar_height(&self) -> f32;
fn present_scene(&mut self, scene: Scene);
}
pub struct WindowOptions<'a> {
pub bounds: RectF,
pub title: Option<&'a str>,
+ pub titlebar_appears_transparent: bool,
+ pub traffic_light_position: Option<Vector2F>,
}
pub struct PathPromptOptions {
@@ -112,7 +120,15 @@ pub enum PromptLevel {
Critical,
}
+#[derive(Copy, Clone, Debug)]
+pub enum CursorStyle {
+ Arrow,
+ ResizeLeftRight,
+ PointingHand,
+}
+
pub trait FontSystem: Send + Sync {
+ fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>>;
fn select_font(
&self,
@@ -130,11 +146,17 @@ pub trait FontSystem: Send + Sync {
subpixel_shift: Vector2F,
scale_factor: f32,
) -> Option<(RectI, Vec<u8>)>;
- fn layout_line(
- &self,
- text: &str,
- font_size: f32,
- runs: &[(usize, FontId, Color)],
- ) -> LineLayout;
+ fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout;
fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec<usize>;
}
+
+impl<'a> Default for WindowOptions<'a> {
+ fn default() -> Self {
+ Self {
+ bounds: RectF::new(Default::default(), vec2f(1024.0, 768.0)),
+ title: Default::default(),
+ titlebar_appears_transparent: Default::default(),
+ traffic_light_position: Default::default(),
+ }
+ }
+}
@@ -24,5 +24,6 @@ pub enum Event {
},
MouseMoved {
position: Vector2F,
+ left_mouse_down: bool,
},
}
@@ -6,8 +6,8 @@ use cocoa::appkit::{
NSUpArrowFunctionKey as ARROW_UP_KEY,
};
use cocoa::{
- appkit::{NSEvent as _, NSEventModifierFlags, NSEventType},
- base::{id, YES},
+ appkit::{NSEvent, NSEventModifierFlags, NSEventType},
+ base::{id, nil, YES},
foundation::NSString as _,
};
use std::{ffi::CStr, os::raw::c_char};
@@ -116,6 +116,7 @@ impl Event {
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
+ left_mouse_down: NSEvent::pressedMouseButtons(nil) & 1 != 0,
}),
_ => None,
}
@@ -1,5 +1,4 @@
use crate::{
- color::Color,
fonts::{FontId, GlyphId, Metrics, Properties},
geometry::{
rect::{RectF, RectI},
@@ -7,7 +6,7 @@ use crate::{
vector::{vec2f, vec2i, Vector2F},
},
platform,
- text_layout::{Glyph, LineLayout, Run},
+ text_layout::{Glyph, LineLayout, Run, RunStyle},
};
use cocoa::appkit::{CGFloat, CGPoint};
use core_foundation::{
@@ -21,9 +20,12 @@ use core_graphics::{
base::CGGlyph, color_space::CGColorSpace, context::CGContext, geometry::CGAffineTransform,
};
use core_text::{line::CTLine, string_attributes::kCTFontAttributeName};
-use font_kit::{canvas::RasterizationOptions, hinting::HintingOptions, source::SystemSource};
+use font_kit::{
+ canvas::RasterizationOptions, handle::Handle, hinting::HintingOptions, source::SystemSource,
+ sources::mem::MemSource,
+};
use parking_lot::RwLock;
-use std::{cell::RefCell, char, cmp, convert::TryFrom, ffi::c_void};
+use std::{cell::RefCell, char, cmp, convert::TryFrom, ffi::c_void, sync::Arc};
#[allow(non_upper_case_globals)]
const kCGImageAlphaOnly: u32 = 7;
@@ -31,20 +33,26 @@ const kCGImageAlphaOnly: u32 = 7;
pub struct FontSystem(RwLock<FontSystemState>);
struct FontSystemState {
- source: SystemSource,
+ memory_source: MemSource,
+ system_source: SystemSource,
fonts: Vec<font_kit::font::Font>,
}
impl FontSystem {
pub fn new() -> Self {
Self(RwLock::new(FontSystemState {
- source: SystemSource::new(),
+ memory_source: MemSource::empty(),
+ system_source: SystemSource::new(),
fonts: Vec::new(),
}))
}
}
impl platform::FontSystem for FontSystem {
+ fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()> {
+ self.0.write().add_fonts(fonts)
+ }
+
fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>> {
self.0.write().load_family(name)
}
@@ -78,12 +86,7 @@ impl platform::FontSystem for FontSystem {
.rasterize_glyph(font_id, font_size, glyph_id, subpixel_shift, scale_factor)
}
- fn layout_line(
- &self,
- text: &str,
- font_size: f32,
- runs: &[(usize, FontId, Color)],
- ) -> LineLayout {
+ fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout {
self.0.read().layout_line(text, font_size, runs)
}
@@ -93,9 +96,23 @@ impl platform::FontSystem for FontSystem {
}
impl FontSystemState {
+ fn add_fonts(&mut self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()> {
+ self.memory_source.add_fonts(
+ fonts
+ .iter()
+ .map(|bytes| Handle::from_memory(bytes.clone(), 0)),
+ )?;
+ Ok(())
+ }
+
fn load_family(&mut self, name: &str) -> anyhow::Result<Vec<FontId>> {
let mut font_ids = Vec::new();
- for font in self.source.select_family_by_name(name)?.fonts() {
+
+ let family = self
+ .memory_source
+ .select_family_by_name(name)
+ .or_else(|_| self.system_source.select_family_by_name(name))?;
+ for font in family.fonts() {
let font = font.load()?;
font_ids.push(FontId(self.fonts.len()));
self.fonts.push(font);
@@ -187,12 +204,7 @@ impl FontSystemState {
}
}
- fn layout_line(
- &self,
- text: &str,
- font_size: f32,
- runs: &[(usize, FontId, Color)],
- ) -> LineLayout {
+ fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout {
let font_id_attr_name = CFString::from_static_string("zed_font_id");
// Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
@@ -204,20 +216,20 @@ impl FontSystemState {
let last_run: RefCell<Option<(usize, FontId)>> = Default::default();
let font_runs = runs
.iter()
- .filter_map(|(len, font_id, _)| {
+ .filter_map(|(len, style)| {
let mut last_run = last_run.borrow_mut();
if let Some((last_len, last_font_id)) = last_run.as_mut() {
- if font_id == last_font_id {
+ if style.font_id == *last_font_id {
*last_len += *len;
None
} else {
let result = (*last_len, *last_font_id);
*last_len = *len;
- *last_font_id = *font_id;
+ *last_font_id = style.font_id;
Some(result)
}
} else {
- *last_run = Some((*len, *font_id));
+ *last_run = Some((*len, style.font_id));
None
}
})
@@ -392,9 +404,8 @@ extern "C" {
#[cfg(test)]
mod tests {
- use crate::MutableAppContext;
-
use super::*;
+ use crate::MutableAppContext;
use font_kit::properties::{Style, Weight};
use platform::FontSystem as _;
@@ -403,13 +414,25 @@ mod tests {
// This is failing intermittently on CI and we don't have time to figure it out
let fonts = FontSystem::new();
let menlo = fonts.load_family("Menlo").unwrap();
- let menlo_regular = fonts.select_font(&menlo, &Properties::new()).unwrap();
- let menlo_italic = fonts
- .select_font(&menlo, &Properties::new().style(Style::Italic))
- .unwrap();
- let menlo_bold = fonts
- .select_font(&menlo, &Properties::new().weight(Weight::BOLD))
- .unwrap();
+ let menlo_regular = RunStyle {
+ font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(),
+ color: Default::default(),
+ underline: false,
+ };
+ let menlo_italic = RunStyle {
+ font_id: fonts
+ .select_font(&menlo, &Properties::new().style(Style::Italic))
+ .unwrap(),
+ color: Default::default(),
+ underline: false,
+ };
+ let menlo_bold = RunStyle {
+ font_id: fonts
+ .select_font(&menlo, &Properties::new().weight(Weight::BOLD))
+ .unwrap(),
+ color: Default::default(),
+ underline: false,
+ };
assert_ne!(menlo_regular, menlo_italic);
assert_ne!(menlo_regular, menlo_bold);
assert_ne!(menlo_italic, menlo_bold);
@@ -417,18 +440,14 @@ mod tests {
let line = fonts.layout_line(
"hello world",
16.0,
- &[
- (2, menlo_bold, Default::default()),
- (4, menlo_italic, Default::default()),
- (5, menlo_regular, Default::default()),
- ],
+ &[(2, menlo_bold), (4, menlo_italic), (5, menlo_regular)],
);
assert_eq!(line.runs.len(), 3);
- assert_eq!(line.runs[0].font_id, menlo_bold);
+ assert_eq!(line.runs[0].font_id, menlo_bold.font_id);
assert_eq!(line.runs[0].glyphs.len(), 2);
- assert_eq!(line.runs[1].font_id, menlo_italic);
+ assert_eq!(line.runs[1].font_id, menlo_italic.font_id);
assert_eq!(line.runs[1].glyphs.len(), 4);
- assert_eq!(line.runs[2].font_id, menlo_regular);
+ assert_eq!(line.runs[2].font_id, menlo_regular.font_id);
assert_eq!(line.runs[2].glyphs.len(), 5);
}
@@ -436,18 +455,26 @@ mod tests {
fn test_glyph_offsets() -> anyhow::Result<()> {
let fonts = FontSystem::new();
let zapfino = fonts.load_family("Zapfino")?;
- let zapfino_regular = fonts.select_font(&zapfino, &Properties::new())?;
+ let zapfino_regular = RunStyle {
+ font_id: fonts.select_font(&zapfino, &Properties::new())?,
+ color: Default::default(),
+ underline: false,
+ };
let menlo = fonts.load_family("Menlo")?;
- let menlo_regular = fonts.select_font(&menlo, &Properties::new())?;
+ let menlo_regular = RunStyle {
+ font_id: fonts.select_font(&menlo, &Properties::new())?,
+ color: Default::default(),
+ underline: false,
+ };
let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
let line = fonts.layout_line(
text,
16.0,
&[
- (9, zapfino_regular, Color::default()),
- (13, menlo_regular, Color::default()),
- (text.len() - 22, zapfino_regular, Color::default()),
+ (9, zapfino_regular),
+ (13, menlo_regular),
+ (text.len() - 22, zapfino_regular),
],
);
assert_eq!(
@@ -513,15 +540,19 @@ mod tests {
fn test_layout_line_bom_char() {
let fonts = FontSystem::new();
let font_ids = fonts.load_family("Helvetica").unwrap();
- let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
+ let style = RunStyle {
+ font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(),
+ color: Default::default(),
+ underline: false,
+ };
let line = "\u{feff}";
- let layout = fonts.layout_line(line, 16., &[(line.len(), font_id, Default::default())]);
+ let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
assert_eq!(layout.len, line.len());
assert!(layout.runs.is_empty());
let line = "a\u{feff}b";
- let layout = fonts.layout_line(line, 16., &[(line.len(), font_id, Default::default())]);
+ let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
assert_eq!(layout.len, line.len());
assert_eq!(layout.runs.len(), 1);
assert_eq!(layout.runs[0].glyphs.len(), 2);
@@ -1,5 +1,11 @@
use super::{BoolExt as _, Dispatcher, FontSystem, Window};
-use crate::{executor, keymap::Keystroke, platform, ClipboardItem, Event, Menu, MenuItem};
+use crate::{
+ executor,
+ keymap::Keystroke,
+ platform::{self, CursorStyle},
+ AnyAction, ClipboardItem, Event, Menu, MenuItem,
+};
+use anyhow::{anyhow, Result};
use block::ConcreteBlock;
use cocoa::{
appkit::{
@@ -27,7 +33,6 @@ use objc::{
};
use ptr::null_mut;
use std::{
- any::Any,
cell::{Cell, RefCell},
convert::TryInto,
ffi::{c_void, CStr},
@@ -38,6 +43,7 @@ use std::{
slice, str,
sync::Arc,
};
+use time::UtcOffset;
const MAC_PLATFORM_IVAR: &'static str = "platform";
static mut APP_CLASS: *const Class = ptr::null();
@@ -90,10 +96,10 @@ pub struct MacForegroundPlatformState {
become_active: Option<Box<dyn FnMut()>>,
resign_active: Option<Box<dyn FnMut()>>,
event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
- menu_command: Option<Box<dyn FnMut(&str, Option<&dyn Any>)>>,
+ menu_command: Option<Box<dyn FnMut(&dyn AnyAction)>>,
open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
finish_launching: Option<Box<dyn FnOnce() -> ()>>,
- menu_actions: Vec<(String, Option<Box<dyn Any>>)>,
+ menu_actions: Vec<Box<dyn AnyAction>>,
}
impl MacForegroundPlatform {
@@ -121,7 +127,6 @@ impl MacForegroundPlatform {
name,
keystroke,
action,
- arg,
} => {
if let Some(keystroke) = keystroke {
let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
@@ -162,7 +167,7 @@ impl MacForegroundPlatform {
let tag = state.menu_actions.len() as NSInteger;
let _: () = msg_send![item, setTag: tag];
- state.menu_actions.push((action.to_string(), arg));
+ state.menu_actions.push(action);
}
}
@@ -215,7 +220,7 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
}
}
- fn on_menu_command(&self, callback: Box<dyn FnMut(&str, Option<&dyn Any>)>) {
+ fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn AnyAction)>) {
self.0.borrow_mut().menu_command = Some(callback);
}
@@ -465,7 +470,7 @@ impl platform::Platform for MacPlatform {
}
}
- fn write_credentials(&self, url: &str, username: &str, password: &[u8]) {
+ fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> {
let url = CFString::from(url);
let username = CFString::from(username);
let password = CFData::from_buffer(password);
@@ -498,12 +503,13 @@ impl platform::Platform for MacPlatform {
}
if status != errSecSuccess {
- panic!("{} password failed: {}", verb, status);
+ return Err(anyhow!("{} password failed: {}", verb, status));
}
}
+ Ok(())
}
- fn read_credentials(&self, url: &str) -> Option<(String, Vec<u8>)> {
+ fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>> {
let url = CFString::from(url);
let cf_true = CFBoolean::true_value().as_CFTypeRef();
@@ -521,27 +527,46 @@ impl platform::Platform for MacPlatform {
let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
match status {
security::errSecSuccess => {}
- security::errSecItemNotFound | security::errSecUserCanceled => return None,
- _ => panic!("reading password failed: {}", status),
+ security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None),
+ _ => return Err(anyhow!("reading password failed: {}", status)),
}
let result = CFType::wrap_under_create_rule(result)
.downcast::<CFDictionary>()
- .expect("keychain item was not a dictionary");
+ .ok_or_else(|| anyhow!("keychain item was not a dictionary"))?;
let username = result
.find(kSecAttrAccount as *const _)
- .expect("account was missing from keychain item");
+ .ok_or_else(|| anyhow!("account was missing from keychain item"))?;
let username = CFType::wrap_under_get_rule(*username)
.downcast::<CFString>()
- .expect("account was not a string");
+ .ok_or_else(|| anyhow!("account was not a string"))?;
let password = result
.find(kSecValueData as *const _)
- .expect("password was missing from keychain item");
+ .ok_or_else(|| anyhow!("password was missing from keychain item"))?;
let password = CFType::wrap_under_get_rule(*password)
.downcast::<CFData>()
- .expect("password was not a string");
+ .ok_or_else(|| anyhow!("password was not a string"))?;
- Some((username.to_string(), password.bytes().to_vec()))
+ Ok(Some((username.to_string(), password.bytes().to_vec())))
+ }
+ }
+
+ fn set_cursor_style(&self, style: CursorStyle) {
+ unsafe {
+ let cursor: id = match style {
+ CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
+ CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
+ CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
+ };
+ let _: () = msg_send![cursor, set];
+ }
+ }
+
+ fn local_timezone(&self) -> UtcOffset {
+ unsafe {
+ let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
+ let seconds_from_gmt: NSInteger = msg_send![local_timezone, secondsFromGMT];
+ UtcOffset::from_whole_seconds(seconds_from_gmt.try_into().unwrap()).unwrap()
}
}
}
@@ -623,8 +648,8 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
if let Some(mut callback) = platform.menu_command.take() {
let tag: NSInteger = msg_send![item, tag];
let index = tag as usize;
- if let Some((action, arg)) = platform.menu_actions.get(index) {
- callback(action, arg.as_ref().map(Box::as_ref));
+ if let Some(action) = platform.menu_actions.get(index) {
+ callback(action.as_ref());
}
platform.menu_command = Some(callback);
}
@@ -6,7 +6,7 @@ use crate::{
vector::{vec2f, vec2i, Vector2F},
},
platform,
- scene::Layer,
+ scene::{Glyph, Icon, Layer, Quad, Shadow},
Scene,
};
use cocoa::foundation::NSUInteger;
@@ -142,7 +142,7 @@ impl Renderer {
let mut sprites = Vec::new();
let mut vertices = Vec::<shaders::GPUIPathVertex>::new();
let mut current_atlas_id = None;
- for (layer_id, layer) in scene.layers().iter().enumerate() {
+ for (layer_id, layer) in scene.layers().enumerate() {
for path in layer.paths() {
let origin = path.bounds.origin() * scene.scale_factor();
let size = (path.bounds.size() * scene.scale_factor()).ceil();
@@ -283,12 +283,24 @@ impl Renderer {
zfar: 1.0,
});
+ let scale_factor = scene.scale_factor();
let mut path_sprites = path_sprites.into_iter().peekable();
-
- for (layer_id, layer) in scene.layers().iter().enumerate() {
+ for (layer_id, layer) in scene.layers().enumerate() {
self.clip(scene, layer, drawable_size, command_encoder);
- self.render_shadows(scene, layer, offset, drawable_size, command_encoder);
- self.render_quads(scene, layer, offset, drawable_size, command_encoder);
+ self.render_shadows(
+ layer.shadows(),
+ scale_factor,
+ offset,
+ drawable_size,
+ command_encoder,
+ );
+ self.render_quads(
+ layer.quads(),
+ scale_factor,
+ offset,
+ drawable_size,
+ command_encoder,
+ );
self.render_path_sprites(
layer_id,
&mut path_sprites,
@@ -296,7 +308,21 @@ impl Renderer {
drawable_size,
command_encoder,
);
- self.render_sprites(scene, layer, offset, drawable_size, command_encoder);
+ self.render_sprites(
+ layer.glyphs(),
+ layer.icons(),
+ scale_factor,
+ offset,
+ drawable_size,
+ command_encoder,
+ );
+ self.render_quads(
+ layer.underlines(),
+ scale_factor,
+ offset,
+ drawable_size,
+ command_encoder,
+ );
}
command_encoder.end_encoding();
@@ -324,18 +350,18 @@ impl Renderer {
fn render_shadows(
&mut self,
- scene: &Scene,
- layer: &Layer,
+ shadows: &[Shadow],
+ scale_factor: f32,
offset: &mut usize,
drawable_size: Vector2F,
command_encoder: &metal::RenderCommandEncoderRef,
) {
- if layer.shadows().is_empty() {
+ if shadows.is_empty() {
return;
}
align_offset(offset);
- let next_offset = *offset + layer.shadows().len() * mem::size_of::<shaders::GPUIShadow>();
+ let next_offset = *offset + shadows.len() * mem::size_of::<shaders::GPUIShadow>();
assert!(
next_offset <= INSTANCE_BUFFER_SIZE,
"instance buffer exhausted"
@@ -365,12 +391,12 @@ impl Renderer {
(self.instances.contents() as *mut u8).offset(*offset as isize)
as *mut shaders::GPUIShadow
};
- for (ix, shadow) in layer.shadows().iter().enumerate() {
- let shape_bounds = shadow.bounds * scene.scale_factor();
+ for (ix, shadow) in shadows.iter().enumerate() {
+ let shape_bounds = shadow.bounds * scale_factor;
let shader_shadow = shaders::GPUIShadow {
origin: shape_bounds.origin().to_float2(),
size: shape_bounds.size().to_float2(),
- corner_radius: shadow.corner_radius * scene.scale_factor(),
+ corner_radius: shadow.corner_radius * scale_factor,
sigma: shadow.sigma,
color: shadow.color.to_uchar4(),
};
@@ -383,24 +409,24 @@ impl Renderer {
metal::MTLPrimitiveType::Triangle,
0,
6,
- layer.shadows().len() as u64,
+ shadows.len() as u64,
);
*offset = next_offset;
}
fn render_quads(
&mut self,
- scene: &Scene,
- layer: &Layer,
+ quads: &[Quad],
+ scale_factor: f32,
offset: &mut usize,
drawable_size: Vector2F,
command_encoder: &metal::RenderCommandEncoderRef,
) {
- if layer.quads().is_empty() {
+ if quads.is_empty() {
return;
}
align_offset(offset);
- let next_offset = *offset + layer.quads().len() * mem::size_of::<shaders::GPUIQuad>();
+ let next_offset = *offset + quads.len() * mem::size_of::<shaders::GPUIQuad>();
assert!(
next_offset <= INSTANCE_BUFFER_SIZE,
"instance buffer exhausted"
@@ -430,9 +456,9 @@ impl Renderer {
(self.instances.contents() as *mut u8).offset(*offset as isize)
as *mut shaders::GPUIQuad
};
- for (ix, quad) in layer.quads().iter().enumerate() {
- let bounds = quad.bounds * scene.scale_factor();
- let border_width = quad.border.width * scene.scale_factor();
+ for (ix, quad) in quads.iter().enumerate() {
+ let bounds = quad.bounds * scale_factor;
+ let border_width = quad.border.width * scale_factor;
let shader_quad = shaders::GPUIQuad {
origin: bounds.origin().round().to_float2(),
size: bounds.size().round().to_float2(),
@@ -445,7 +471,7 @@ impl Renderer {
border_bottom: border_width * (quad.border.bottom as usize as f32),
border_left: border_width * (quad.border.left as usize as f32),
border_color: quad.border.color.to_uchar4(),
- corner_radius: quad.corner_radius * scene.scale_factor(),
+ corner_radius: quad.corner_radius * scale_factor,
};
unsafe {
*(buffer_contents.offset(ix as isize)) = shader_quad;
@@ -456,35 +482,36 @@ impl Renderer {
metal::MTLPrimitiveType::Triangle,
0,
6,
- layer.quads().len() as u64,
+ quads.len() as u64,
);
*offset = next_offset;
}
fn render_sprites(
&mut self,
- scene: &Scene,
- layer: &Layer,
+ glyphs: &[Glyph],
+ icons: &[Icon],
+ scale_factor: f32,
offset: &mut usize,
drawable_size: Vector2F,
command_encoder: &metal::RenderCommandEncoderRef,
) {
- if layer.glyphs().is_empty() && layer.icons().is_empty() {
+ if glyphs.is_empty() && icons.is_empty() {
return;
}
let mut sprites_by_atlas = HashMap::new();
- for glyph in layer.glyphs() {
+ for glyph in glyphs {
if let Some(sprite) = self.sprite_cache.render_glyph(
glyph.font_id,
glyph.font_size,
glyph.id,
glyph.origin,
- scene.scale_factor(),
+ scale_factor,
) {
// Snap sprite to pixel grid.
- let origin = (glyph.origin * scene.scale_factor()).floor() + sprite.offset.to_f32();
+ let origin = (glyph.origin * scale_factor).floor() + sprite.offset.to_f32();
sprites_by_atlas
.entry(sprite.atlas_id)
.or_insert_with(Vec::new)
@@ -499,9 +526,9 @@ impl Renderer {
}
}
- for icon in layer.icons() {
- let origin = icon.bounds.origin() * scene.scale_factor();
- let target_size = icon.bounds.size() * scene.scale_factor();
+ for icon in icons {
+ let origin = icon.bounds.origin() * scale_factor;
+ let target_size = icon.bounds.size() * scale_factor;
let source_size = (target_size * 2.).ceil().to_i32();
let sprite =
@@ -8,13 +8,14 @@ use crate::{
use block::ConcreteBlock;
use cocoa::{
appkit::{
- NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
- NSViewWidthSizable, NSWindow, NSWindowStyleMask,
+ CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
+ NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowStyleMask,
},
base::{id, nil},
foundation::{NSAutoreleasePool, NSInteger, NSSize, NSString},
quartzcore::AutoresizingMask,
};
+use core_graphics::display::CGRect;
use ctor::ctor;
use foreign_types::ForeignType as _;
use objc::{
@@ -65,6 +66,10 @@ unsafe fn build_classes() {
sel!(sendEvent:),
send_event as extern "C" fn(&Object, Sel, id),
);
+ decl.add_method(
+ sel!(windowDidResize:),
+ window_did_resize as extern "C" fn(&Object, Sel, id),
+ );
decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
decl.register()
};
@@ -129,7 +134,7 @@ struct WindowState {
id: usize,
native_window: id,
event_callback: Option<Box<dyn FnMut(Event)>>,
- resize_callback: Option<Box<dyn FnMut(&mut dyn platform::WindowContext)>>,
+ resize_callback: Option<Box<dyn FnMut()>>,
close_callback: Option<Box<dyn FnOnce()>>,
synthetic_drag_counter: usize,
executor: Rc<executor::Foreground>,
@@ -138,6 +143,7 @@ struct WindowState {
command_queue: metal::CommandQueue,
last_fresh_keydown: Option<(Keystroke, String)>,
layer: id,
+ traffic_light_position: Option<Vector2F>,
}
impl Window {
@@ -153,11 +159,15 @@ impl Window {
let pool = NSAutoreleasePool::new(nil);
let frame = options.bounds.to_ns_rect();
- let style_mask = NSWindowStyleMask::NSClosableWindowMask
+ let mut style_mask = NSWindowStyleMask::NSClosableWindowMask
| NSWindowStyleMask::NSMiniaturizableWindowMask
| NSWindowStyleMask::NSResizableWindowMask
| NSWindowStyleMask::NSTitledWindowMask;
+ if options.titlebar_appears_transparent {
+ style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
+ }
+
let native_window: id = msg_send![WINDOW_CLASS, alloc];
let native_window = native_window.initWithContentRect_styleMask_backing_defer_(
frame,
@@ -199,12 +209,14 @@ impl Window {
command_queue: device.new_command_queue(),
last_fresh_keydown: None,
layer,
+ traffic_light_position: options.traffic_light_position,
})));
(*native_window).set_ivar(
WINDOW_STATE_IVAR,
Rc::into_raw(window.0.clone()) as *const c_void,
);
+ native_window.setDelegate_(native_window);
(*native_view).set_ivar(
WINDOW_STATE_IVAR,
Rc::into_raw(window.0.clone()) as *const c_void,
@@ -213,6 +225,9 @@ impl Window {
if let Some(title) = options.title.as_ref() {
native_window.setTitle_(NSString::alloc(nil).init_str(title));
}
+ if options.titlebar_appears_transparent {
+ native_window.setTitlebarAppearsTransparent_(YES);
+ }
native_window.setAcceptsMouseMovedEvents_(YES);
native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
@@ -235,6 +250,7 @@ impl Window {
native_window.center();
native_window.makeKeyAndOrderFront_(nil);
+ window.0.borrow().move_traffic_light();
pool.drain();
window
@@ -272,7 +288,7 @@ impl platform::Window for Window {
self.0.as_ref().borrow_mut().event_callback = Some(callback);
}
- fn on_resize(&mut self, callback: Box<dyn FnMut(&mut dyn platform::WindowContext)>) {
+ fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
self.0.as_ref().borrow_mut().resize_callback = Some(callback);
}
@@ -329,6 +345,56 @@ impl platform::WindowContext for Window {
fn present_scene(&mut self, scene: Scene) {
self.0.as_ref().borrow_mut().present_scene(scene);
}
+
+ fn titlebar_height(&self) -> f32 {
+ self.0.as_ref().borrow().titlebar_height()
+ }
+}
+
+impl WindowState {
+ fn move_traffic_light(&self) {
+ if let Some(traffic_light_position) = self.traffic_light_position {
+ let titlebar_height = self.titlebar_height();
+
+ unsafe {
+ let close_button: id = msg_send![
+ self.native_window,
+ standardWindowButton: NSWindowButton::NSWindowCloseButton
+ ];
+ let min_button: id = msg_send![
+ self.native_window,
+ standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton
+ ];
+ let zoom_button: id = msg_send![
+ self.native_window,
+ standardWindowButton: NSWindowButton::NSWindowZoomButton
+ ];
+
+ let mut close_button_frame: CGRect = msg_send![close_button, frame];
+ let mut min_button_frame: CGRect = msg_send![min_button, frame];
+ let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame];
+ let mut origin = vec2f(
+ traffic_light_position.x(),
+ titlebar_height
+ - traffic_light_position.y()
+ - close_button_frame.size.height as f32,
+ );
+ let button_spacing =
+ (min_button_frame.origin.x - close_button_frame.origin.x) as f32;
+
+ close_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
+ let _: () = msg_send![close_button, setFrame: close_button_frame];
+ origin.set_x(origin.x() + button_spacing);
+
+ min_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
+ let _: () = msg_send![min_button, setFrame: min_button_frame];
+ origin.set_x(origin.x() + button_spacing);
+
+ zoom_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
+ let _: () = msg_send![zoom_button, setFrame: zoom_button_frame];
+ }
+ }
+ }
}
impl platform::WindowContext for WindowState {
@@ -345,6 +411,14 @@ impl platform::WindowContext for WindowState {
}
}
+ fn titlebar_height(&self) -> f32 {
+ unsafe {
+ let frame = NSWindow::frame(self.native_window);
+ let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect];
+ (frame.size.height - content_layout_rect.size.height) as f32
+ }
+ }
+
fn present_scene(&mut self, scene: Scene) {
self.scene_to_render = Some(scene);
unsafe {
@@ -442,6 +516,11 @@ extern "C" fn send_event(this: &Object, _: Sel, native_event: id) {
}
}
+extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
+ let window_state = unsafe { get_window_state(this) };
+ window_state.as_ref().borrow().move_traffic_light();
+}
+
extern "C" fn close_window(this: &Object, _: Sel) {
unsafe {
let close_callback = {
@@ -469,24 +548,24 @@ extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
let window_state = unsafe { get_window_state(this) };
- let mut window_state = window_state.as_ref().borrow_mut();
+ let mut window_state_borrow = window_state.as_ref().borrow_mut();
unsafe {
- let _: () =
- msg_send![window_state.layer, setContentsScale: window_state.scale_factor() as f64];
+ let _: () = msg_send![window_state_borrow.layer, setContentsScale: window_state_borrow.scale_factor() as f64];
}
- if let Some(mut callback) = window_state.resize_callback.take() {
- callback(&mut *window_state);
- window_state.resize_callback = Some(callback);
+ if let Some(mut callback) = window_state_borrow.resize_callback.take() {
+ drop(window_state_borrow);
+ callback();
+ window_state.as_ref().borrow_mut().resize_callback = Some(callback);
};
}
extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
let window_state = unsafe { get_window_state(this) };
- let mut window_state = window_state.as_ref().borrow_mut();
+ let mut window_state_borrow = window_state.as_ref().borrow_mut();
- if window_state.size() == vec2f(size.width as f32, size.height as f32) {
+ if window_state_borrow.size() == vec2f(size.width as f32, size.height as f32) {
return;
}
@@ -494,19 +573,20 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size];
}
- let scale_factor = window_state.scale_factor() as f64;
+ let scale_factor = window_state_borrow.scale_factor() as f64;
let drawable_size: NSSize = NSSize {
width: size.width * scale_factor,
height: size.height * scale_factor,
};
unsafe {
- let _: () = msg_send![window_state.layer, setDrawableSize: drawable_size];
+ let _: () = msg_send![window_state_borrow.layer, setDrawableSize: drawable_size];
}
- if let Some(mut callback) = window_state.resize_callback.take() {
- callback(&mut *window_state);
- window_state.resize_callback = Some(callback);
+ if let Some(mut callback) = window_state_borrow.resize_callback.take() {
+ drop(window_state_borrow);
+ callback();
+ window_state.borrow_mut().resize_callback = Some(callback);
};
}
@@ -1,4 +1,6 @@
-use crate::ClipboardItem;
+use super::CursorStyle;
+use crate::{AnyAction, ClipboardItem};
+use anyhow::Result;
use parking_lot::Mutex;
use pathfinder_geometry::vector::Vector2F;
use std::{
@@ -8,11 +10,13 @@ use std::{
rc::Rc,
sync::Arc,
};
+use time::UtcOffset;
pub struct Platform {
dispatcher: Arc<dyn super::Dispatcher>,
fonts: Arc<dyn super::FontSystem>,
current_clipboard_item: Mutex<Option<ClipboardItem>>,
+ cursor: Mutex<CursorStyle>,
}
#[derive(Default)]
@@ -27,7 +31,7 @@ pub struct Window {
scale_factor: f32,
current_scene: Option<crate::Scene>,
event_handlers: Vec<Box<dyn FnMut(super::Event)>>,
- resize_handlers: Vec<Box<dyn FnMut(&mut dyn super::WindowContext)>>,
+ resize_handlers: Vec<Box<dyn FnMut()>>,
close_handlers: Vec<Box<dyn FnOnce()>>,
pub(crate) last_prompt: RefCell<Option<Box<dyn FnOnce(usize)>>>,
}
@@ -62,7 +66,7 @@ impl super::ForegroundPlatform for ForegroundPlatform {
unimplemented!()
}
- fn on_menu_command(&self, _: Box<dyn FnMut(&str, Option<&dyn Any>)>) {}
+ fn on_menu_command(&self, _: Box<dyn FnMut(&dyn AnyAction)>) {}
fn set_menus(&self, _: Vec<crate::Menu>) {}
@@ -84,6 +88,7 @@ impl Platform {
dispatcher: Arc::new(Dispatcher),
fonts: Arc::new(super::current::FontSystem::new()),
current_clipboard_item: Default::default(),
+ cursor: Mutex::new(CursorStyle::Arrow),
}
}
}
@@ -124,10 +129,20 @@ impl super::Platform for Platform {
fn open_url(&self, _: &str) {}
- fn write_credentials(&self, _: &str, _: &str, _: &[u8]) {}
+ fn write_credentials(&self, _: &str, _: &str, _: &[u8]) -> Result<()> {
+ Ok(())
+ }
- fn read_credentials(&self, _: &str) -> Option<(String, Vec<u8>)> {
- None
+ fn read_credentials(&self, _: &str) -> Result<Option<(String, Vec<u8>)>> {
+ Ok(None)
+ }
+
+ fn set_cursor_style(&self, style: CursorStyle) {
+ *self.cursor.lock() = style;
+ }
+
+ fn local_timezone(&self) -> UtcOffset {
+ UtcOffset::UTC
}
}
@@ -164,6 +179,10 @@ impl super::WindowContext for Window {
self.scale_factor
}
+ fn titlebar_height(&self) -> f32 {
+ 24.
+ }
+
fn present_scene(&mut self, scene: crate::Scene) {
self.current_scene = Some(scene);
}
@@ -178,7 +197,7 @@ impl super::Window for Window {
self.event_handlers.push(callback);
}
- fn on_resize(&mut self, callback: Box<dyn FnMut(&mut dyn super::WindowContext)>) {
+ fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
self.resize_handlers.push(callback);
}
@@ -2,16 +2,18 @@ use crate::{
app::{AppContext, MutableAppContext, WindowInvalidation},
elements::Element,
font_cache::FontCache,
+ geometry::rect::RectF,
json::{self, ToJson},
platform::Event,
text_layout::TextLayoutCache,
- AssetCache, ElementBox, Scene,
+ Action, AnyAction, AssetCache, ElementBox, Entity, FontSystem, ModelHandle, ReadModel,
+ ReadView, Scene, View, ViewHandle,
};
use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json;
use std::{
- any::Any,
collections::{HashMap, HashSet},
+ ops::{Deref, DerefMut},
sync::Arc,
};
@@ -23,24 +25,27 @@ pub struct Presenter {
text_layout_cache: TextLayoutCache,
asset_cache: Arc<AssetCache>,
last_mouse_moved_event: Option<Event>,
+ titlebar_height: f32,
}
impl Presenter {
pub fn new(
window_id: usize,
+ titlebar_height: f32,
font_cache: Arc<FontCache>,
text_layout_cache: TextLayoutCache,
asset_cache: Arc<AssetCache>,
- cx: &MutableAppContext,
+ cx: &mut MutableAppContext,
) -> Self {
Self {
window_id,
- rendered_views: cx.render_views(window_id),
+ rendered_views: cx.render_views(window_id, titlebar_height),
parents: HashMap::new(),
font_cache,
text_layout_cache,
asset_cache,
last_mouse_moved_event: None,
+ titlebar_height,
}
}
@@ -55,15 +60,37 @@ impl Presenter {
path
}
- pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, cx: &AppContext) {
+ pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, cx: &mut MutableAppContext) {
for view_id in invalidation.removed {
invalidation.updated.remove(&view_id);
self.rendered_views.remove(&view_id);
self.parents.remove(&view_id);
}
for view_id in invalidation.updated {
- self.rendered_views
- .insert(view_id, cx.render_view(self.window_id, view_id).unwrap());
+ self.rendered_views.insert(
+ view_id,
+ cx.render_view(self.window_id, view_id, self.titlebar_height, false)
+ .unwrap(),
+ );
+ }
+ }
+
+ pub fn refresh(
+ &mut self,
+ invalidation: Option<WindowInvalidation>,
+ cx: &mut MutableAppContext,
+ ) {
+ if let Some(invalidation) = invalidation {
+ for view_id in invalidation.removed {
+ self.rendered_views.remove(&view_id);
+ self.parents.remove(&view_id);
+ }
+ }
+
+ for (view_id, view) in &mut self.rendered_views {
+ *view = cx
+ .render_view(self.window_id, *view_id, self.titlebar_height, true)
+ .unwrap();
}
}
@@ -71,13 +98,13 @@ impl Presenter {
&mut self,
window_size: Vector2F,
scale_factor: f32,
+ refreshing: bool,
cx: &mut MutableAppContext,
) -> Scene {
let mut scene = Scene::new(scale_factor);
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
- self.layout(window_size, cx);
- self.after_layout(cx);
+ self.layout(window_size, refreshing, cx);
let mut paint_cx = PaintContext {
scene: &mut scene,
font_cache: &self.font_cache,
@@ -85,7 +112,11 @@ impl Presenter {
rendered_views: &mut self.rendered_views,
app: cx.as_ref(),
};
- paint_cx.paint(root_view_id, Vector2F::zero());
+ paint_cx.paint(
+ root_view_id,
+ Vector2F::zero(),
+ RectF::new(Vector2F::zero(), window_size),
+ );
self.text_layout_cache.finish_frame();
if let Some(event) = self.last_mouse_moved_event.clone() {
@@ -98,67 +129,76 @@ impl Presenter {
scene
}
- fn layout(&mut self, size: Vector2F, cx: &mut MutableAppContext) {
+ fn layout(&mut self, size: Vector2F, refreshing: bool, cx: &mut MutableAppContext) {
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
- let mut layout_ctx = LayoutContext {
- rendered_views: &mut self.rendered_views,
- parents: &mut self.parents,
- font_cache: &self.font_cache,
- text_layout_cache: &self.text_layout_cache,
- asset_cache: &self.asset_cache,
- view_stack: Vec::new(),
- app: cx,
- };
- layout_ctx.layout(root_view_id, SizeConstraint::strict(size));
+ self.build_layout_context(refreshing, cx)
+ .layout(root_view_id, SizeConstraint::strict(size));
}
}
- fn after_layout(&mut self, cx: &mut MutableAppContext) {
- if let Some(root_view_id) = cx.root_view_id(self.window_id) {
- let mut layout_cx = AfterLayoutContext {
- rendered_views: &mut self.rendered_views,
- font_cache: &self.font_cache,
- text_layout_cache: &self.text_layout_cache,
- app: cx,
- };
- layout_cx.after_layout(root_view_id);
+ pub fn build_layout_context<'a>(
+ &'a mut self,
+ refreshing: bool,
+ cx: &'a mut MutableAppContext,
+ ) -> LayoutContext<'a> {
+ LayoutContext {
+ rendered_views: &mut self.rendered_views,
+ parents: &mut self.parents,
+ refreshing,
+ font_cache: &self.font_cache,
+ font_system: cx.platform().fonts(),
+ text_layout_cache: &self.text_layout_cache,
+ asset_cache: &self.asset_cache,
+ view_stack: Vec::new(),
+ app: cx,
}
}
pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) {
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
- if matches!(event, Event::MouseMoved { .. }) {
- self.last_mouse_moved_event = Some(event.clone());
+ match event {
+ Event::MouseMoved { .. } => {
+ self.last_mouse_moved_event = Some(event.clone());
+ }
+ Event::LeftMouseDragged { position } => {
+ self.last_mouse_moved_event = Some(Event::MouseMoved {
+ position,
+ left_mouse_down: true,
+ });
+ }
+ _ => {}
}
- let mut event_cx = EventContext {
- rendered_views: &mut self.rendered_views,
- actions: Default::default(),
- font_cache: &self.font_cache,
- text_layout_cache: &self.text_layout_cache,
- view_stack: Default::default(),
- invalidated_views: Default::default(),
- app: cx,
- };
+ let mut event_cx = self.build_event_context(cx);
event_cx.dispatch_event(root_view_id, &event);
let invalidated_views = event_cx.invalidated_views;
- let actions = event_cx.actions;
+ let dispatch_directives = event_cx.dispatched_actions;
for view_id in invalidated_views {
cx.notify_view(self.window_id, view_id);
}
- for action in actions {
- cx.dispatch_action_any(
- self.window_id,
- &action.path,
- action.name,
- action.arg.as_ref(),
- );
+ for directive in dispatch_directives {
+ cx.dispatch_action_any(self.window_id, &directive.path, directive.action.as_ref());
}
}
}
+ pub fn build_event_context<'a>(
+ &'a mut self,
+ cx: &'a mut MutableAppContext,
+ ) -> EventContext<'a> {
+ EventContext {
+ rendered_views: &mut self.rendered_views,
+ dispatched_actions: Default::default(),
+ font_cache: &self.font_cache,
+ text_layout_cache: &self.text_layout_cache,
+ view_stack: Default::default(),
+ invalidated_views: Default::default(),
+ app: cx,
+ }
+ }
+
pub fn debug_elements(&self, cx: &AppContext) -> Option<json::Value> {
cx.root_view_id(self.window_id)
.and_then(|root_view_id| self.rendered_views.get(&root_view_id))
@@ -172,20 +212,21 @@ impl Presenter {
}
}
-pub struct ActionToDispatch {
+pub struct DispatchDirective {
pub path: Vec<usize>,
- pub name: &'static str,
- pub arg: Box<dyn Any>,
+ pub action: Box<dyn AnyAction>,
}
pub struct LayoutContext<'a> {
rendered_views: &'a mut HashMap<usize, ElementBox>,
parents: &'a mut HashMap<usize, usize>,
- pub font_cache: &'a FontCache,
+ view_stack: Vec<usize>,
+ pub refreshing: bool,
+ pub font_cache: &'a Arc<FontCache>,
+ pub font_system: Arc<dyn FontSystem>,
pub text_layout_cache: &'a TextLayoutCache,
pub asset_cache: &'a AssetCache,
pub app: &'a mut MutableAppContext,
- view_stack: Vec<usize>,
}
impl<'a> LayoutContext<'a> {
@@ -202,19 +243,29 @@ impl<'a> LayoutContext<'a> {
}
}
-pub struct AfterLayoutContext<'a> {
- rendered_views: &'a mut HashMap<usize, ElementBox>,
- pub font_cache: &'a FontCache,
- pub text_layout_cache: &'a TextLayoutCache,
- pub app: &'a mut MutableAppContext,
+impl<'a> Deref for LayoutContext<'a> {
+ type Target = MutableAppContext;
+
+ fn deref(&self) -> &Self::Target {
+ self.app
+ }
}
-impl<'a> AfterLayoutContext<'a> {
- fn after_layout(&mut self, view_id: usize) {
- if let Some(mut view) = self.rendered_views.remove(&view_id) {
- view.after_layout(self);
- self.rendered_views.insert(view_id, view);
- }
+impl<'a> DerefMut for LayoutContext<'a> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.app
+ }
+}
+
+impl<'a> ReadView for LayoutContext<'a> {
+ fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
+ self.app.read_view(handle)
+ }
+}
+
+impl<'a> ReadModel for LayoutContext<'a> {
+ fn read_model<T: Entity>(&self, handle: &ModelHandle<T>) -> &T {
+ self.app.read_model(handle)
}
}
@@ -227,9 +278,9 @@ pub struct PaintContext<'a> {
}
impl<'a> PaintContext<'a> {
- fn paint(&mut self, view_id: usize, origin: Vector2F) {
+ fn paint(&mut self, view_id: usize, origin: Vector2F, visible_bounds: RectF) {
if let Some(mut tree) = self.rendered_views.remove(&view_id) {
- tree.paint(origin, self);
+ tree.paint(origin, visible_bounds, self);
self.rendered_views.insert(view_id, tree);
}
}
@@ -237,7 +288,7 @@ impl<'a> PaintContext<'a> {
pub struct EventContext<'a> {
rendered_views: &'a mut HashMap<usize, ElementBox>,
- actions: Vec<ActionToDispatch>,
+ dispatched_actions: Vec<DispatchDirective>,
pub font_cache: &'a FontCache,
pub text_layout_cache: &'a TextLayoutCache,
pub app: &'a mut MutableAppContext,
@@ -258,17 +309,31 @@ impl<'a> EventContext<'a> {
}
}
- pub fn dispatch_action<A: 'static + Any>(&mut self, name: &'static str, arg: A) {
- self.actions.push(ActionToDispatch {
+ pub fn dispatch_action<A: Action>(&mut self, action: A) {
+ self.dispatched_actions.push(DispatchDirective {
path: self.view_stack.clone(),
- name,
- arg: Box::new(arg),
+ action: Box::new(action),
});
}
pub fn notify(&mut self) {
- self.invalidated_views
- .insert(*self.view_stack.last().unwrap());
+ if let Some(view_id) = self.view_stack.last() {
+ self.invalidated_views.insert(*view_id);
+ }
+ }
+}
+
+impl<'a> Deref for EventContext<'a> {
+ type Target = MutableAppContext;
+
+ fn deref(&self) -> &Self::Target {
+ self.app
+ }
+}
+
+impl<'a> DerefMut for EventContext<'a> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.app
}
}
@@ -352,6 +417,13 @@ impl SizeConstraint {
Axis::Vertical => self.max.y(),
}
}
+
+ pub fn min_along(&self, axis: Axis) -> f32 {
+ match axis {
+ Axis::Horizontal => self.min.x(),
+ Axis::Vertical => self.min.y(),
+ }
+ }
}
impl ToJson for SizeConstraint {
@@ -386,28 +458,20 @@ impl Element for ChildView {
(size, ())
}
- fn after_layout(
- &mut self,
- _: Vector2F,
- _: &mut Self::LayoutState,
- cx: &mut AfterLayoutContext,
- ) {
- cx.after_layout(self.view_id);
- }
-
fn paint(
&mut self,
- bounds: pathfinder_geometry::rect::RectF,
+ bounds: RectF,
+ visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
- cx.paint(self.view_id, bounds.origin());
+ cx.paint(self.view_id, bounds.origin(), visible_bounds);
}
fn dispatch_event(
&mut self,
event: &Event,
- _: pathfinder_geometry::rect::RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
@@ -417,7 +481,7 @@ impl Element for ChildView {
fn debug(
&self,
- bounds: pathfinder_geometry::rect::RectF,
+ bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
@@ -11,6 +11,11 @@ use crate::{
pub struct Scene {
scale_factor: f32,
+ stacking_contexts: Vec<StackingContext>,
+ active_stacking_context_stack: Vec<usize>,
+}
+
+struct StackingContext {
layers: Vec<Layer>,
active_layer_stack: Vec<usize>,
}
@@ -19,6 +24,7 @@ pub struct Scene {
pub struct Layer {
clip_bounds: Option<RectF>,
quads: Vec<Quad>,
+ underlines: Vec<Quad>,
shadows: Vec<Shadow>,
glyphs: Vec<Glyph>,
icons: Vec<Icon>,
@@ -120,10 +126,11 @@ pub struct PathVertex {
impl Scene {
pub fn new(scale_factor: f32) -> Self {
+ let stacking_context = StackingContext::new(None);
Scene {
scale_factor,
- layers: vec![Layer::new(None)],
- active_layer_stack: vec![0],
+ stacking_contexts: vec![stacking_context],
+ active_stacking_context_stack: vec![0],
}
}
@@ -131,25 +138,38 @@ impl Scene {
self.scale_factor
}
- pub fn layers(&self) -> &[Layer] {
- self.layers.as_slice()
+ pub fn layers(&self) -> impl Iterator<Item = &Layer> {
+ self.stacking_contexts.iter().flat_map(|s| &s.layers)
+ }
+
+ pub fn push_stacking_context(&mut self, clip_bounds: Option<RectF>) {
+ self.active_stacking_context_stack
+ .push(self.stacking_contexts.len());
+ self.stacking_contexts
+ .push(StackingContext::new(clip_bounds))
+ }
+
+ pub fn pop_stacking_context(&mut self) {
+ self.active_stacking_context_stack.pop();
+ assert!(!self.active_stacking_context_stack.is_empty());
}
pub fn push_layer(&mut self, clip_bounds: Option<RectF>) {
- let ix = self.layers.len();
- self.layers.push(Layer::new(clip_bounds));
- self.active_layer_stack.push(ix);
+ self.active_stacking_context().push_layer(clip_bounds);
}
pub fn pop_layer(&mut self) {
- assert!(self.active_layer_stack.len() > 1);
- self.active_layer_stack.pop();
+ self.active_stacking_context().pop_layer();
}
pub fn push_quad(&mut self, quad: Quad) {
self.active_layer().push_quad(quad)
}
+ pub fn push_underline(&mut self, underline: Quad) {
+ self.active_layer().push_underline(underline)
+ }
+
pub fn push_shadow(&mut self, shadow: Shadow) {
self.active_layer().push_shadow(shadow)
}
@@ -166,9 +186,52 @@ impl Scene {
self.active_layer().push_path(path);
}
+ fn active_stacking_context(&mut self) -> &mut StackingContext {
+ let ix = *self.active_stacking_context_stack.last().unwrap();
+ &mut self.stacking_contexts[ix]
+ }
+
+ fn active_layer(&mut self) -> &mut Layer {
+ self.active_stacking_context().active_layer()
+ }
+}
+
+impl StackingContext {
+ fn new(clip_bounds: Option<RectF>) -> Self {
+ Self {
+ layers: vec![Layer::new(clip_bounds)],
+ active_layer_stack: vec![0],
+ }
+ }
+
fn active_layer(&mut self) -> &mut Layer {
&mut self.layers[*self.active_layer_stack.last().unwrap()]
}
+
+ fn push_layer(&mut self, clip_bounds: Option<RectF>) {
+ let parent_clip_bounds = self.active_layer().clip_bounds();
+ let clip_bounds = clip_bounds
+ .map(|clip_bounds| {
+ clip_bounds
+ .intersection(parent_clip_bounds.unwrap_or(clip_bounds))
+ .unwrap_or_else(|| {
+ if !clip_bounds.is_empty() {
+ log::warn!("specified clip bounds are disjoint from parent layer");
+ }
+ RectF::default()
+ })
+ })
+ .or(parent_clip_bounds);
+
+ let ix = self.layers.len();
+ self.layers.push(Layer::new(clip_bounds));
+ self.active_layer_stack.push(ix);
+ }
+
+ fn pop_layer(&mut self) {
+ self.active_layer_stack.pop().unwrap();
+ assert!(!self.active_layer_stack.is_empty());
+ }
}
impl Layer {
@@ -176,6 +239,7 @@ impl Layer {
Self {
clip_bounds,
quads: Vec::new(),
+ underlines: Vec::new(),
shadows: Vec::new(),
glyphs: Vec::new(),
icons: Vec::new(),
@@ -195,6 +259,14 @@ impl Layer {
self.quads.as_slice()
}
+ fn push_underline(&mut self, underline: Quad) {
+ self.underlines.push(underline);
+ }
+
+ pub fn underlines(&self) -> &[Quad] {
+ self.underlines.as_slice()
+ }
+
fn push_shadow(&mut self, shadow: Shadow) {
self.shadows.push(shadow);
}
@@ -1,6 +1,5 @@
mod cursor;
-use crate::util::Bias;
use arrayvec::ArrayVec;
pub use cursor::Cursor;
pub use cursor::FilterCursor;
@@ -11,7 +10,7 @@ const TREE_BASE: usize = 2;
#[cfg(not(test))]
const TREE_BASE: usize = 6;
-pub trait Item: Clone + fmt::Debug {
+pub trait Item: Clone {
type Summary: Summary;
fn summary(&self) -> Self::Summary;
@@ -47,6 +46,29 @@ impl<'a, S: Summary, T: Dimension<'a, S> + Ord> SeekDimension<'a, S> for T {
}
}
+#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
+pub enum Bias {
+ Left,
+ Right,
+}
+
+impl PartialOrd for Bias {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for Bias {
+ fn cmp(&self, other: &Self) -> Ordering {
+ match (self, other) {
+ (Self::Left, Self::Left) => Ordering::Equal,
+ (Self::Left, Self::Right) => Ordering::Less,
+ (Self::Right, Self::Right) => Ordering::Equal,
+ (Self::Right, Self::Left) => Ordering::Greater,
+ }
+ }
+}
+
#[derive(Debug, Clone)]
pub struct SumTree<T: Item>(Arc<Node<T>>);
@@ -65,6 +87,15 @@ impl<T: Item> SumTree<T> {
tree
}
+ pub fn from_iter<I: IntoIterator<Item = T>>(
+ iter: I,
+ cx: &<T::Summary as Summary>::Context,
+ ) -> Self {
+ let mut tree = Self::new();
+ tree.extend(iter, cx);
+ tree
+ }
+
#[allow(unused)]
pub fn items(&self, cx: &<T::Summary as Summary>::Context) -> Vec<T> {
let mut items = Vec::new();
@@ -253,8 +284,8 @@ impl<T: Item> SumTree<T> {
summary.add_summary(other_node.summary(), cx);
let height_delta = *height - other_node.height();
- let mut summaries_to_append = ArrayVec::<[T::Summary; 2 * TREE_BASE]>::new();
- let mut trees_to_append = ArrayVec::<[SumTree<T>; 2 * TREE_BASE]>::new();
+ let mut summaries_to_append = ArrayVec::<T::Summary, { 2 * TREE_BASE }>::new();
+ let mut trees_to_append = ArrayVec::<SumTree<T>, { 2 * TREE_BASE }>::new();
if height_delta == 0 {
summaries_to_append.extend(other_node.child_summaries().iter().cloned());
trees_to_append.extend(other_node.child_trees().iter().cloned());
@@ -277,8 +308,8 @@ impl<T: Item> SumTree<T> {
let child_count = child_trees.len() + trees_to_append.len();
if child_count > 2 * TREE_BASE {
- let left_summaries: ArrayVec<_>;
- let right_summaries: ArrayVec<_>;
+ let left_summaries: ArrayVec<_, { 2 * TREE_BASE }>;
+ let right_summaries: ArrayVec<_, { 2 * TREE_BASE }>;
let left_trees;
let right_trees;
@@ -323,7 +354,7 @@ impl<T: Item> SumTree<T> {
let left_items;
let right_items;
let left_summaries;
- let right_summaries: ArrayVec<[T::Summary; 2 * TREE_BASE]>;
+ let right_summaries: ArrayVec<T::Summary, { 2 * TREE_BASE }>;
let midpoint = (child_count + child_count % 2) / 2;
{
@@ -491,13 +522,13 @@ pub enum Node<T: Item> {
Internal {
height: u8,
summary: T::Summary,
- child_summaries: ArrayVec<[T::Summary; 2 * TREE_BASE]>,
- child_trees: ArrayVec<[SumTree<T>; 2 * TREE_BASE]>,
+ child_summaries: ArrayVec<T::Summary, { 2 * TREE_BASE }>,
+ child_trees: ArrayVec<SumTree<T>, { 2 * TREE_BASE }>,
},
Leaf {
summary: T::Summary,
- items: ArrayVec<[T; 2 * TREE_BASE]>,
- item_summaries: ArrayVec<[T::Summary; 2 * TREE_BASE]>,
+ items: ArrayVec<T, { 2 * TREE_BASE }>,
+ item_summaries: ArrayVec<T::Summary, { 2 * TREE_BASE }>,
},
}
@@ -532,14 +563,14 @@ impl<T: Item> Node<T> {
}
}
- fn child_trees(&self) -> &ArrayVec<[SumTree<T>; 2 * TREE_BASE]> {
+ fn child_trees(&self) -> &ArrayVec<SumTree<T>, { 2 * TREE_BASE }> {
match self {
Node::Internal { child_trees, .. } => child_trees,
Node::Leaf { .. } => panic!("Leaf nodes have no child trees"),
}
}
- fn items(&self) -> &ArrayVec<[T; 2 * TREE_BASE]> {
+ fn items(&self) -> &ArrayVec<T, { 2 * TREE_BASE }> {
match self {
Node::Leaf { items, .. } => items,
Node::Internal { .. } => panic!("Internal nodes have no items"),
@@ -603,7 +634,7 @@ mod tests {
);
}
- #[gpui::test(iterations = 100)]
+ #[crate::test(self, iterations = 100)]
fn test_random(mut rng: StdRng) {
let rng = &mut rng;
let mut tree = SumTree::<u8>::new();
@@ -10,10 +10,26 @@ struct StackEntry<'a, T: Item, S, U> {
sum_dimension: U,
}
+impl<'a, T, S, U> StackEntry<'a, T, S, U>
+where
+ T: Item,
+ S: SeekDimension<'a, T::Summary>,
+ U: SeekDimension<'a, T::Summary>,
+{
+ fn swap_dimensions(self) -> StackEntry<'a, T, U, S> {
+ StackEntry {
+ tree: self.tree,
+ index: self.index,
+ seek_dimension: self.sum_dimension,
+ sum_dimension: self.seek_dimension,
+ }
+ }
+}
+
#[derive(Clone)]
pub struct Cursor<'a, T: Item, S, U> {
tree: &'a SumTree<T>,
- stack: ArrayVec<[StackEntry<'a, T, S, U>; 16]>,
+ stack: ArrayVec<StackEntry<'a, T, S, U>, 16>,
seek_dimension: S,
sum_dimension: U,
did_seek: bool,
@@ -147,7 +163,6 @@ where
None
}
- #[allow(unused)]
pub fn prev(&mut self, cx: &<T::Summary as Summary>::Context) {
assert!(self.did_seek, "Must seek before calling this method");
@@ -495,8 +510,8 @@ where
ref item_summaries,
..
} => {
- let mut slice_items = ArrayVec::<[T; 2 * TREE_BASE]>::new();
- let mut slice_item_summaries = ArrayVec::<[T::Summary; 2 * TREE_BASE]>::new();
+ let mut slice_items = ArrayVec::<T, { 2 * TREE_BASE }>::new();
+ let mut slice_item_summaries = ArrayVec::<T::Summary, { 2 * TREE_BASE }>::new();
let mut slice_items_summary = match aggregate {
SeekAggregate::Slice(_) => Some(T::Summary::default()),
_ => None,
@@ -602,6 +617,28 @@ where
}
}
+impl<'a, T, S, U> Cursor<'a, T, S, U>
+where
+ T: Item,
+ S: SeekDimension<'a, T::Summary>,
+ U: SeekDimension<'a, T::Summary>,
+{
+ pub fn swap_dimensions(self) -> Cursor<'a, T, U, S> {
+ Cursor {
+ tree: self.tree,
+ stack: self
+ .stack
+ .into_iter()
+ .map(StackEntry::swap_dimensions)
+ .collect(),
+ seek_dimension: self.sum_dimension,
+ sum_dimension: self.seek_dimension,
+ did_seek: self.did_seek,
+ at_end: self.at_end,
+ }
+ }
+}
+
pub struct FilterCursor<'a, F: Fn(&T::Summary) -> bool, T: Item, U> {
cursor: Cursor<'a, T, (), U>,
filter_node: F,
@@ -2,5 +2,7 @@ use ctor::ctor;
#[ctor]
fn init_logger() {
- env_logger::init();
+ env_logger::builder()
+ .filter_level(log::LevelFilter::Info)
+ .init();
}
@@ -5,7 +5,7 @@ use crate::{
rect::RectF,
vector::{vec2f, Vector2F},
},
- platform, scene, PaintContext,
+ platform, scene, FontSystem, PaintContext,
};
use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
@@ -14,6 +14,7 @@ use std::{
borrow::Borrow,
collections::HashMap,
hash::{Hash, Hasher},
+ iter,
sync::Arc,
};
@@ -23,6 +24,13 @@ pub struct TextLayoutCache {
fonts: Arc<dyn platform::FontSystem>,
}
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct RunStyle {
+ pub color: Color,
+ pub font_id: FontId,
+ pub underline: bool,
+}
+
impl TextLayoutCache {
pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self {
Self {
@@ -43,7 +51,7 @@ impl TextLayoutCache {
&'a self,
text: &'a str,
font_size: f32,
- runs: &'a [(usize, FontId, Color)],
+ runs: &'a [(usize, RunStyle)],
) -> Line {
let key = &CacheKeyRef {
text,
@@ -94,7 +102,7 @@ impl<'a> Hash for (dyn CacheKey + 'a) {
struct CacheKeyValue {
text: String,
font_size: OrderedFloat<f32>,
- runs: SmallVec<[(usize, FontId, Color); 1]>,
+ runs: SmallVec<[(usize, RunStyle); 1]>,
}
impl CacheKey for CacheKeyValue {
@@ -119,11 +127,11 @@ impl<'a> Borrow<dyn CacheKey + 'a> for CacheKeyValue {
}
}
-#[derive(Copy, Clone, PartialEq, Eq, Hash)]
+#[derive(Copy, Clone)]
struct CacheKeyRef<'a> {
text: &'a str,
font_size: OrderedFloat<f32>,
- runs: &'a [(usize, FontId, Color)],
+ runs: &'a [(usize, RunStyle)],
}
impl<'a> CacheKey for CacheKeyRef<'a> {
@@ -132,10 +140,34 @@ impl<'a> CacheKey for CacheKeyRef<'a> {
}
}
+impl<'a> PartialEq for CacheKeyRef<'a> {
+ fn eq(&self, other: &Self) -> bool {
+ self.text == other.text
+ && self.font_size == other.font_size
+ && self.runs.len() == other.runs.len()
+ && self.runs.iter().zip(other.runs.iter()).all(
+ |((len_a, style_a), (len_b, style_b))| {
+ len_a == len_b && style_a.font_id == style_b.font_id
+ },
+ )
+ }
+}
+
+impl<'a> Hash for CacheKeyRef<'a> {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.text.hash(state);
+ self.font_size.hash(state);
+ for (len, style_id) in self.runs {
+ len.hash(state);
+ style_id.font_id.hash(state);
+ }
+ }
+}
+
#[derive(Default, Debug)]
pub struct Line {
layout: Arc<LineLayout>,
- color_runs: SmallVec<[(u32, Color); 32]>,
+ style_runs: SmallVec<[(u32, Color, bool); 32]>,
}
#[derive(Default, Debug)]
@@ -162,12 +194,16 @@ pub struct Glyph {
}
impl Line {
- fn new(layout: Arc<LineLayout>, runs: &[(usize, FontId, Color)]) -> Self {
- let mut color_runs = SmallVec::new();
- for (len, _, color) in runs {
- color_runs.push((*len as u32, *color));
+ fn new(layout: Arc<LineLayout>, runs: &[(usize, RunStyle)]) -> Self {
+ let mut style_runs = SmallVec::new();
+ for (len, style) in runs {
+ style_runs.push((*len as u32, style.color, style.underline));
}
- Self { layout, color_runs }
+ Self { layout, style_runs }
+ }
+
+ pub fn runs(&self) -> &[Run] {
+ &self.layout.runs
}
pub fn width(&self) -> f32 {
@@ -200,13 +236,20 @@ impl Line {
}
}
- pub fn paint(&self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext) {
- let padding_top = (visible_bounds.height() - self.layout.ascent - self.layout.descent) / 2.;
- let baseline_origin = vec2f(0., padding_top + self.layout.ascent);
+ pub fn paint(
+ &self,
+ origin: Vector2F,
+ visible_bounds: RectF,
+ line_height: f32,
+ cx: &mut PaintContext,
+ ) {
+ let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.;
+ let baseline_offset = vec2f(0., padding_top + self.layout.ascent);
- let mut color_runs = self.color_runs.iter();
- let mut color_end = 0;
+ let mut style_runs = self.style_runs.iter();
+ let mut run_end = 0;
let mut color = Color::black();
+ let mut underline_start = None;
for run in &self.layout.runs {
let max_glyph_width = cx
@@ -215,7 +258,7 @@ impl Line {
.x();
for glyph in &run.glyphs {
- let glyph_origin = baseline_origin + glyph.position;
+ let glyph_origin = origin + baseline_offset + glyph.position;
if glyph_origin.x() + max_glyph_width < visible_bounds.origin().x() {
continue;
@@ -224,13 +267,43 @@ impl Line {
break;
}
- if glyph.index >= color_end {
- if let Some(next_run) = color_runs.next() {
- color_end += next_run.0 as usize;
- color = next_run.1;
+ if glyph.index >= run_end {
+ if let Some((run_len, run_color, run_underlined)) = style_runs.next() {
+ if let Some(underline_origin) = underline_start {
+ if !*run_underlined || *run_color != color {
+ cx.scene.push_underline(scene::Quad {
+ bounds: RectF::from_points(
+ underline_origin,
+ glyph_origin + vec2f(0., 1.),
+ ),
+ background: Some(color),
+ border: Default::default(),
+ corner_radius: 0.,
+ });
+ underline_start = None;
+ }
+ }
+
+ if *run_underlined {
+ underline_start.get_or_insert(glyph_origin);
+ }
+
+ run_end += *run_len as usize;
+ color = *run_color;
} else {
- color_end = self.layout.len;
+ run_end = self.layout.len;
color = Color::black();
+ if let Some(underline_origin) = underline_start.take() {
+ cx.scene.push_underline(scene::Quad {
+ bounds: RectF::from_points(
+ underline_origin,
+ glyph_origin + vec2f(0., 1.),
+ ),
+ background: Some(color),
+ border: Default::default(),
+ corner_radius: 0.,
+ });
+ }
}
}
@@ -238,10 +311,417 @@ impl Line {
font_id: run.font_id,
font_size: self.layout.font_size,
id: glyph.id,
- origin: origin + glyph_origin,
+ origin: glyph_origin,
color,
});
}
}
+
+ if let Some(underline_start) = underline_start.take() {
+ let line_end = origin + baseline_offset + vec2f(self.layout.width, 0.);
+
+ cx.scene.push_underline(scene::Quad {
+ bounds: RectF::from_points(underline_start, line_end + vec2f(0., 1.)),
+ background: Some(color),
+ border: Default::default(),
+ corner_radius: 0.,
+ });
+ }
+ }
+
+ pub fn paint_wrapped(
+ &self,
+ origin: Vector2F,
+ visible_bounds: RectF,
+ line_height: f32,
+ boundaries: impl IntoIterator<Item = ShapedBoundary>,
+ cx: &mut PaintContext,
+ ) {
+ let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.;
+ let baseline_origin = vec2f(0., padding_top + self.layout.ascent);
+
+ let mut boundaries = boundaries.into_iter().peekable();
+ let mut color_runs = self.style_runs.iter();
+ let mut color_end = 0;
+ let mut color = Color::black();
+
+ let mut glyph_origin = vec2f(0., 0.);
+ let mut prev_position = 0.;
+ for run in &self.layout.runs {
+ for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
+ if boundaries.peek().map_or(false, |b| b.glyph_ix == glyph_ix) {
+ boundaries.next();
+ glyph_origin = vec2f(0., glyph_origin.y() + line_height);
+ } else {
+ glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position);
+ }
+ prev_position = glyph.position.x();
+
+ if glyph.index >= color_end {
+ if let Some(next_run) = color_runs.next() {
+ color_end += next_run.0 as usize;
+ color = next_run.1;
+ } else {
+ color_end = self.layout.len;
+ color = Color::black();
+ }
+ }
+
+ let glyph_bounds = RectF::new(
+ origin + glyph_origin,
+ cx.font_cache
+ .bounding_box(run.font_id, self.layout.font_size),
+ );
+ if glyph_bounds.intersects(visible_bounds) {
+ cx.scene.push_glyph(scene::Glyph {
+ font_id: run.font_id,
+ font_size: self.layout.font_size,
+ id: glyph.id,
+ origin: glyph_bounds.origin() + baseline_origin,
+ color,
+ });
+ }
+ }
+ }
+ }
+}
+
+impl Run {
+ pub fn glyphs(&self) -> &[Glyph] {
+ &self.glyphs
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct Boundary {
+ pub ix: usize,
+ pub next_indent: u32,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct ShapedBoundary {
+ pub run_ix: usize,
+ pub glyph_ix: usize,
+}
+
+impl Boundary {
+ fn new(ix: usize, next_indent: u32) -> Self {
+ Self { ix, next_indent }
+ }
+}
+
+pub struct LineWrapper {
+ font_system: Arc<dyn FontSystem>,
+ pub(crate) font_id: FontId,
+ pub(crate) font_size: f32,
+ cached_ascii_char_widths: [f32; 128],
+ cached_other_char_widths: HashMap<char, f32>,
+}
+
+impl LineWrapper {
+ pub const MAX_INDENT: u32 = 256;
+
+ pub fn new(font_id: FontId, font_size: f32, font_system: Arc<dyn FontSystem>) -> Self {
+ Self {
+ font_system,
+ font_id,
+ font_size,
+ cached_ascii_char_widths: [f32::NAN; 128],
+ cached_other_char_widths: HashMap::new(),
+ }
+ }
+
+ pub fn wrap_line<'a>(
+ &'a mut self,
+ line: &'a str,
+ wrap_width: f32,
+ ) -> impl Iterator<Item = Boundary> + 'a {
+ let mut width = 0.0;
+ let mut first_non_whitespace_ix = None;
+ let mut indent = None;
+ let mut last_candidate_ix = 0;
+ let mut last_candidate_width = 0.0;
+ let mut last_wrap_ix = 0;
+ let mut prev_c = '\0';
+ let mut char_indices = line.char_indices();
+ iter::from_fn(move || {
+ while let Some((ix, c)) = char_indices.next() {
+ if c == '\n' {
+ continue;
+ }
+
+ if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() {
+ last_candidate_ix = ix;
+ last_candidate_width = width;
+ }
+
+ if c != ' ' && first_non_whitespace_ix.is_none() {
+ first_non_whitespace_ix = Some(ix);
+ }
+
+ let char_width = self.width_for_char(c);
+ width += char_width;
+ if width > wrap_width && ix > last_wrap_ix {
+ if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
+ {
+ indent = Some(
+ Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
+ );
+ }
+
+ if last_candidate_ix > 0 {
+ last_wrap_ix = last_candidate_ix;
+ width -= last_candidate_width;
+ last_candidate_ix = 0;
+ } else {
+ last_wrap_ix = ix;
+ width = char_width;
+ }
+
+ let indent_width =
+ indent.map(|indent| indent as f32 * self.width_for_char(' '));
+ width += indent_width.unwrap_or(0.);
+
+ return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
+ }
+ prev_c = c;
+ }
+
+ None
+ })
+ }
+
+ pub fn wrap_shaped_line<'a>(
+ &'a mut self,
+ str: &'a str,
+ line: &'a Line,
+ wrap_width: f32,
+ ) -> impl Iterator<Item = ShapedBoundary> + 'a {
+ let mut first_non_whitespace_ix = None;
+ let mut last_candidate_ix = None;
+ let mut last_candidate_x = 0.0;
+ let mut last_wrap_ix = ShapedBoundary {
+ run_ix: 0,
+ glyph_ix: 0,
+ };
+ let mut last_wrap_x = 0.;
+ let mut prev_c = '\0';
+ let mut glyphs = line
+ .runs()
+ .iter()
+ .enumerate()
+ .flat_map(move |(run_ix, run)| {
+ run.glyphs()
+ .iter()
+ .enumerate()
+ .map(move |(glyph_ix, glyph)| {
+ let character = str[glyph.index..].chars().next().unwrap();
+ (
+ ShapedBoundary { run_ix, glyph_ix },
+ character,
+ glyph.position.x(),
+ )
+ })
+ })
+ .peekable();
+
+ iter::from_fn(move || {
+ while let Some((ix, c, x)) = glyphs.next() {
+ if c == '\n' {
+ continue;
+ }
+
+ if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() {
+ last_candidate_ix = Some(ix);
+ last_candidate_x = x;
+ }
+
+ if c != ' ' && first_non_whitespace_ix.is_none() {
+ first_non_whitespace_ix = Some(ix);
+ }
+
+ let next_x = glyphs.peek().map_or(line.width(), |(_, _, x)| *x);
+ let width = next_x - last_wrap_x;
+ if width > wrap_width && ix > last_wrap_ix {
+ if let Some(last_candidate_ix) = last_candidate_ix.take() {
+ last_wrap_ix = last_candidate_ix;
+ last_wrap_x = last_candidate_x;
+ } else {
+ last_wrap_ix = ix;
+ last_wrap_x = x;
+ }
+
+ return Some(last_wrap_ix);
+ }
+ prev_c = c;
+ }
+
+ None
+ })
+ }
+
+ fn is_boundary(&self, prev: char, next: char) -> bool {
+ (prev == ' ') && (next != ' ')
+ }
+
+ #[inline(always)]
+ fn width_for_char(&mut self, c: char) -> f32 {
+ if (c as u32) < 128 {
+ let mut width = self.cached_ascii_char_widths[c as usize];
+ if width.is_nan() {
+ width = self.compute_width_for_char(c);
+ self.cached_ascii_char_widths[c as usize] = width;
+ }
+ width
+ } else {
+ let mut width = self
+ .cached_other_char_widths
+ .get(&c)
+ .copied()
+ .unwrap_or(f32::NAN);
+ if width.is_nan() {
+ width = self.compute_width_for_char(c);
+ self.cached_other_char_widths.insert(c, width);
+ }
+ width
+ }
+ }
+
+ fn compute_width_for_char(&self, c: char) -> f32 {
+ self.font_system
+ .layout_line(
+ &c.to_string(),
+ self.font_size,
+ &[(
+ 1,
+ RunStyle {
+ font_id: self.font_id,
+ color: Default::default(),
+ underline: false,
+ },
+ )],
+ )
+ .width
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::fonts::{Properties, Weight};
+
+ #[crate::test(self)]
+ fn test_wrap_line(cx: &mut crate::MutableAppContext) {
+ let font_cache = cx.font_cache().clone();
+ let font_system = cx.platform().fonts();
+ let family = font_cache.load_family(&["Courier"]).unwrap();
+ let font_id = font_cache.select_font(family, &Default::default()).unwrap();
+
+ let mut wrapper = LineWrapper::new(font_id, 16., font_system);
+ assert_eq!(
+ wrapper
+ .wrap_line("aa bbb cccc ddddd eeee", 72.0)
+ .collect::<Vec<_>>(),
+ &[
+ Boundary::new(7, 0),
+ Boundary::new(12, 0),
+ Boundary::new(18, 0)
+ ],
+ );
+ assert_eq!(
+ wrapper
+ .wrap_line("aaa aaaaaaaaaaaaaaaaaa", 72.0)
+ .collect::<Vec<_>>(),
+ &[
+ Boundary::new(4, 0),
+ Boundary::new(11, 0),
+ Boundary::new(18, 0)
+ ],
+ );
+ assert_eq!(
+ wrapper.wrap_line(" aaaaaaa", 72.).collect::<Vec<_>>(),
+ &[
+ Boundary::new(7, 5),
+ Boundary::new(9, 5),
+ Boundary::new(11, 5),
+ ]
+ );
+ assert_eq!(
+ wrapper
+ .wrap_line(" ", 72.)
+ .collect::<Vec<_>>(),
+ &[
+ Boundary::new(7, 0),
+ Boundary::new(14, 0),
+ Boundary::new(21, 0)
+ ]
+ );
+ assert_eq!(
+ wrapper
+ .wrap_line(" aaaaaaaaaaaaaa", 72.)
+ .collect::<Vec<_>>(),
+ &[
+ Boundary::new(7, 0),
+ Boundary::new(14, 3),
+ Boundary::new(18, 3),
+ Boundary::new(22, 3),
+ ]
+ );
+ }
+
+ #[crate::test(self)]
+ fn test_wrap_shaped_line(cx: &mut crate::MutableAppContext) {
+ let font_cache = cx.font_cache().clone();
+ let font_system = cx.platform().fonts();
+ let text_layout_cache = TextLayoutCache::new(font_system.clone());
+
+ let family = font_cache.load_family(&["Helvetica"]).unwrap();
+ let font_id = font_cache.select_font(family, &Default::default()).unwrap();
+ let normal = RunStyle {
+ font_id,
+ color: Default::default(),
+ underline: false,
+ };
+ let bold = RunStyle {
+ font_id: font_cache
+ .select_font(
+ family,
+ &Properties {
+ weight: Weight::BOLD,
+ ..Default::default()
+ },
+ )
+ .unwrap(),
+ color: Default::default(),
+ underline: false,
+ };
+
+ let text = "aa bbb cccc ddddd eeee";
+ let line = text_layout_cache.layout_str(
+ text,
+ 16.0,
+ &[(4, normal), (5, bold), (6, normal), (1, bold), (7, normal)],
+ );
+
+ let mut wrapper = LineWrapper::new(font_id, 16., font_system);
+ assert_eq!(
+ wrapper
+ .wrap_shaped_line(&text, &line, 72.0)
+ .collect::<Vec<_>>(),
+ &[
+ ShapedBoundary {
+ run_ix: 1,
+ glyph_ix: 3
+ },
+ ShapedBoundary {
+ run_ix: 2,
+ glyph_ix: 3
+ },
+ ShapedBoundary {
+ run_ix: 4,
+ glyph_ix: 2
+ }
+ ],
+ );
}
}
@@ -0,0 +1,7 @@
+mod select;
+
+pub use select::{ItemType, Select, SelectStyle};
+
+pub fn init(cx: &mut super::MutableAppContext) {
+ select::init(cx);
+}
@@ -0,0 +1,169 @@
+use crate::{
+ action, elements::*, AppContext, Entity, MutableAppContext, RenderContext, View, ViewContext,
+ WeakViewHandle,
+};
+
+pub struct Select {
+ handle: WeakViewHandle<Self>,
+ render_item: Box<dyn Fn(usize, ItemType, bool, &AppContext) -> ElementBox>,
+ selected_item_ix: usize,
+ item_count: usize,
+ is_open: bool,
+ list_state: UniformListState,
+ build_style: Option<Box<dyn FnMut(&mut MutableAppContext) -> SelectStyle>>,
+}
+
+#[derive(Clone, Default)]
+pub struct SelectStyle {
+ pub header: ContainerStyle,
+ pub menu: ContainerStyle,
+}
+
+pub enum ItemType {
+ Header,
+ Selected,
+ Unselected,
+}
+
+action!(ToggleSelect);
+action!(SelectItem, usize);
+
+pub enum Event {}
+
+pub fn init(cx: &mut MutableAppContext) {
+ cx.add_action(Select::toggle);
+ cx.add_action(Select::select_item);
+}
+
+impl Select {
+ pub fn new<F: 'static + Fn(usize, ItemType, bool, &AppContext) -> ElementBox>(
+ item_count: usize,
+ cx: &mut ViewContext<Self>,
+ render_item: F,
+ ) -> Self {
+ Self {
+ handle: cx.handle().downgrade(),
+ render_item: Box::new(render_item),
+ selected_item_ix: 0,
+ item_count,
+ is_open: false,
+ list_state: UniformListState::default(),
+ build_style: Default::default(),
+ }
+ }
+
+ pub fn with_style(
+ mut self,
+ f: impl 'static + FnMut(&mut MutableAppContext) -> SelectStyle,
+ ) -> Self {
+ self.build_style = Some(Box::new(f));
+ self
+ }
+
+ pub fn set_item_count(&mut self, count: usize, cx: &mut ViewContext<Self>) {
+ self.item_count = count;
+ cx.notify();
+ }
+
+ fn toggle(&mut self, _: &ToggleSelect, cx: &mut ViewContext<Self>) {
+ self.is_open = !self.is_open;
+ cx.notify();
+ }
+
+ fn select_item(&mut self, action: &SelectItem, cx: &mut ViewContext<Self>) {
+ self.selected_item_ix = action.0;
+ self.is_open = false;
+ cx.notify();
+ }
+
+ pub fn selected_index(&self) -> usize {
+ self.selected_item_ix
+ }
+}
+
+impl Entity for Select {
+ type Event = Event;
+}
+
+impl View for Select {
+ fn ui_name() -> &'static str {
+ "Select"
+ }
+
+ fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+ if self.item_count == 0 {
+ return Empty::new().boxed();
+ }
+
+ enum Header {}
+ enum Item {}
+
+ let style = if let Some(build_style) = self.build_style.as_mut() {
+ (build_style)(cx)
+ } else {
+ Default::default()
+ };
+ let mut result = Flex::column().with_child(
+ MouseEventHandler::new::<Header, _, _, _>(self.handle.id(), cx, |mouse_state, cx| {
+ Container::new((self.render_item)(
+ self.selected_item_ix,
+ ItemType::Header,
+ mouse_state.hovered,
+ cx,
+ ))
+ .with_style(&style.header)
+ .boxed()
+ })
+ .on_click(move |cx| cx.dispatch_action(ToggleSelect))
+ .boxed(),
+ );
+ if self.is_open {
+ let handle = self.handle.clone();
+ result.add_child(
+ Overlay::new(
+ Container::new(
+ ConstrainedBox::new(
+ UniformList::new(
+ self.list_state.clone(),
+ self.item_count,
+ move |mut range, items, mut cx| {
+ let handle = handle.upgrade(cx).unwrap();
+ let this = handle.read(cx);
+ let selected_item_ix = this.selected_item_ix;
+ range.end = range.end.min(this.item_count);
+ items.extend(range.map(|ix| {
+ MouseEventHandler::new::<Item, _, _, _>(
+ (handle.id(), ix),
+ &mut cx,
+ |mouse_state, cx| {
+ (handle.read(*cx).render_item)(
+ ix,
+ if ix == selected_item_ix {
+ ItemType::Selected
+ } else {
+ ItemType::Unselected
+ },
+ mouse_state.hovered,
+ cx,
+ )
+ },
+ )
+ .on_click(move |cx| cx.dispatch_action(SelectItem(ix)))
+ .boxed()
+ }))
+ },
+ )
+ .boxed(),
+ )
+ .with_max_height(200.)
+ .boxed(),
+ )
+ .with_style(&style.menu)
+ .boxed(),
+ )
+ .boxed(),
+ )
+ }
+ result.boxed()
+ }
+}
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+set -e
+cd server
+cargo run --features seed-support --bin seed
@@ -3,7 +3,9 @@
set -e
# Install sqlx-cli if needed
-[[ "$(sqlx --version)" == "sqlx-cli 0.5.5" ]] || cargo install sqlx-cli --version 0.5.5
+[[ "$(sqlx --version)" == "sqlx-cli 0.5.7" ]] || cargo install sqlx-cli --version 0.5.7
+
+cd server
# Export contents of .env.toml
eval "$(cargo run --bin dotenv)"
@@ -5,6 +5,10 @@ edition = "2018"
name = "zed-server"
version = "0.1.0"
+[[bin]]
+name = "seed"
+required-features = ["seed-support"]
+
[dependencies]
anyhow = "1.0.40"
async-std = { version = "1.8.0", features = ["attributes"] }
@@ -19,18 +23,20 @@ futures = "0.3"
handlebars = "3.5"
http-auth-basic = "0.1.3"
jwt-simple = "0.10.0"
+lipsum = { version = "0.8", optional = true }
oauth2 = { version = "4.0.0", default_features = false }
oauth2-surf = "0.1.1"
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
rand = "0.8"
-rust-embed = "5.9.0"
+rust-embed = { version = "6.2", features = ["include-exclude"] }
scrypt = "0.7"
serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.9"
surf = "2.2.0"
tide = "0.16.0"
tide-compress = "0.9.0"
+time = "0.2"
toml = "0.5.8"
zrpc = { path = "../zrpc" }
@@ -41,10 +47,13 @@ default-features = false
[dependencies.sqlx]
version = "0.5.2"
-features = ["runtime-async-std-rustls", "postgres"]
+features = ["runtime-async-std-rustls", "postgres", "time"]
[dev-dependencies]
gpui = { path = "../gpui" }
-zed = { path = "../zed", features = ["test-support"] }
lazy_static = "1.4"
serde_json = { version = "1.0.64", features = ["preserve_order"] }
+zed = { path = "../zed", features = ["test-support"] }
+
+[features]
+seed-support = ["lipsum"]
@@ -0,0 +1,46 @@
+CREATE TABLE IF NOT EXISTS "orgs" (
+ "id" SERIAL PRIMARY KEY,
+ "name" VARCHAR NOT NULL,
+ "slug" VARCHAR NOT NULL
+);
+
+CREATE UNIQUE INDEX "index_orgs_slug" ON "orgs" ("slug");
+
+CREATE TABLE IF NOT EXISTS "org_memberships" (
+ "id" SERIAL PRIMARY KEY,
+ "org_id" INTEGER REFERENCES orgs (id) NOT NULL,
+ "user_id" INTEGER REFERENCES users (id) NOT NULL,
+ "admin" BOOLEAN NOT NULL
+);
+
+CREATE INDEX "index_org_memberships_user_id" ON "org_memberships" ("user_id");
+CREATE UNIQUE INDEX "index_org_memberships_org_id_and_user_id" ON "org_memberships" ("org_id", "user_id");
+
+CREATE TABLE IF NOT EXISTS "channels" (
+ "id" SERIAL PRIMARY KEY,
+ "owner_id" INTEGER NOT NULL,
+ "owner_is_user" BOOLEAN NOT NULL,
+ "name" VARCHAR NOT NULL
+);
+
+CREATE UNIQUE INDEX "index_channels_owner_and_name" ON "channels" ("owner_is_user", "owner_id", "name");
+
+CREATE TABLE IF NOT EXISTS "channel_memberships" (
+ "id" SERIAL PRIMARY KEY,
+ "channel_id" INTEGER REFERENCES channels (id) NOT NULL,
+ "user_id" INTEGER REFERENCES users (id) NOT NULL,
+ "admin" BOOLEAN NOT NULL
+);
+
+CREATE INDEX "index_channel_memberships_user_id" ON "channel_memberships" ("user_id");
+CREATE UNIQUE INDEX "index_channel_memberships_channel_id_and_user_id" ON "channel_memberships" ("channel_id", "user_id");
+
+CREATE TABLE IF NOT EXISTS "channel_messages" (
+ "id" SERIAL PRIMARY KEY,
+ "channel_id" INTEGER REFERENCES channels (id) NOT NULL,
+ "sender_id" INTEGER REFERENCES users (id) NOT NULL,
+ "body" TEXT NOT NULL,
+ "sent_at" TIMESTAMP
+);
+
+CREATE INDEX "index_channel_messages_channel_id" ON "channel_messages" ("channel_id");
@@ -1,7 +1,6 @@
-use crate::{auth::RequestExt as _, AppState, DbPool, LayoutData, Request, RequestExt as _};
+use crate::{auth::RequestExt as _, db, AppState, LayoutData, Request, RequestExt as _};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
-use sqlx::{Executor, FromRow};
use std::sync::Arc;
use surf::http::mime;
@@ -41,23 +40,8 @@ pub fn add_routes(app: &mut tide::Server<Arc<AppState>>) {
struct AdminData {
#[serde(flatten)]
layout: Arc<LayoutData>,
- users: Vec<User>,
- signups: Vec<Signup>,
-}
-
-#[derive(Debug, FromRow, Serialize)]
-pub struct User {
- pub id: i32,
- pub github_login: String,
- pub admin: bool,
-}
-
-#[derive(Debug, FromRow, Serialize)]
-pub struct Signup {
- pub id: i32,
- pub github_login: String,
- pub email_address: String,
- pub about: String,
+ users: Vec<db::User>,
+ signups: Vec<db::Signup>,
}
async fn get_admin_page(mut request: Request) -> tide::Result {
@@ -65,12 +49,8 @@ async fn get_admin_page(mut request: Request) -> tide::Result {
let data = AdminData {
layout: request.layout_data().await?,
- users: sqlx::query_as("SELECT * FROM users ORDER BY github_login ASC")
- .fetch_all(request.db())
- .await?,
- signups: sqlx::query_as("SELECT * FROM signups ORDER BY id DESC")
- .fetch_all(request.db())
- .await?,
+ users: request.db().get_all_users().await?,
+ signups: request.db().get_all_signups().await?,
};
Ok(tide::Response::builder(200)
@@ -96,7 +76,7 @@ async fn post_user(mut request: Request) -> tide::Result {
.unwrap_or(&form.github_login);
if !github_login.is_empty() {
- create_user(request.db(), github_login, form.admin).await?;
+ request.db().create_user(github_login, form.admin).await?;
}
Ok(tide::Redirect::new("/admin").into())
@@ -105,7 +85,7 @@ async fn post_user(mut request: Request) -> tide::Result {
async fn put_user(mut request: Request) -> tide::Result {
request.require_admin().await?;
- let user_id = request.param("id")?.parse::<i32>()?;
+ let user_id = request.param("id")?.parse()?;
#[derive(Deserialize)]
struct Body {
@@ -116,11 +96,7 @@ async fn put_user(mut request: Request) -> tide::Result {
request
.db()
- .execute(
- sqlx::query("UPDATE users SET admin = $1 WHERE id = $2;")
- .bind(body.admin)
- .bind(user_id),
- )
+ .set_user_is_admin(db::UserId(user_id), body.admin)
.await?;
Ok(tide::Response::builder(200).build())
@@ -128,33 +104,14 @@ async fn put_user(mut request: Request) -> tide::Result {
async fn delete_user(request: Request) -> tide::Result {
request.require_admin().await?;
-
- let user_id = request.param("id")?.parse::<i32>()?;
- request
- .db()
- .execute(sqlx::query("DELETE FROM users WHERE id = $1;").bind(user_id))
- .await?;
-
+ let user_id = db::UserId(request.param("id")?.parse()?);
+ request.db().delete_user(user_id).await?;
Ok(tide::Redirect::new("/admin").into())
}
-pub async fn create_user(db: &DbPool, github_login: &str, admin: bool) -> tide::Result<i32> {
- let id: i32 =
- sqlx::query_scalar("INSERT INTO users (github_login, admin) VALUES ($1, $2) RETURNING id;")
- .bind(github_login)
- .bind(admin)
- .fetch_one(db)
- .await?;
- Ok(id)
-}
-
async fn delete_signup(request: Request) -> tide::Result {
request.require_admin().await?;
- let signup_id = request.param("id")?.parse::<i32>()?;
- request
- .db()
- .execute(sqlx::query("DELETE FROM signups WHERE id = $1;").bind(signup_id))
- .await?;
-
+ let signup_id = db::SignupId(request.param("id")?.parse()?);
+ request.db().delete_signup(signup_id).await?;
Ok(tide::Redirect::new("/admin").into())
}
@@ -26,6 +26,6 @@ async fn get_static_asset(request: Request) -> tide::Result {
Ok(tide::Response::builder(200)
.content_type(content_type)
- .body(content.as_ref())
+ .body(content.data.as_ref())
.build())
}
@@ -1,7 +1,9 @@
-use super::errors::TideResultExt;
-use crate::{github, rpc, AppState, DbPool, Request, RequestExt as _};
+use super::{
+ db::{self, UserId},
+ errors::TideResultExt,
+};
+use crate::{github, AppState, Request, RequestExt as _};
use anyhow::{anyhow, Context};
-use async_std::stream::StreamExt;
use async_trait::async_trait;
pub use oauth2::basic::BasicClient as Client;
use oauth2::{
@@ -14,11 +16,10 @@ use scrypt::{
Scrypt,
};
use serde::{Deserialize, Serialize};
-use sqlx::FromRow;
use std::{borrow::Cow, convert::TryFrom, sync::Arc};
use surf::Url;
use tide::Server;
-use zrpc::{auth as zed_auth, proto, Peer};
+use zrpc::auth as zed_auth;
static CURRENT_GITHUB_USER: &'static str = "current_github_user";
static GITHUB_AUTH_URL: &'static str = "https://github.com/login/oauth/authorize";
@@ -34,9 +35,6 @@ pub struct User {
pub struct VerifyToken;
-#[derive(Clone, Copy)]
-pub struct UserId(pub i32);
-
#[async_trait]
impl tide::Middleware<Arc<AppState>> for VerifyToken {
async fn handle(
@@ -51,33 +49,28 @@ impl tide::Middleware<Arc<AppState>> for VerifyToken {
.as_str()
.split_whitespace();
- let user_id: i32 = auth_header
- .next()
- .ok_or_else(|| anyhow!("missing user id in authorization header"))?
- .parse()?;
+ let user_id = UserId(
+ auth_header
+ .next()
+ .ok_or_else(|| anyhow!("missing user id in authorization header"))?
+ .parse()?,
+ );
let access_token = auth_header
.next()
.ok_or_else(|| anyhow!("missing access token in authorization header"))?;
let state = request.state().clone();
- let mut password_hashes =
- sqlx::query_scalar::<_, String>("SELECT hash FROM access_tokens WHERE user_id = $1")
- .bind(&user_id)
- .fetch_many(&state.db);
-
let mut credentials_valid = false;
- while let Some(password_hash) = password_hashes.next().await {
- if let either::Either::Right(password_hash) = password_hash? {
- if verify_access_token(&access_token, &password_hash)? {
- credentials_valid = true;
- break;
- }
+ for password_hash in state.db.get_access_token_hashes(user_id).await? {
+ if verify_access_token(&access_token, &password_hash)? {
+ credentials_valid = true;
+ break;
}
}
if credentials_valid {
- request.set_ext(UserId(user_id));
+ request.set_ext(user_id);
Ok(next.run(request).await)
} else {
Err(anyhow!("invalid credentials").into())
@@ -94,25 +87,12 @@ pub trait RequestExt {
impl RequestExt for Request {
async fn current_user(&self) -> tide::Result<Option<User>> {
if let Some(details) = self.session().get::<github::User>(CURRENT_GITHUB_USER) {
- #[derive(FromRow)]
- struct UserRow {
- admin: bool,
- }
-
- let user_row: Option<UserRow> =
- sqlx::query_as("SELECT admin FROM users WHERE github_login = $1")
- .bind(&details.login)
- .fetch_optional(self.db())
- .await?;
-
- let is_insider = user_row.is_some();
- let is_admin = user_row.map_or(false, |row| row.admin);
-
+ let user = self.db().get_user_by_github_login(&details.login).await?;
Ok(Some(User {
github_login: details.login,
avatar_url: details.avatar_url,
- is_insider,
- is_admin,
+ is_insider: user.is_some(),
+ is_admin: user.map_or(false, |user| user.admin),
}))
} else {
Ok(None)
@@ -120,43 +100,6 @@ impl RequestExt for Request {
}
}
-#[async_trait]
-pub trait PeerExt {
- async fn sign_out(
- self: &Arc<Self>,
- connection_id: zrpc::ConnectionId,
- state: &AppState,
- ) -> tide::Result<()>;
-}
-
-#[async_trait]
-impl PeerExt for Peer {
- async fn sign_out(
- self: &Arc<Self>,
- connection_id: zrpc::ConnectionId,
- state: &AppState,
- ) -> tide::Result<()> {
- self.disconnect(connection_id).await;
- let worktree_ids = state.rpc.write().await.remove_connection(connection_id);
- for worktree_id in worktree_ids {
- let state = state.rpc.read().await;
- if let Some(worktree) = state.worktrees.get(&worktree_id) {
- rpc::broadcast(connection_id, worktree.connection_ids(), |conn_id| {
- self.send(
- conn_id,
- proto::RemovePeer {
- worktree_id,
- peer_id: connection_id.0,
- },
- )
- })
- .await?;
- }
- }
- Ok(())
- }
-}
-
pub fn build_client(client_id: &str, client_secret: &str) -> Client {
Client::new(
ClientId::new(client_id.to_string()),
@@ -265,9 +208,9 @@ async fn get_auth_callback(mut request: Request) -> tide::Result {
.await
.context("failed to fetch user")?;
- let user_id: Option<i32> = sqlx::query_scalar("SELECT id from users where github_login = $1")
- .bind(&user_details.login)
- .fetch_optional(request.db())
+ let user = request
+ .db()
+ .get_user_by_github_login(&user_details.login)
.await?;
request
@@ -276,8 +219,8 @@ async fn get_auth_callback(mut request: Request) -> tide::Result {
// When signing in from the native app, generate a new access token for the current user. Return
// a redirect so that the user's browser sends this access token to the locally-running app.
- if let Some((user_id, app_sign_in_params)) = user_id.zip(query.native_app_sign_in_params) {
- let access_token = create_access_token(request.db(), user_id).await?;
+ if let Some((user, app_sign_in_params)) = user.zip(query.native_app_sign_in_params) {
+ let access_token = create_access_token(request.db(), user.id).await?;
let native_app_public_key =
zed_auth::PublicKey::try_from(app_sign_in_params.native_app_public_key.clone())
.context("failed to parse app public key")?;
@@ -287,7 +230,7 @@ async fn get_auth_callback(mut request: Request) -> tide::Result {
return Ok(tide::Redirect::new(&format!(
"http://127.0.0.1:{}?user_id={}&access_token={}",
- app_sign_in_params.native_app_port, user_id, encrypted_access_token,
+ app_sign_in_params.native_app_port, user.id.0, encrypted_access_token,
))
.into());
}
@@ -300,14 +243,11 @@ async fn post_sign_out(mut request: Request) -> tide::Result {
Ok(tide::Redirect::new("/").into())
}
-pub async fn create_access_token(db: &DbPool, user_id: i32) -> tide::Result<String> {
+pub async fn create_access_token(db: &db::Db, user_id: UserId) -> tide::Result<String> {
let access_token = zed_auth::random_token();
let access_token_hash =
hash_access_token(&access_token).context("failed to hash access token")?;
- sqlx::query("INSERT INTO access_tokens (user_id, hash) values ($1, $2)")
- .bind(user_id)
- .bind(access_token_hash)
- .fetch_optional(db)
+ db.create_access_token_hash(user_id, access_token_hash)
.await?;
Ok(access_token)
}
@@ -0,0 +1,91 @@
+use db::{Db, UserId};
+use rand::prelude::*;
+use tide::log;
+use time::{Duration, OffsetDateTime};
+
+#[allow(unused)]
+#[path = "../db.rs"]
+mod db;
+#[path = "../env.rs"]
+mod env;
+
+#[async_std::main]
+async fn main() {
+ if let Err(error) = env::load_dotenv() {
+ log::error!(
+ "error loading .env.toml (this is expected in production): {}",
+ error
+ );
+ }
+
+ let mut rng = StdRng::from_entropy();
+ let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var");
+ let db = Db::new(&database_url, 5)
+ .await
+ .expect("failed to connect to postgres database");
+
+ let zed_users = ["nathansobo", "maxbrunsfeld", "as-cii", "iamnbutler"];
+ let mut zed_user_ids = Vec::<UserId>::new();
+ for zed_user in zed_users {
+ if let Some(user_id) = db.get_user(zed_user).await.expect("failed to fetch user") {
+ zed_user_ids.push(user_id);
+ } else {
+ zed_user_ids.push(
+ db.create_user(zed_user, true)
+ .await
+ .expect("failed to insert user"),
+ );
+ }
+ }
+
+ let zed_org_id = if let Some(org) = db
+ .find_org_by_slug("zed")
+ .await
+ .expect("failed to fetch org")
+ {
+ org.id
+ } else {
+ db.create_org("Zed", "zed")
+ .await
+ .expect("failed to insert org")
+ };
+
+ let general_channel_id = if let Some(channel) = db
+ .get_org_channels(zed_org_id)
+ .await
+ .expect("failed to fetch channels")
+ .iter()
+ .find(|c| c.name == "General")
+ {
+ channel.id
+ } else {
+ let channel_id = db
+ .create_org_channel(zed_org_id, "General")
+ .await
+ .expect("failed to insert channel");
+
+ let now = OffsetDateTime::now_utc();
+ let max_seconds = Duration::days(100).as_seconds_f64();
+ let mut timestamps = (0..1000)
+ .map(|_| now - Duration::seconds_f64(rng.gen_range(0_f64..=max_seconds)))
+ .collect::<Vec<_>>();
+ timestamps.sort();
+ 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)
+ .await
+ .expect("failed to insert message");
+ }
+ channel_id
+ };
+
+ for user_id in zed_user_ids {
+ db.add_org_member(zed_org_id, user_id, true)
+ .await
+ .expect("failed to insert org membership");
+ db.add_channel_member(general_channel_id, user_id, true)
+ .await
+ .expect("failed to insert channel membership");
+ }
+}
@@ -0,0 +1,594 @@
+use anyhow::Context;
+use async_std::task::{block_on, yield_now};
+use serde::Serialize;
+use sqlx::{FromRow, Result};
+use time::OffsetDateTime;
+
+pub use async_sqlx_session::PostgresSessionStore as SessionStore;
+pub use sqlx::postgres::PgPoolOptions as DbOptions;
+
+macro_rules! test_support {
+ ($self:ident, { $($token:tt)* }) => {{
+ let body = async {
+ $($token)*
+ };
+ if $self.test_mode {
+ yield_now().await;
+ block_on(body)
+ } else {
+ body.await
+ }
+ }};
+}
+
+#[derive(Clone)]
+pub struct Db {
+ pool: sqlx::PgPool,
+ test_mode: bool,
+}
+
+impl Db {
+ pub async fn new(url: &str, max_connections: u32) -> tide::Result<Self> {
+ let pool = DbOptions::new()
+ .max_connections(max_connections)
+ .connect(url)
+ .await
+ .context("failed to connect to postgres database")?;
+ Ok(Self {
+ pool,
+ test_mode: false,
+ })
+ }
+
+ // signups
+
+ pub async fn create_signup(
+ &self,
+ github_login: &str,
+ email_address: &str,
+ about: &str,
+ ) -> Result<SignupId> {
+ test_support!(self, {
+ let query = "
+ INSERT INTO signups (github_login, email_address, about)
+ VALUES ($1, $2, $3)
+ RETURNING id
+ ";
+ sqlx::query_scalar(query)
+ .bind(github_login)
+ .bind(email_address)
+ .bind(about)
+ .fetch_one(&self.pool)
+ .await
+ .map(SignupId)
+ })
+ }
+
+ pub async fn get_all_signups(&self) -> Result<Vec<Signup>> {
+ test_support!(self, {
+ let query = "SELECT * FROM users ORDER BY github_login ASC";
+ sqlx::query_as(query).fetch_all(&self.pool).await
+ })
+ }
+
+ pub async fn delete_signup(&self, id: SignupId) -> Result<()> {
+ test_support!(self, {
+ let query = "DELETE FROM signups WHERE id = $1";
+ sqlx::query(query)
+ .bind(id.0)
+ .execute(&self.pool)
+ .await
+ .map(drop)
+ })
+ }
+
+ // users
+
+ #[allow(unused)] // Help rust-analyzer
+ #[cfg(any(test, feature = "seed-support"))]
+ pub async fn get_user(&self, github_login: &str) -> Result<Option<UserId>> {
+ test_support!(self, {
+ let query = "
+ SELECT id
+ FROM users
+ WHERE github_login = $1
+ ";
+ sqlx::query_scalar(query)
+ .bind(github_login)
+ .fetch_optional(&self.pool)
+ .await
+ })
+ }
+
+ pub async fn create_user(&self, github_login: &str, admin: bool) -> Result<UserId> {
+ test_support!(self, {
+ let query = "
+ INSERT INTO users (github_login, admin)
+ VALUES ($1, $2)
+ RETURNING id
+ ";
+ sqlx::query_scalar(query)
+ .bind(github_login)
+ .bind(admin)
+ .fetch_one(&self.pool)
+ .await
+ .map(UserId)
+ })
+ }
+
+ pub async fn get_all_users(&self) -> Result<Vec<User>> {
+ test_support!(self, {
+ let query = "SELECT * FROM users ORDER BY github_login ASC";
+ sqlx::query_as(query).fetch_all(&self.pool).await
+ })
+ }
+
+ pub async fn get_users_by_ids(
+ &self,
+ requester_id: UserId,
+ ids: impl Iterator<Item = UserId>,
+ ) -> Result<Vec<User>> {
+ test_support!(self, {
+ // Only return users that are in a common channel with the requesting user.
+ let query = "
+ SELECT users.*
+ FROM
+ users, channel_memberships
+ WHERE
+ users.id = ANY ($1) AND
+ channel_memberships.user_id = users.id AND
+ channel_memberships.channel_id IN (
+ SELECT channel_id
+ FROM channel_memberships
+ WHERE channel_memberships.user_id = $2
+ )
+ ";
+
+ sqlx::query_as(query)
+ .bind(&ids.map(|id| id.0).collect::<Vec<_>>())
+ .bind(requester_id)
+ .fetch_all(&self.pool)
+ .await
+ })
+ }
+
+ pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
+ test_support!(self, {
+ let query = "SELECT * FROM users WHERE github_login = $1 LIMIT 1";
+ sqlx::query_as(query)
+ .bind(github_login)
+ .fetch_optional(&self.pool)
+ .await
+ })
+ }
+
+ pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> {
+ test_support!(self, {
+ let query = "UPDATE users SET admin = $1 WHERE id = $2";
+ sqlx::query(query)
+ .bind(is_admin)
+ .bind(id.0)
+ .execute(&self.pool)
+ .await
+ .map(drop)
+ })
+ }
+
+ pub async fn delete_user(&self, id: UserId) -> Result<()> {
+ test_support!(self, {
+ let query = "DELETE FROM users WHERE id = $1;";
+ sqlx::query(query)
+ .bind(id.0)
+ .execute(&self.pool)
+ .await
+ .map(drop)
+ })
+ }
+
+ // access tokens
+
+ pub async fn create_access_token_hash(
+ &self,
+ user_id: UserId,
+ access_token_hash: String,
+ ) -> Result<()> {
+ test_support!(self, {
+ let query = "
+ INSERT INTO access_tokens (user_id, hash)
+ VALUES ($1, $2)
+ ";
+ sqlx::query(query)
+ .bind(user_id.0)
+ .bind(access_token_hash)
+ .execute(&self.pool)
+ .await
+ .map(drop)
+ })
+ }
+
+ pub async fn get_access_token_hashes(&self, user_id: UserId) -> Result<Vec<String>> {
+ test_support!(self, {
+ let query = "SELECT hash FROM access_tokens WHERE user_id = $1";
+ sqlx::query_scalar(query)
+ .bind(user_id.0)
+ .fetch_all(&self.pool)
+ .await
+ })
+ }
+
+ // orgs
+
+ #[allow(unused)] // Help rust-analyzer
+ #[cfg(any(test, feature = "seed-support"))]
+ pub async fn find_org_by_slug(&self, slug: &str) -> Result<Option<Org>> {
+ test_support!(self, {
+ let query = "
+ SELECT *
+ FROM orgs
+ WHERE slug = $1
+ ";
+ sqlx::query_as(query)
+ .bind(slug)
+ .fetch_optional(&self.pool)
+ .await
+ })
+ }
+
+ #[cfg(any(test, feature = "seed-support"))]
+ pub async fn create_org(&self, name: &str, slug: &str) -> Result<OrgId> {
+ test_support!(self, {
+ let query = "
+ INSERT INTO orgs (name, slug)
+ VALUES ($1, $2)
+ RETURNING id
+ ";
+ sqlx::query_scalar(query)
+ .bind(name)
+ .bind(slug)
+ .fetch_one(&self.pool)
+ .await
+ .map(OrgId)
+ })
+ }
+
+ #[cfg(any(test, feature = "seed-support"))]
+ pub async fn add_org_member(
+ &self,
+ org_id: OrgId,
+ user_id: UserId,
+ is_admin: bool,
+ ) -> Result<()> {
+ test_support!(self, {
+ let query = "
+ INSERT INTO org_memberships (org_id, user_id, admin)
+ VALUES ($1, $2, $3)
+ ON CONFLICT DO NOTHING
+ ";
+ sqlx::query(query)
+ .bind(org_id.0)
+ .bind(user_id.0)
+ .bind(is_admin)
+ .execute(&self.pool)
+ .await
+ .map(drop)
+ })
+ }
+
+ // channels
+
+ #[cfg(any(test, feature = "seed-support"))]
+ pub async fn create_org_channel(&self, org_id: OrgId, name: &str) -> Result<ChannelId> {
+ test_support!(self, {
+ let query = "
+ INSERT INTO channels (owner_id, owner_is_user, name)
+ VALUES ($1, false, $2)
+ RETURNING id
+ ";
+ sqlx::query_scalar(query)
+ .bind(org_id.0)
+ .bind(name)
+ .fetch_one(&self.pool)
+ .await
+ .map(ChannelId)
+ })
+ }
+
+ #[allow(unused)] // Help rust-analyzer
+ #[cfg(any(test, feature = "seed-support"))]
+ pub async fn get_org_channels(&self, org_id: OrgId) -> Result<Vec<Channel>> {
+ test_support!(self, {
+ let query = "
+ SELECT *
+ FROM channels
+ WHERE
+ channels.owner_is_user = false AND
+ channels.owner_id = $1
+ ";
+ sqlx::query_as(query)
+ .bind(org_id.0)
+ .fetch_all(&self.pool)
+ .await
+ })
+ }
+
+ pub async fn get_accessible_channels(&self, user_id: UserId) -> Result<Vec<Channel>> {
+ test_support!(self, {
+ let query = "
+ SELECT
+ channels.id, channels.name
+ FROM
+ channel_memberships, channels
+ WHERE
+ channel_memberships.user_id = $1 AND
+ channel_memberships.channel_id = channels.id
+ ";
+ sqlx::query_as(query)
+ .bind(user_id.0)
+ .fetch_all(&self.pool)
+ .await
+ })
+ }
+
+ pub async fn can_user_access_channel(
+ &self,
+ user_id: UserId,
+ channel_id: ChannelId,
+ ) -> Result<bool> {
+ test_support!(self, {
+ let query = "
+ SELECT id
+ FROM channel_memberships
+ WHERE user_id = $1 AND channel_id = $2
+ LIMIT 1
+ ";
+ sqlx::query_scalar::<_, i32>(query)
+ .bind(user_id.0)
+ .bind(channel_id.0)
+ .fetch_optional(&self.pool)
+ .await
+ .map(|e| e.is_some())
+ })
+ }
+
+ #[cfg(any(test, feature = "seed-support"))]
+ pub async fn add_channel_member(
+ &self,
+ channel_id: ChannelId,
+ user_id: UserId,
+ is_admin: bool,
+ ) -> Result<()> {
+ test_support!(self, {
+ let query = "
+ INSERT INTO channel_memberships (channel_id, user_id, admin)
+ VALUES ($1, $2, $3)
+ ON CONFLICT DO NOTHING
+ ";
+ sqlx::query(query)
+ .bind(channel_id.0)
+ .bind(user_id.0)
+ .bind(is_admin)
+ .execute(&self.pool)
+ .await
+ .map(drop)
+ })
+ }
+
+ // messages
+
+ pub async fn create_channel_message(
+ &self,
+ channel_id: ChannelId,
+ sender_id: UserId,
+ body: &str,
+ timestamp: OffsetDateTime,
+ ) -> Result<MessageId> {
+ test_support!(self, {
+ let query = "
+ INSERT INTO channel_messages (channel_id, sender_id, body, sent_at)
+ VALUES ($1, $2, $3, $4)
+ RETURNING id
+ ";
+ sqlx::query_scalar(query)
+ .bind(channel_id.0)
+ .bind(sender_id.0)
+ .bind(body)
+ .bind(timestamp)
+ .fetch_one(&self.pool)
+ .await
+ .map(MessageId)
+ })
+ }
+
+ pub async fn get_channel_messages(
+ &self,
+ channel_id: ChannelId,
+ count: usize,
+ before_id: Option<MessageId>,
+ ) -> Result<Vec<ChannelMessage>> {
+ test_support!(self, {
+ let query = r#"
+ SELECT * FROM (
+ SELECT
+ id, sender_id, body, sent_at AT TIME ZONE 'UTC' as sent_at
+ FROM
+ channel_messages
+ WHERE
+ channel_id = $1 AND
+ id < $2
+ ORDER BY id DESC
+ LIMIT $3
+ ) as recent_messages
+ ORDER BY id ASC
+ "#;
+ sqlx::query_as(query)
+ .bind(channel_id.0)
+ .bind(before_id.unwrap_or(MessageId::MAX))
+ .bind(count as i64)
+ .fetch_all(&self.pool)
+ .await
+ })
+ }
+}
+
+macro_rules! id_type {
+ ($name:ident) => {
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, sqlx::Type, Serialize)]
+ #[sqlx(transparent)]
+ #[serde(transparent)]
+ pub struct $name(pub i32);
+
+ impl $name {
+ #[allow(unused)]
+ pub const MAX: Self = Self(i32::MAX);
+
+ #[allow(unused)]
+ pub fn from_proto(value: u64) -> Self {
+ Self(value as i32)
+ }
+
+ #[allow(unused)]
+ pub fn to_proto(&self) -> u64 {
+ self.0 as u64
+ }
+ }
+ };
+}
+
+id_type!(UserId);
+#[derive(Debug, FromRow, Serialize)]
+pub struct User {
+ pub id: UserId,
+ pub github_login: String,
+ pub admin: bool,
+}
+
+id_type!(OrgId);
+#[derive(FromRow)]
+pub struct Org {
+ pub id: OrgId,
+ pub name: String,
+ pub slug: String,
+}
+
+id_type!(SignupId);
+#[derive(Debug, FromRow, Serialize)]
+pub struct Signup {
+ pub id: SignupId,
+ pub github_login: String,
+ pub email_address: String,
+ pub about: String,
+}
+
+id_type!(ChannelId);
+#[derive(Debug, FromRow, Serialize)]
+pub struct Channel {
+ pub id: ChannelId,
+ pub name: String,
+}
+
+id_type!(MessageId);
+#[derive(Debug, FromRow)]
+pub struct ChannelMessage {
+ pub id: MessageId,
+ pub sender_id: UserId,
+ pub body: String,
+ pub sent_at: OffsetDateTime,
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use rand::prelude::*;
+ use sqlx::{
+ migrate::{MigrateDatabase, Migrator},
+ Postgres,
+ };
+ use std::path::Path;
+
+ pub struct TestDb {
+ pub db: Db,
+ pub name: String,
+ pub url: String,
+ }
+
+ impl TestDb {
+ pub fn new() -> Self {
+ // Enable tests to run in parallel by serializing the creation of each test database.
+ lazy_static::lazy_static! {
+ static ref DB_CREATION: std::sync::Mutex<()> = std::sync::Mutex::new(());
+ }
+
+ let mut rng = StdRng::from_entropy();
+ let name = format!("zed-test-{}", rng.gen::<u128>());
+ let url = format!("postgres://postgres@localhost/{}", name);
+ let migrations_path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations"));
+ let db = block_on(async {
+ {
+ let _lock = DB_CREATION.lock();
+ Postgres::create_database(&url)
+ .await
+ .expect("failed to create test db");
+ }
+ let mut db = Db::new(&url, 5).await.unwrap();
+ db.test_mode = true;
+ let migrator = Migrator::new(migrations_path).await.unwrap();
+ migrator.run(&db.pool).await.unwrap();
+ db
+ });
+
+ Self { db, name, url }
+ }
+
+ pub fn db(&self) -> &Db {
+ &self.db
+ }
+ }
+
+ impl Drop for TestDb {
+ fn drop(&mut self) {
+ block_on(async {
+ let query = "
+ SELECT pg_terminate_backend(pg_stat_activity.pid)
+ FROM pg_stat_activity
+ WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid();
+ ";
+ sqlx::query(query)
+ .bind(&self.name)
+ .execute(&self.db.pool)
+ .await
+ .unwrap();
+ self.db.pool.close().await;
+ Postgres::drop_database(&self.url).await.unwrap();
+ });
+ }
+ }
+
+ #[gpui::test]
+ async fn test_recent_channel_messages() {
+ 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();
+ for i in 0..10 {
+ db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc())
+ .await
+ .unwrap();
+ }
+
+ let messages = db.get_channel_messages(channel, 5, None).await.unwrap();
+ assert_eq!(
+ messages.iter().map(|m| &m.body).collect::<Vec<_>>(),
+ ["5", "6", "7", "8", "9"]
+ );
+
+ let prev_messages = db
+ .get_channel_messages(channel, 4, Some(messages[0].id))
+ .await
+ .unwrap();
+ assert_eq!(
+ prev_messages.iter().map(|m| &m.body).collect::<Vec<_>>(),
+ ["1", "2", "3", "4"]
+ );
+ }
+}
@@ -3,7 +3,6 @@ use crate::{
};
use comrak::ComrakOptions;
use serde::{Deserialize, Serialize};
-use sqlx::Executor as _;
use std::sync::Arc;
use tide::{http::mime, log, Server};
@@ -76,14 +75,7 @@ async fn post_signup(mut request: Request) -> tide::Result {
// Save signup in the database
request
.db()
- .execute(
- sqlx::query(
- "INSERT INTO signups (github_login, email_address, about) VALUES ($1, $2, $3);",
- )
- .bind(&form.github_login)
- .bind(&form.email_address)
- .bind(&form.about),
- )
+ .create_signup(&form.github_login, &form.email_address, &form.about)
.await?;
let layout_data = request.layout_data().await?;
@@ -1,6 +1,7 @@
mod admin;
mod assets;
mod auth;
+mod db;
mod env;
mod errors;
mod expiring;
@@ -8,20 +9,17 @@ mod github;
mod home;
mod rpc;
mod team;
-#[cfg(test)]
-mod tests;
use self::errors::TideResultExt as _;
-use anyhow::{Context, Result};
-use async_sqlx_session::PostgresSessionStore;
-use async_std::{net::TcpListener, sync::RwLock as AsyncRwLock};
+use anyhow::Result;
+use async_std::net::TcpListener;
use async_trait::async_trait;
use auth::RequestExt as _;
+use db::Db;
use handlebars::{Handlebars, TemplateRenderError};
use parking_lot::RwLock;
use rust_embed::RustEmbed;
use serde::{Deserialize, Serialize};
-use sqlx::postgres::{PgPool, PgPoolOptions};
use std::sync::Arc;
use surf::http::cookies::SameSite;
use tide::{log, sessions::SessionMiddleware};
@@ -29,7 +27,6 @@ use tide_compress::CompressMiddleware;
use zrpc::Peer;
type Request = tide::Request<Arc<AppState>>;
-type DbPool = PgPool;
#[derive(RustEmbed)]
#[folder = "templates"]
@@ -47,23 +44,17 @@ pub struct Config {
}
pub struct AppState {
- db: sqlx::PgPool,
+ db: Db,
handlebars: RwLock<Handlebars<'static>>,
auth_client: auth::Client,
github_client: Arc<github::AppClient>,
repo_client: github::RepoClient,
- rpc: AsyncRwLock<rpc::State>,
config: Config,
}
impl AppState {
async fn new(config: Config) -> tide::Result<Arc<Self>> {
- let db = PgPoolOptions::new()
- .max_connections(5)
- .connect(&config.database_url)
- .await
- .context("failed to connect to postgres database")?;
-
+ let db = Db::new(&config.database_url, 5).await?;
let github_client =
github::AppClient::new(config.github_app_id, config.github_private_key.clone());
let repo_client = github_client
@@ -77,7 +68,6 @@ impl AppState {
auth_client: auth::build_client(&config.github_client_id, &config.github_client_secret),
github_client,
repo_client,
- rpc: Default::default(),
config,
};
this.register_partials();
@@ -93,7 +83,7 @@ impl AppState {
let partial = Templates::get(path.as_ref()).unwrap();
self.handlebars
.write()
- .register_partial(partial_name, std::str::from_utf8(partial.as_ref()).unwrap())
+ .register_partial(partial_name, std::str::from_utf8(&partial.data).unwrap())
.unwrap()
}
}
@@ -108,7 +98,7 @@ impl AppState {
self.register_partials();
self.handlebars.read().render_template(
- std::str::from_utf8(Templates::get(path).unwrap().as_ref()).unwrap(),
+ std::str::from_utf8(&Templates::get(path).unwrap().data).unwrap(),
data,
)
}
@@ -117,7 +107,7 @@ impl AppState {
#[async_trait]
trait RequestExt {
async fn layout_data(&mut self) -> tide::Result<Arc<LayoutData>>;
- fn db(&self) -> &DbPool;
+ fn db(&self) -> &Db;
}
#[async_trait]
@@ -131,7 +121,7 @@ impl RequestExt for Request {
Ok(self.ext::<Arc<LayoutData>>().unwrap().clone())
}
- fn db(&self) -> &DbPool {
+ fn db(&self) -> &Db {
&self.state().db
}
}
@@ -173,7 +163,7 @@ pub async fn run_server(
web.with(CompressMiddleware::new());
web.with(
SessionMiddleware::new(
- PostgresSessionStore::new_with_table_name(&state.config.database_url, "sessions")
+ db::SessionStore::new_with_table_name(&state.config.database_url, "sessions")
.await
.unwrap(),
state.config.session_secret.as_bytes(),
@@ -1,15 +1,20 @@
-use crate::auth::{self, UserId};
-
-use super::{auth::PeerExt as _, AppState};
+use super::{
+ auth,
+ db::{ChannelId, MessageId, UserId},
+ AppState,
+};
use anyhow::anyhow;
-use async_std::task;
+use async_std::{sync::RwLock, task};
use async_tungstenite::{
tungstenite::{protocol::Role, Error as WebSocketError, Message as WebSocketMessage},
WebSocketStream,
};
+use futures::{future::BoxFuture, FutureExt};
+use postage::{mpsc, prelude::Sink as _, prelude::Stream as _};
use sha1::{Digest as _, Sha1};
use std::{
- collections::{HashMap, HashSet},
+ any::TypeId,
+ collections::{hash_map, HashMap, HashSet},
future::Future,
mem,
sync::Arc,
@@ -21,27 +26,44 @@ use tide::{
http::headers::{HeaderName, CONNECTION, UPGRADE},
Request, Response,
};
+use time::OffsetDateTime;
use zrpc::{
auth::random_token,
- proto::{self, EnvelopedMessage},
- ConnectionId, Peer, Router, TypedEnvelope,
+ proto::{self, AnyTypedEnvelope, EnvelopedMessage},
+ ConnectionId, Peer, TypedEnvelope,
};
type ReplicaId = u16;
+type MessageHandler = Box<
+ dyn Send
+ + Sync
+ + Fn(Arc<Server>, Box<dyn AnyTypedEnvelope>) -> BoxFuture<'static, tide::Result<()>>,
+>;
+
+pub struct Server {
+ peer: Arc<Peer>,
+ state: RwLock<ServerState>,
+ app_state: Arc<AppState>,
+ handlers: HashMap<TypeId, MessageHandler>,
+ notifications: Option<mpsc::Sender<()>>,
+}
+
#[derive(Default)]
-pub struct State {
- connections: HashMap<ConnectionId, ConnectionState>,
- pub worktrees: HashMap<u64, WorktreeState>,
+struct ServerState {
+ connections: HashMap<ConnectionId, Connection>,
+ pub worktrees: HashMap<u64, Worktree>,
+ channels: HashMap<ChannelId, Channel>,
next_worktree_id: u64,
}
-struct ConnectionState {
- _user_id: i32,
+struct Connection {
+ user_id: UserId,
worktrees: HashSet<u64>,
+ channels: HashSet<ChannelId>,
}
-pub struct WorktreeState {
+struct Worktree {
host_connection_id: Option<ConnectionId>,
guest_connection_ids: HashMap<ConnectionId, ReplicaId>,
active_replica_ids: HashSet<ReplicaId>,
@@ -50,40 +72,174 @@ pub struct WorktreeState {
entries: HashMap<u64, proto::Entry>,
}
-impl WorktreeState {
- pub fn connection_ids(&self) -> Vec<ConnectionId> {
- self.guest_connection_ids
- .keys()
- .copied()
- .chain(self.host_connection_id)
- .collect()
+#[derive(Default)]
+struct Channel {
+ connection_ids: HashSet<ConnectionId>,
+}
+
+const MESSAGE_COUNT_PER_PAGE: usize = 100;
+const MAX_MESSAGE_LEN: usize = 1024;
+
+impl Server {
+ pub fn new(
+ app_state: Arc<AppState>,
+ peer: Arc<Peer>,
+ notifications: Option<mpsc::Sender<()>>,
+ ) -> Arc<Self> {
+ let mut server = Self {
+ peer,
+ app_state,
+ state: Default::default(),
+ handlers: Default::default(),
+ notifications,
+ };
+
+ server
+ .add_handler(Server::share_worktree)
+ .add_handler(Server::join_worktree)
+ .add_handler(Server::update_worktree)
+ .add_handler(Server::close_worktree)
+ .add_handler(Server::open_buffer)
+ .add_handler(Server::close_buffer)
+ .add_handler(Server::update_buffer)
+ .add_handler(Server::buffer_saved)
+ .add_handler(Server::save_buffer)
+ .add_handler(Server::get_channels)
+ .add_handler(Server::get_users)
+ .add_handler(Server::join_channel)
+ .add_handler(Server::leave_channel)
+ .add_handler(Server::send_channel_message)
+ .add_handler(Server::get_channel_messages);
+
+ Arc::new(server)
}
- fn host_connection_id(&self) -> tide::Result<ConnectionId> {
- Ok(self
- .host_connection_id
- .ok_or_else(|| anyhow!("host disconnected from worktree"))?)
+ fn add_handler<F, Fut, M>(&mut self, handler: F) -> &mut Self
+ where
+ F: 'static + Send + Sync + Fn(Arc<Self>, TypedEnvelope<M>) -> Fut,
+ Fut: 'static + Send + Future<Output = tide::Result<()>>,
+ M: EnvelopedMessage,
+ {
+ let prev_handler = self.handlers.insert(
+ TypeId::of::<M>(),
+ Box::new(move |server, envelope| {
+ let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
+ (handler)(server, *envelope).boxed()
+ }),
+ );
+ if prev_handler.is_some() {
+ panic!("registered a handler for the same message twice");
+ }
+ self
+ }
+
+ pub fn handle_connection<Conn>(
+ self: &Arc<Self>,
+ connection: Conn,
+ addr: String,
+ user_id: UserId,
+ ) -> impl Future<Output = ()>
+ where
+ Conn: 'static
+ + futures::Sink<WebSocketMessage, Error = WebSocketError>
+ + futures::Stream<Item = Result<WebSocketMessage, WebSocketError>>
+ + Send
+ + Unpin,
+ {
+ let this = self.clone();
+ async move {
+ let (connection_id, handle_io, mut incoming_rx) =
+ this.peer.add_connection(connection).await;
+ this.add_connection(connection_id, user_id).await;
+
+ let handle_io = handle_io.fuse();
+ futures::pin_mut!(handle_io);
+ loop {
+ let next_message = incoming_rx.recv().fuse();
+ futures::pin_mut!(next_message);
+ futures::select_biased! {
+ message = next_message => {
+ if let Some(message) = message {
+ let start_time = Instant::now();
+ log::info!("RPC message received: {}", message.payload_type_name());
+ if let Some(handler) = this.handlers.get(&message.payload_type_id()) {
+ if let Err(err) = (handler)(this.clone(), message).await {
+ log::error!("error handling message: {:?}", err);
+ } else {
+ log::info!("RPC message handled. duration:{:?}", start_time.elapsed());
+ }
+
+ if let Some(mut notifications) = this.notifications.clone() {
+ let _ = notifications.send(()).await;
+ }
+ } else {
+ log::warn!("unhandled message: {}", message.payload_type_name());
+ }
+ } else {
+ log::info!("rpc connection closed {:?}", addr);
+ break;
+ }
+ }
+ handle_io = handle_io => {
+ if let Err(err) = handle_io {
+ log::error!("error handling rpc connection {:?} - {:?}", addr, err);
+ }
+ break;
+ }
+ }
+ }
+
+ if let Err(err) = this.sign_out(connection_id).await {
+ log::error!("error signing out connection {:?} - {:?}", addr, err);
+ }
+ }
+ }
+
+ async fn sign_out(self: &Arc<Self>, connection_id: zrpc::ConnectionId) -> tide::Result<()> {
+ self.peer.disconnect(connection_id).await;
+ let worktree_ids = self.remove_connection(connection_id).await;
+ for worktree_id in worktree_ids {
+ let state = self.state.read().await;
+ if let Some(worktree) = state.worktrees.get(&worktree_id) {
+ broadcast(connection_id, worktree.connection_ids(), |conn_id| {
+ self.peer.send(
+ conn_id,
+ proto::RemovePeer {
+ worktree_id,
+ peer_id: connection_id.0,
+ },
+ )
+ })
+ .await?;
+ }
+ }
+ Ok(())
}
-}
-impl State {
// Add a new connection associated with a given user.
- pub fn add_connection(&mut self, connection_id: ConnectionId, _user_id: i32) {
- self.connections.insert(
+ async fn add_connection(&self, connection_id: ConnectionId, user_id: UserId) {
+ self.state.write().await.connections.insert(
connection_id,
- ConnectionState {
- _user_id,
+ Connection {
+ user_id,
worktrees: Default::default(),
+ channels: Default::default(),
},
);
}
// Remove the given connection and its association with any worktrees.
- pub fn remove_connection(&mut self, connection_id: ConnectionId) -> Vec<u64> {
+ async fn remove_connection(&self, connection_id: ConnectionId) -> Vec<u64> {
let mut worktree_ids = Vec::new();
- if let Some(connection_state) = self.connections.remove(&connection_id) {
- for worktree_id in connection_state.worktrees {
- if let Some(worktree) = self.worktrees.get_mut(&worktree_id) {
+ let mut state = self.state.write().await;
+ if let Some(connection) = state.connections.remove(&connection_id) {
+ for channel_id in connection.channels {
+ if let Some(channel) = state.channels.get_mut(&channel_id) {
+ channel.connection_ids.remove(&connection_id);
+ }
+ }
+ for worktree_id in connection.worktrees {
+ if let Some(worktree) = state.worktrees.get_mut(&worktree_id) {
if worktree.host_connection_id == Some(connection_id) {
worktree_ids.push(worktree_id);
} else if let Some(replica_id) =
@@ -98,28 +254,615 @@ impl State {
worktree_ids
}
+ async fn share_worktree(
+ self: Arc<Server>,
+ mut request: TypedEnvelope<proto::ShareWorktree>,
+ ) -> tide::Result<()> {
+ let mut state = self.state.write().await;
+ let worktree_id = state.next_worktree_id;
+ state.next_worktree_id += 1;
+ let access_token = random_token();
+ let worktree = request
+ .payload
+ .worktree
+ .as_mut()
+ .ok_or_else(|| anyhow!("missing worktree"))?;
+ let entries = mem::take(&mut worktree.entries)
+ .into_iter()
+ .map(|entry| (entry.id, entry))
+ .collect();
+ state.worktrees.insert(
+ worktree_id,
+ Worktree {
+ host_connection_id: Some(request.sender_id),
+ guest_connection_ids: Default::default(),
+ active_replica_ids: Default::default(),
+ access_token: access_token.clone(),
+ root_name: mem::take(&mut worktree.root_name),
+ entries,
+ },
+ );
+
+ self.peer
+ .respond(
+ request.receipt(),
+ proto::ShareWorktreeResponse {
+ worktree_id,
+ access_token,
+ },
+ )
+ .await?;
+ Ok(())
+ }
+
+ async fn join_worktree(
+ self: Arc<Server>,
+ request: TypedEnvelope<proto::OpenWorktree>,
+ ) -> tide::Result<()> {
+ let worktree_id = request.payload.worktree_id;
+ let access_token = &request.payload.access_token;
+
+ let mut state = self.state.write().await;
+ if let Some((peer_replica_id, worktree)) =
+ state.join_worktree(request.sender_id, worktree_id, access_token)
+ {
+ let mut peers = Vec::new();
+ if let Some(host_connection_id) = worktree.host_connection_id {
+ peers.push(proto::Peer {
+ peer_id: host_connection_id.0,
+ replica_id: 0,
+ });
+ }
+ for (peer_conn_id, peer_replica_id) in &worktree.guest_connection_ids {
+ if *peer_conn_id != request.sender_id {
+ peers.push(proto::Peer {
+ peer_id: peer_conn_id.0,
+ replica_id: *peer_replica_id as u32,
+ });
+ }
+ }
+
+ broadcast(request.sender_id, worktree.connection_ids(), |conn_id| {
+ self.peer.send(
+ conn_id,
+ proto::AddPeer {
+ worktree_id,
+ peer: Some(proto::Peer {
+ peer_id: request.sender_id.0,
+ replica_id: peer_replica_id as u32,
+ }),
+ },
+ )
+ })
+ .await?;
+ self.peer
+ .respond(
+ request.receipt(),
+ proto::OpenWorktreeResponse {
+ worktree_id,
+ worktree: Some(proto::Worktree {
+ root_name: worktree.root_name.clone(),
+ entries: worktree.entries.values().cloned().collect(),
+ }),
+ replica_id: peer_replica_id as u32,
+ peers,
+ },
+ )
+ .await?;
+ } else {
+ self.peer
+ .respond(
+ request.receipt(),
+ proto::OpenWorktreeResponse {
+ worktree_id,
+ worktree: None,
+ replica_id: 0,
+ peers: Vec::new(),
+ },
+ )
+ .await?;
+ }
+
+ Ok(())
+ }
+
+ async fn update_worktree(
+ self: Arc<Server>,
+ request: TypedEnvelope<proto::UpdateWorktree>,
+ ) -> tide::Result<()> {
+ {
+ let mut state = self.state.write().await;
+ let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?;
+ for entry_id in &request.payload.removed_entries {
+ worktree.entries.remove(&entry_id);
+ }
+
+ for entry in &request.payload.updated_entries {
+ worktree.entries.insert(entry.id, entry.clone());
+ }
+ }
+
+ self.broadcast_in_worktree(request.payload.worktree_id, &request)
+ .await?;
+ Ok(())
+ }
+
+ async fn close_worktree(
+ self: Arc<Server>,
+ request: TypedEnvelope<proto::CloseWorktree>,
+ ) -> tide::Result<()> {
+ let connection_ids;
+ {
+ let mut state = self.state.write().await;
+ let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?;
+ connection_ids = worktree.connection_ids();
+ if worktree.host_connection_id == Some(request.sender_id) {
+ worktree.host_connection_id = None;
+ } else if let Some(replica_id) =
+ worktree.guest_connection_ids.remove(&request.sender_id)
+ {
+ worktree.active_replica_ids.remove(&replica_id);
+ }
+ }
+
+ broadcast(request.sender_id, connection_ids, |conn_id| {
+ self.peer.send(
+ conn_id,
+ proto::RemovePeer {
+ worktree_id: request.payload.worktree_id,
+ peer_id: request.sender_id.0,
+ },
+ )
+ })
+ .await?;
+
+ Ok(())
+ }
+
+ async fn open_buffer(
+ self: Arc<Server>,
+ request: TypedEnvelope<proto::OpenBuffer>,
+ ) -> tide::Result<()> {
+ let receipt = request.receipt();
+ let worktree_id = request.payload.worktree_id;
+ let host_connection_id = self
+ .state
+ .read()
+ .await
+ .read_worktree(worktree_id, request.sender_id)?
+ .host_connection_id()?;
+
+ let response = self
+ .peer
+ .forward_request(request.sender_id, host_connection_id, request.payload)
+ .await?;
+ self.peer.respond(receipt, response).await?;
+ Ok(())
+ }
+
+ async fn close_buffer(
+ self: Arc<Server>,
+ request: TypedEnvelope<proto::CloseBuffer>,
+ ) -> tide::Result<()> {
+ let host_connection_id = self
+ .state
+ .read()
+ .await
+ .read_worktree(request.payload.worktree_id, request.sender_id)?
+ .host_connection_id()?;
+
+ self.peer
+ .forward_send(request.sender_id, host_connection_id, request.payload)
+ .await?;
+
+ Ok(())
+ }
+
+ async fn save_buffer(
+ self: Arc<Server>,
+ request: TypedEnvelope<proto::SaveBuffer>,
+ ) -> tide::Result<()> {
+ let host;
+ let guests;
+ {
+ let state = self.state.read().await;
+ let worktree = state.read_worktree(request.payload.worktree_id, request.sender_id)?;
+ host = worktree.host_connection_id()?;
+ guests = worktree
+ .guest_connection_ids
+ .keys()
+ .copied()
+ .collect::<Vec<_>>();
+ }
+
+ let sender = request.sender_id;
+ let receipt = request.receipt();
+ let response = self
+ .peer
+ .forward_request(sender, host, request.payload.clone())
+ .await?;
+
+ broadcast(host, guests, |conn_id| {
+ let response = response.clone();
+ let peer = &self.peer;
+ async move {
+ if conn_id == sender {
+ peer.respond(receipt, response).await
+ } else {
+ peer.forward_send(host, conn_id, response).await
+ }
+ }
+ })
+ .await?;
+
+ Ok(())
+ }
+
+ async fn update_buffer(
+ self: Arc<Server>,
+ request: TypedEnvelope<proto::UpdateBuffer>,
+ ) -> tide::Result<()> {
+ self.broadcast_in_worktree(request.payload.worktree_id, &request)
+ .await
+ }
+
+ async fn buffer_saved(
+ self: Arc<Server>,
+ request: TypedEnvelope<proto::BufferSaved>,
+ ) -> tide::Result<()> {
+ self.broadcast_in_worktree(request.payload.worktree_id, &request)
+ .await
+ }
+
+ async fn get_channels(
+ self: Arc<Server>,
+ request: TypedEnvelope<proto::GetChannels>,
+ ) -> tide::Result<()> {
+ let user_id = self
+ .state
+ .read()
+ .await
+ .user_id_for_connection(request.sender_id)?;
+ let channels = self.app_state.db.get_accessible_channels(user_id).await?;
+ self.peer
+ .respond(
+ request.receipt(),
+ proto::GetChannelsResponse {
+ channels: channels
+ .into_iter()
+ .map(|chan| proto::Channel {
+ id: chan.id.to_proto(),
+ name: chan.name,
+ })
+ .collect(),
+ },
+ )
+ .await?;
+ Ok(())
+ }
+
+ async fn get_users(
+ self: Arc<Server>,
+ request: TypedEnvelope<proto::GetUsers>,
+ ) -> tide::Result<()> {
+ let user_id = self
+ .state
+ .read()
+ .await
+ .user_id_for_connection(request.sender_id)?;
+ let receipt = request.receipt();
+ let user_ids = request.payload.user_ids.into_iter().map(UserId::from_proto);
+ let users = self
+ .app_state
+ .db
+ .get_users_by_ids(user_id, user_ids)
+ .await?
+ .into_iter()
+ .map(|user| proto::User {
+ id: user.id.to_proto(),
+ github_login: user.github_login,
+ avatar_url: String::new(),
+ })
+ .collect();
+ self.peer
+ .respond(receipt, proto::GetUsersResponse { users })
+ .await?;
+ Ok(())
+ }
+
+ async fn join_channel(
+ self: Arc<Self>,
+ request: TypedEnvelope<proto::JoinChannel>,
+ ) -> tide::Result<()> {
+ let user_id = self
+ .state
+ .read()
+ .await
+ .user_id_for_connection(request.sender_id)?;
+ let channel_id = ChannelId::from_proto(request.payload.channel_id);
+ if !self
+ .app_state
+ .db
+ .can_user_access_channel(user_id, channel_id)
+ .await?
+ {
+ Err(anyhow!("access denied"))?;
+ }
+
+ self.state
+ .write()
+ .await
+ .join_channel(request.sender_id, channel_id);
+ let messages = self
+ .app_state
+ .db
+ .get_channel_messages(channel_id, MESSAGE_COUNT_PER_PAGE, None)
+ .await?
+ .into_iter()
+ .map(|msg| proto::ChannelMessage {
+ id: msg.id.to_proto(),
+ body: msg.body,
+ timestamp: msg.sent_at.unix_timestamp() as u64,
+ sender_id: msg.sender_id.to_proto(),
+ })
+ .collect::<Vec<_>>();
+ self.peer
+ .respond(
+ request.receipt(),
+ proto::JoinChannelResponse {
+ done: messages.len() < MESSAGE_COUNT_PER_PAGE,
+ messages,
+ },
+ )
+ .await?;
+ Ok(())
+ }
+
+ async fn leave_channel(
+ self: Arc<Self>,
+ request: TypedEnvelope<proto::LeaveChannel>,
+ ) -> tide::Result<()> {
+ let user_id = self
+ .state
+ .read()
+ .await
+ .user_id_for_connection(request.sender_id)?;
+ let channel_id = ChannelId::from_proto(request.payload.channel_id);
+ if !self
+ .app_state
+ .db
+ .can_user_access_channel(user_id, channel_id)
+ .await?
+ {
+ Err(anyhow!("access denied"))?;
+ }
+
+ self.state
+ .write()
+ .await
+ .leave_channel(request.sender_id, channel_id);
+
+ Ok(())
+ }
+
+ async fn send_channel_message(
+ self: Arc<Self>,
+ request: TypedEnvelope<proto::SendChannelMessage>,
+ ) -> tide::Result<()> {
+ let receipt = request.receipt();
+ let channel_id = ChannelId::from_proto(request.payload.channel_id);
+ let user_id;
+ let connection_ids;
+ {
+ let state = self.state.read().await;
+ user_id = state.user_id_for_connection(request.sender_id)?;
+ if let Some(channel) = state.channels.get(&channel_id) {
+ connection_ids = channel.connection_ids();
+ } else {
+ return Ok(());
+ }
+ }
+
+ // Validate the message body.
+ let body = request.payload.body.trim().to_string();
+ if body.len() > MAX_MESSAGE_LEN {
+ self.peer
+ .respond_with_error(
+ receipt,
+ proto::Error {
+ message: "message is too long".to_string(),
+ },
+ )
+ .await?;
+ return Ok(());
+ }
+ if body.is_empty() {
+ self.peer
+ .respond_with_error(
+ receipt,
+ proto::Error {
+ message: "message can't be blank".to_string(),
+ },
+ )
+ .await?;
+ return Ok(());
+ }
+
+ let timestamp = OffsetDateTime::now_utc();
+ let message_id = self
+ .app_state
+ .db
+ .create_channel_message(channel_id, user_id, &body, timestamp)
+ .await?
+ .to_proto();
+ let message = proto::ChannelMessage {
+ sender_id: user_id.to_proto(),
+ id: message_id,
+ body,
+ timestamp: timestamp.unix_timestamp() as u64,
+ };
+ broadcast(request.sender_id, connection_ids, |conn_id| {
+ self.peer.send(
+ conn_id,
+ proto::ChannelMessageSent {
+ channel_id: channel_id.to_proto(),
+ message: Some(message.clone()),
+ },
+ )
+ })
+ .await?;
+ self.peer
+ .respond(
+ receipt,
+ proto::SendChannelMessageResponse {
+ message: Some(message),
+ },
+ )
+ .await?;
+ Ok(())
+ }
+
+ async fn get_channel_messages(
+ self: Arc<Self>,
+ request: TypedEnvelope<proto::GetChannelMessages>,
+ ) -> tide::Result<()> {
+ let user_id = self
+ .state
+ .read()
+ .await
+ .user_id_for_connection(request.sender_id)?;
+ let channel_id = ChannelId::from_proto(request.payload.channel_id);
+ if !self
+ .app_state
+ .db
+ .can_user_access_channel(user_id, channel_id)
+ .await?
+ {
+ Err(anyhow!("access denied"))?;
+ }
+
+ let messages = self
+ .app_state
+ .db
+ .get_channel_messages(
+ channel_id,
+ MESSAGE_COUNT_PER_PAGE,
+ Some(MessageId::from_proto(request.payload.before_message_id)),
+ )
+ .await?
+ .into_iter()
+ .map(|msg| proto::ChannelMessage {
+ id: msg.id.to_proto(),
+ body: msg.body,
+ timestamp: msg.sent_at.unix_timestamp() as u64,
+ sender_id: msg.sender_id.to_proto(),
+ })
+ .collect::<Vec<_>>();
+ self.peer
+ .respond(
+ request.receipt(),
+ proto::GetChannelMessagesResponse {
+ done: messages.len() < MESSAGE_COUNT_PER_PAGE,
+ messages,
+ },
+ )
+ .await?;
+ Ok(())
+ }
+
+ async fn broadcast_in_worktree<T: proto::EnvelopedMessage>(
+ &self,
+ worktree_id: u64,
+ message: &TypedEnvelope<T>,
+ ) -> tide::Result<()> {
+ let connection_ids = self
+ .state
+ .read()
+ .await
+ .read_worktree(worktree_id, message.sender_id)?
+ .connection_ids();
+
+ broadcast(message.sender_id, connection_ids, |conn_id| {
+ self.peer
+ .forward_send(message.sender_id, conn_id, message.payload.clone())
+ })
+ .await?;
+
+ Ok(())
+ }
+}
+
+pub async fn broadcast<F, T>(
+ sender_id: ConnectionId,
+ receiver_ids: Vec<ConnectionId>,
+ mut f: F,
+) -> anyhow::Result<()>
+where
+ F: FnMut(ConnectionId) -> T,
+ T: Future<Output = anyhow::Result<()>>,
+{
+ let futures = receiver_ids
+ .into_iter()
+ .filter(|id| *id != sender_id)
+ .map(|id| f(id));
+ futures::future::try_join_all(futures).await?;
+ Ok(())
+}
+
+impl ServerState {
+ fn join_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) {
+ if let Some(connection) = self.connections.get_mut(&connection_id) {
+ connection.channels.insert(channel_id);
+ self.channels
+ .entry(channel_id)
+ .or_default()
+ .connection_ids
+ .insert(connection_id);
+ }
+ }
+
+ fn leave_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) {
+ if let Some(connection) = self.connections.get_mut(&connection_id) {
+ connection.channels.remove(&channel_id);
+ if let hash_map::Entry::Occupied(mut entry) = self.channels.entry(channel_id) {
+ entry.get_mut().connection_ids.remove(&connection_id);
+ if entry.get_mut().connection_ids.is_empty() {
+ entry.remove();
+ }
+ }
+ }
+ }
+
+ fn user_id_for_connection(&self, connection_id: ConnectionId) -> tide::Result<UserId> {
+ Ok(self
+ .connections
+ .get(&connection_id)
+ .ok_or_else(|| anyhow!("unknown connection"))?
+ .user_id)
+ }
+
// Add the given connection as a guest of the given worktree
- pub fn join_worktree(
+ fn join_worktree(
&mut self,
connection_id: ConnectionId,
worktree_id: u64,
access_token: &str,
- ) -> Option<(ReplicaId, &WorktreeState)> {
- if let Some(worktree_state) = self.worktrees.get_mut(&worktree_id) {
- if access_token == worktree_state.access_token {
- if let Some(connection_state) = self.connections.get_mut(&connection_id) {
- connection_state.worktrees.insert(worktree_id);
+ ) -> Option<(ReplicaId, &Worktree)> {
+ if let Some(worktree) = self.worktrees.get_mut(&worktree_id) {
+ if access_token == worktree.access_token {
+ if let Some(connection) = self.connections.get_mut(&connection_id) {
+ connection.worktrees.insert(worktree_id);
}
let mut replica_id = 1;
- while worktree_state.active_replica_ids.contains(&replica_id) {
+ while worktree.active_replica_ids.contains(&replica_id) {
replica_id += 1;
}
- worktree_state.active_replica_ids.insert(replica_id);
- worktree_state
+ worktree.active_replica_ids.insert(replica_id);
+ worktree
.guest_connection_ids
.insert(connection_id, replica_id);
- Some((replica_id, worktree_state))
+ Some((replica_id, worktree))
} else {
None
}
@@ -132,7 +875,7 @@ impl State {
&self,
worktree_id: u64,
connection_id: ConnectionId,
- ) -> tide::Result<&WorktreeState> {
+ ) -> tide::Result<&Worktree> {
let worktree = self
.worktrees
.get(&worktree_id)
@@ -155,7 +898,7 @@ impl State {
&mut self,
worktree_id: u64,
connection_id: ConnectionId,
- ) -> tide::Result<&mut WorktreeState> {
+ ) -> tide::Result<&mut Worktree> {
let worktree = self
.worktrees
.get_mut(&worktree_id)
@@ -175,95 +918,33 @@ impl State {
}
}
-trait MessageHandler<'a, M: proto::EnvelopedMessage> {
- type Output: 'a + Send + Future<Output = tide::Result<()>>;
-
- fn handle(
- &self,
- message: TypedEnvelope<M>,
- rpc: &'a Arc<Peer>,
- app_state: &'a Arc<AppState>,
- ) -> Self::Output;
-}
-
-impl<'a, M, F, Fut> MessageHandler<'a, M> for F
-where
- M: proto::EnvelopedMessage,
- F: Fn(TypedEnvelope<M>, &'a Arc<Peer>, &'a Arc<AppState>) -> Fut,
- Fut: 'a + Send + Future<Output = tide::Result<()>>,
-{
- type Output = Fut;
-
- fn handle(
- &self,
- message: TypedEnvelope<M>,
- rpc: &'a Arc<Peer>,
- app_state: &'a Arc<AppState>,
- ) -> Self::Output {
- (self)(message, rpc, app_state)
+impl Worktree {
+ pub fn connection_ids(&self) -> Vec<ConnectionId> {
+ self.guest_connection_ids
+ .keys()
+ .copied()
+ .chain(self.host_connection_id)
+ .collect()
}
-}
-
-fn on_message<M, H>(router: &mut Router, rpc: &Arc<Peer>, app_state: &Arc<AppState>, handler: H)
-where
- M: EnvelopedMessage,
- H: 'static + Clone + Send + Sync + for<'a> MessageHandler<'a, M>,
-{
- let rpc = rpc.clone();
- let handler = handler.clone();
- let app_state = app_state.clone();
- router.add_message_handler(move |message| {
- let rpc = rpc.clone();
- let handler = handler.clone();
- let app_state = app_state.clone();
- async move {
- let sender_id = message.sender_id;
- let message_id = message.message_id;
- let start_time = Instant::now();
- log::info!(
- "RPC message received. id: {}.{}, type:{}",
- sender_id,
- message_id,
- M::NAME
- );
- if let Err(err) = handler.handle(message, &rpc, &app_state).await {
- log::error!("error handling message: {:?}", err);
- } else {
- log::info!(
- "RPC message handled. id:{}.{}, duration:{:?}",
- sender_id,
- message_id,
- start_time.elapsed()
- );
- }
- Ok(())
- }
- });
+ fn host_connection_id(&self) -> tide::Result<ConnectionId> {
+ Ok(self
+ .host_connection_id
+ .ok_or_else(|| anyhow!("host disconnected from worktree"))?)
+ }
}
-pub fn add_rpc_routes(router: &mut Router, state: &Arc<AppState>, rpc: &Arc<Peer>) {
- on_message(router, rpc, state, share_worktree);
- on_message(router, rpc, state, join_worktree);
- on_message(router, rpc, state, update_worktree);
- on_message(router, rpc, state, close_worktree);
- on_message(router, rpc, state, open_buffer);
- on_message(router, rpc, state, close_buffer);
- on_message(router, rpc, state, update_buffer);
- on_message(router, rpc, state, buffer_saved);
- on_message(router, rpc, state, save_buffer);
+impl Channel {
+ fn connection_ids(&self) -> Vec<ConnectionId> {
+ self.connection_ids.iter().copied().collect()
+ }
}
pub fn add_routes(app: &mut tide::Server<Arc<AppState>>, rpc: &Arc<Peer>) {
- let mut router = Router::new();
- add_rpc_routes(&mut router, app.state(), rpc);
- let router = Arc::new(router);
-
- let rpc = rpc.clone();
+ let server = Server::new(app.state().clone(), rpc.clone(), None);
app.at("/rpc").with(auth::VerifyToken).get(move |request: Request<Arc<AppState>>| {
let user_id = request.ext::<UserId>().copied();
- let rpc = rpc.clone();
- let router = router.clone();
+ let server = server.clone();
async move {
const WEBSOCKET_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
@@ -1,613 +0,0 @@
-use crate::{
- admin, auth, github,
- rpc::{self, add_rpc_routes},
- AppState, Config,
-};
-use async_std::task;
-use gpui::TestAppContext;
-use rand::prelude::*;
-use serde_json::json;
-use sqlx::{
- migrate::{MigrateDatabase, Migrator},
- postgres::PgPoolOptions,
- Executor as _, Postgres,
-};
-use std::{path::Path, sync::Arc};
-use zed::{
- editor::Editor,
- fs::{FakeFs, Fs as _},
- language::LanguageRegistry,
- rpc::Client,
- settings,
- test::Channel,
- worktree::Worktree,
-};
-use zrpc::{ForegroundRouter, Peer, Router};
-
-#[gpui::test]
-async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
- let (window_b, _) = cx_b.add_window(|_| EmptyView);
- let settings = settings::channel(&cx_b.font_cache()).unwrap().1;
- let lang_registry = Arc::new(LanguageRegistry::new());
-
- // 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;
-
- cx_a.foreground().forbid_parking();
-
- // Share a local worktree as client A
- let fs = Arc::new(FakeFs::new());
- fs.insert_tree(
- "/a",
- json!({
- "a.txt": "a-contents",
- "b.txt": "b-contents",
- }),
- )
- .await;
- let worktree_a = Worktree::open_local(
- "/a".as_ref(),
- lang_registry.clone(),
- fs,
- &mut cx_a.to_async(),
- )
- .await
- .unwrap();
- worktree_a
- .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
- .await;
- let (worktree_id, worktree_token) = worktree_a
- .update(&mut cx_a, |tree, cx| {
- tree.as_local_mut().unwrap().share(client_a.clone(), cx)
- })
- .await
- .unwrap();
-
- // Join that worktree as client B, and see that a guest has joined as client A.
- let worktree_b = Worktree::open_remote(
- client_b.clone(),
- worktree_id,
- worktree_token,
- lang_registry.clone(),
- &mut cx_b.to_async(),
- )
- .await
- .unwrap();
- let replica_id_b = worktree_b.read_with(&cx_b, |tree, _| tree.replica_id());
- worktree_a
- .condition(&cx_a, |tree, _| {
- tree.peers()
- .values()
- .any(|replica_id| *replica_id == replica_id_b)
- })
- .await;
-
- // Open the same file as client B and client A.
- let buffer_b = worktree_b
- .update(&mut cx_b, |worktree, cx| worktree.open_buffer("b.txt", cx))
- .await
- .unwrap();
- buffer_b.read_with(&cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
- worktree_a.read_with(&cx_a, |tree, cx| assert!(tree.has_open_buffer("b.txt", cx)));
- let buffer_a = worktree_a
- .update(&mut cx_a, |tree, cx| tree.open_buffer("b.txt", cx))
- .await
- .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));
- buffer_a
- .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
- .await;
-
- // Edit the buffer as client B and see that edit as client A.
- editor_b.update(&mut cx_b, |editor, cx| {
- editor.insert(&"ok, ".to_string(), cx)
- });
- buffer_a
- .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents")
- .await;
-
- // Remove the selection set as client B, see those selections disappear as client A.
- cx_b.update(move |_| drop(editor_b));
- buffer_a
- .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0)
- .await;
-
- // Close the buffer as client A, see that the buffer is closed.
- drop(buffer_a);
- worktree_a
- .condition(&cx_a, |tree, cx| !tree.has_open_buffer("b.txt", cx))
- .await;
-
- // Dropping the worktree removes client B from client A's peers.
- cx_b.update(move |_| drop(worktree_b));
- worktree_a
- .condition(&cx_a, |tree, _| tree.peers().is_empty())
- .await;
-}
-
-#[gpui::test]
-async fn test_propagate_saves_and_fs_changes_in_shared_worktree(
- mut cx_a: TestAppContext,
- mut cx_b: TestAppContext,
- mut cx_c: TestAppContext,
-) {
- let lang_registry = Arc::new(LanguageRegistry::new());
-
- // 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;
-
- cx_a.foreground().forbid_parking();
-
- let fs = Arc::new(FakeFs::new());
-
- // Share a worktree as client A.
- fs.insert_tree(
- "/a",
- json!({
- "file1": "",
- "file2": ""
- }),
- )
- .await;
-
- let worktree_a = Worktree::open_local(
- "/a".as_ref(),
- lang_registry.clone(),
- fs.clone(),
- &mut cx_a.to_async(),
- )
- .await
- .unwrap();
- worktree_a
- .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
- .await;
- let (worktree_id, worktree_token) = worktree_a
- .update(&mut cx_a, |tree, cx| {
- tree.as_local_mut().unwrap().share(client_a.clone(), cx)
- })
- .await
- .unwrap();
-
- // Join that worktree as clients B and C.
- let worktree_b = Worktree::open_remote(
- client_b.clone(),
- worktree_id,
- worktree_token.clone(),
- lang_registry.clone(),
- &mut cx_b.to_async(),
- )
- .await
- .unwrap();
- let worktree_c = Worktree::open_remote(
- client_c.clone(),
- worktree_id,
- worktree_token,
- lang_registry.clone(),
- &mut cx_c.to_async(),
- )
- .await
- .unwrap();
-
- // Open and edit a buffer as both guests B and C.
- let buffer_b = worktree_b
- .update(&mut cx_b, |tree, cx| tree.open_buffer("file1", cx))
- .await
- .unwrap();
- let buffer_c = worktree_c
- .update(&mut cx_c, |tree, cx| tree.open_buffer("file1", cx))
- .await
- .unwrap();
- buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx));
- buffer_c.update(&mut cx_c, |buf, cx| buf.edit([0..0], "i-am-c, ", cx));
-
- // Open and edit that buffer as the host.
- let buffer_a = worktree_a
- .update(&mut cx_a, |tree, cx| tree.open_buffer("file1", cx))
- .await
- .unwrap();
-
- buffer_a
- .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ")
- .await;
- buffer_a.update(&mut cx_a, |buf, cx| {
- buf.edit([buf.len()..buf.len()], "i-am-a", cx)
- });
-
- // Wait for edits to propagate
- buffer_a
- .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
- .await;
- buffer_b
- .condition(&mut cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
- .await;
- buffer_c
- .condition(&mut cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
- .await;
-
- // Edit the buffer as the host and concurrently save as guest B.
- let save_b = buffer_b.update(&mut cx_b, |buf, cx| buf.save(cx).unwrap());
- buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "hi-a, ", cx));
- save_b.await.unwrap();
- assert_eq!(
- fs.load("/a/file1".as_ref()).await.unwrap(),
- "hi-a, i-am-c, i-am-b, i-am-a"
- );
- buffer_a.read_with(&cx_a, |buf, _| assert!(!buf.is_dirty()));
- buffer_b.read_with(&cx_b, |buf, _| assert!(!buf.is_dirty()));
- buffer_c.condition(&cx_c, |buf, _| !buf.is_dirty()).await;
-
- // Make changes on host's file system, see those changes on the guests.
- fs.rename("/a/file2".as_ref(), "/a/file3".as_ref())
- .await
- .unwrap();
- fs.insert_file(Path::new("/a/file4"), "4".into())
- .await
- .unwrap();
-
- worktree_b
- .condition(&cx_b, |tree, _| tree.file_count() == 3)
- .await;
- worktree_c
- .condition(&cx_c, |tree, _| tree.file_count() == 3)
- .await;
- worktree_b.read_with(&cx_b, |tree, _| {
- assert_eq!(
- tree.paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
- &["file1", "file3", "file4"]
- )
- });
- worktree_c.read_with(&cx_c, |tree, _| {
- assert_eq!(
- tree.paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
- &["file1", "file3", "file4"]
- )
- });
-}
-
-#[gpui::test]
-async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
- let lang_registry = Arc::new(LanguageRegistry::new());
-
- // 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;
-
- cx_a.foreground().forbid_parking();
-
- // Share a local worktree as client A
- let fs = Arc::new(FakeFs::new());
- fs.save(Path::new("/a.txt"), &"a-contents".into())
- .await
- .unwrap();
- let worktree_a = Worktree::open_local(
- "/".as_ref(),
- lang_registry.clone(),
- fs,
- &mut cx_a.to_async(),
- )
- .await
- .unwrap();
- worktree_a
- .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
- .await;
- let (worktree_id, worktree_token) = worktree_a
- .update(&mut cx_a, |tree, cx| {
- tree.as_local_mut().unwrap().share(client_a.clone(), cx)
- })
- .await
- .unwrap();
-
- // Join that worktree as client B, and see that a guest has joined as client A.
- let worktree_b = Worktree::open_remote(
- client_b.clone(),
- worktree_id,
- worktree_token,
- lang_registry.clone(),
- &mut cx_b.to_async(),
- )
- .await
- .unwrap();
-
- let buffer_b = worktree_b
- .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))
- .await
- .unwrap();
- let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime);
-
- buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "world ", cx));
- buffer_b.read_with(&cx_b, |buf, _| {
- assert!(buf.is_dirty());
- assert!(!buf.has_conflict());
- });
-
- buffer_b
- .update(&mut cx_b, |buf, cx| buf.save(cx))
- .unwrap()
- .await
- .unwrap();
- worktree_b
- .condition(&cx_b, |_, cx| {
- buffer_b.read(cx).file().unwrap().mtime != mtime
- })
- .await;
- buffer_b.read_with(&cx_b, |buf, _| {
- assert!(!buf.is_dirty());
- assert!(!buf.has_conflict());
- });
-
- buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "hello ", cx));
- buffer_b.read_with(&cx_b, |buf, _| {
- assert!(buf.is_dirty());
- assert!(!buf.has_conflict());
- });
-}
-
-#[gpui::test]
-async fn test_editing_while_guest_opens_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
- let lang_registry = Arc::new(LanguageRegistry::new());
-
- // 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;
-
- cx_a.foreground().forbid_parking();
-
- // Share a local worktree as client A
- let fs = Arc::new(FakeFs::new());
- fs.save(Path::new("/a.txt"), &"a-contents".into())
- .await
- .unwrap();
- let worktree_a = Worktree::open_local(
- "/".as_ref(),
- lang_registry.clone(),
- fs,
- &mut cx_a.to_async(),
- )
- .await
- .unwrap();
- worktree_a
- .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
- .await;
- let (worktree_id, worktree_token) = worktree_a
- .update(&mut cx_a, |tree, cx| {
- tree.as_local_mut().unwrap().share(client_a.clone(), cx)
- })
- .await
- .unwrap();
-
- // Join that worktree as client B, and see that a guest has joined as client A.
- let worktree_b = Worktree::open_remote(
- client_b.clone(),
- worktree_id,
- worktree_token,
- lang_registry.clone(),
- &mut cx_b.to_async(),
- )
- .await
- .unwrap();
-
- let buffer_a = worktree_a
- .update(&mut cx_a, |tree, cx| tree.open_buffer("a.txt", cx))
- .await
- .unwrap();
- let buffer_b = cx_b
- .background()
- .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx)));
-
- task::yield_now().await;
- buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "z", cx));
-
- let text = buffer_a.read_with(&cx_a, |buf, _| buf.text());
- let buffer_b = buffer_b.await.unwrap();
- buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await;
-}
-
-#[gpui::test]
-async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) {
- let lang_registry = Arc::new(LanguageRegistry::new());
-
- // 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;
-
- cx_a.foreground().forbid_parking();
-
- // Share a local worktree as client A
- let fs = Arc::new(FakeFs::new());
- fs.insert_tree(
- "/a",
- json!({
- "a.txt": "a-contents",
- "b.txt": "b-contents",
- }),
- )
- .await;
- let worktree_a = Worktree::open_local(
- "/a".as_ref(),
- lang_registry.clone(),
- fs,
- &mut cx_a.to_async(),
- )
- .await
- .unwrap();
- worktree_a
- .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
- .await;
- let (worktree_id, worktree_token) = worktree_a
- .update(&mut cx_a, |tree, cx| {
- tree.as_local_mut().unwrap().share(client_a.clone(), cx)
- })
- .await
- .unwrap();
-
- // Join that worktree as client B, and see that a guest has joined as client A.
- let _worktree_b = Worktree::open_remote(
- client_b.clone(),
- worktree_id,
- worktree_token,
- lang_registry.clone(),
- &mut cx_b.to_async(),
- )
- .await
- .unwrap();
- worktree_a
- .condition(&cx_a, |tree, _| tree.peers().len() == 1)
- .await;
-
- // Drop client B's connection and ensure client A observes client B leaving the worktree.
- client_b.disconnect().await.unwrap();
- worktree_a
- .condition(&cx_a, |tree, _| tree.peers().len() == 0)
- .await;
-}
-
-struct TestServer {
- peer: Arc<Peer>,
- app_state: Arc<AppState>,
- db_name: String,
- router: Arc<Router>,
-}
-
-impl TestServer {
- async fn start() -> Self {
- let mut rng = StdRng::from_entropy();
- let db_name = format!("zed-test-{}", rng.gen::<u128>());
- let app_state = Self::build_app_state(&db_name).await;
- let peer = Peer::new();
- let mut router = Router::new();
- add_rpc_routes(&mut router, &app_state, &peer);
- Self {
- peer,
- router: Arc::new(router),
- app_state,
- db_name,
- }
- }
-
- async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> Client {
- let user_id = admin::create_user(&self.app_state.db, name, false)
- .await
- .unwrap();
- let lang_registry = Arc::new(LanguageRegistry::new());
- let client = Client::new(lang_registry.clone());
- let mut client_router = ForegroundRouter::new();
- cx.update(|cx| zed::worktree::init(cx, &client, &mut client_router));
-
- let (client_conn, server_conn) = Channel::bidirectional();
- cx.background()
- .spawn(rpc::handle_connection(
- self.peer.clone(),
- self.router.clone(),
- self.app_state.clone(),
- name.to_string(),
- server_conn,
- user_id,
- ))
- .detach();
- client
- .add_connection(client_conn, Arc::new(client_router), cx.to_async())
- .await
- .unwrap();
-
- client
- }
-
- async fn build_app_state(db_name: &str) -> Arc<AppState> {
- let mut config = Config::default();
- config.session_secret = "a".repeat(32);
- config.database_url = format!("postgres://postgres@localhost/{}", db_name);
-
- Self::create_db(&config.database_url).await;
- let db = PgPoolOptions::new()
- .max_connections(5)
- .connect(&config.database_url)
- .await
- .expect("failed to connect to postgres database");
- let migrator = Migrator::new(Path::new(concat!(
- env!("CARGO_MANIFEST_DIR"),
- "/migrations"
- )))
- .await
- .unwrap();
- migrator.run(&db).await.unwrap();
-
- let github_client = github::AppClient::test();
- Arc::new(AppState {
- db,
- handlebars: Default::default(),
- auth_client: auth::build_client("", ""),
- repo_client: github::RepoClient::test(&github_client),
- github_client,
- rpc: Default::default(),
- config,
- })
- }
-
- async fn create_db(url: &str) {
- // Enable tests to run in parallel by serializing the creation of each test database.
- lazy_static::lazy_static! {
- static ref DB_CREATION: async_std::sync::Mutex<()> = async_std::sync::Mutex::new(());
- }
-
- let _lock = DB_CREATION.lock().await;
- Postgres::create_database(url)
- .await
- .expect("failed to create test database");
- }
-}
-
-impl Drop for TestServer {
- fn drop(&mut self) {
- task::block_on(async {
- self.peer.reset().await;
- self.app_state
- .db
- .execute(
- format!(
- "
- SELECT pg_terminate_backend(pg_stat_activity.pid)
- FROM pg_stat_activity
- WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid();",
- self.db_name,
- )
- .as_str(),
- )
- .await
- .unwrap();
- self.app_state.db.close().await;
- Postgres::drop_database(&self.app_state.config.database_url)
- .await
- .unwrap();
- });
- }
-}
-
-struct EmptyView;
-
-impl gpui::Entity for EmptyView {
- type Event = ();
-}
-
-impl gpui::View for EmptyView {
- fn ui_name() -> &'static str {
- "empty view"
- }
-
- fn render<'a>(&self, _: &gpui::RenderContext<Self>) -> gpui::ElementBox {
- gpui::Element::boxed(gpui::elements::Empty)
- }
-}
@@ -18,8 +18,8 @@ test-support = ["tempdir", "zrpc/test-support"]
[dependencies]
anyhow = "1.0.38"
-arrayvec = "0.5.2"
async-trait = "0.1"
+arrayvec = "0.7.1"
async-tungstenite = { version = "0.14", features = ["async-tls"] }
crossbeam-channel = "0.5.0"
ctor = "0.1.20"
@@ -38,16 +38,18 @@ parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
rand = "0.8.3"
rsa = "0.4"
-rust-embed = "5.9.0"
+rust-embed = { version = "6.2", features = ["include-exclude"] }
seahash = "4.1"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_path_to_error = "0.1.4"
similar = "1.3"
simplelog = "0.9"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2.5"
surf = "2.2"
tempdir = { version = "0.3.7", optional = true }
+time = { version = "0.3" }
tiny_http = "0.8"
toml = "0.5"
tree-sitter = "0.19.5"
@@ -61,6 +63,7 @@ env_logger = "0.8"
serde_json = { version = "1.0.64", features = ["preserve_order"] }
tempdir = { version = "0.3.7" }
unindent = "0.1.7"
+zrpc = { path = "../zrpc", features = ["test-support"] }
[package.metadata.bundle]
icon = ["app-icon@2x.png", "app-icon.png"]
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8.01234 1.86426C4.13913 1.86426 1.00077 4.41444 1.00077 7.56176C1.00077 8.86644 1.54614 10.0613 2.45007 11.0186C2.04248 12.1006 1.19361 13.0149 1.17991 13.0251C0.998442 13.2168 0.950506 13.4976 1.05323 13.7373C1.15939 13.9769 1.39392 14.1358 1.65743 14.1358C3.34203 14.1358 4.64588 13.4305 5.46764 12.8689C6.23461 13.1168 7.11663 13.2593 8.01234 13.2593C11.8855 13.2593 15 10.7083 15 7.56176C15 4.41526 11.8855 1.86426 8.01234 1.86426ZM8.01508 11.9445C7.28235 11.9445 6.56002 11.8315 5.86811 11.6125L5.24494 11.4173L4.7108 11.7939C4.32047 12.0711 3.78276 12.3796 3.13577 12.5883C3.33778 12.2563 3.52939 11.883 3.68032 11.4858L3.97122 10.7188L3.4064 10.1198C2.91252 9.5915 2.31675 8.7177 2.31675 7.56176C2.31675 5.14443 4.87104 3.17907 8.01426 3.17907C11.1575 3.17907 13.7118 5.14443 13.7118 7.56176C13.7118 9.97909 11.1569 11.9445 8.01508 11.9445Z" fill="#7E7E83"/>
+</svg>
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.3125 9.3125H6.6875C4.02969 9.3125 1.875 11.4672 1.875 14.125C1.875 14.6082 2.26684 15 2.75 15H13.25C13.7332 15 14.125 14.6082 14.125 14.125C14.125 11.4672 11.9703 9.3125 9.3125 9.3125ZM3.21457 13.6875C3.43059 11.9621 4.90469 10.625 6.6875 10.625H9.3125C11.0942 10.625 12.5691 11.9635 12.7852 13.6875H3.21457ZM8 8C9.93293 8 11.5 6.43293 11.5 4.5C11.5 2.56707 9.93293 1 8 1C6.06707 1 4.5 2.56707 4.5 4.5C4.5 6.4332 6.0668 8 8 8ZM8 2.3125C9.20613 2.3125 10.1875 3.29387 10.1875 4.5C10.1875 5.70613 9.20613 6.6875 8 6.6875C6.79387 6.6875 5.8125 5.70586 5.8125 4.5C5.8125 3.29387 6.79414 2.3125 8 2.3125Z" fill="#9BA8BE"/>
+</svg>
@@ -1,47 +1,115 @@
-[ui]
-background = "$surfaces.1"
+[workspace]
+background = "$surface.0"
-[ui.tab]
-background = "$surfaces.2"
-text = "$text_colors.dull"
-border = { color = "#000000", width = 1.0 }
+[workspace.tab]
+text = "$text.2"
padding = { left = 10, right = 10 }
-icon_close = "#383839"
-icon_dirty = "#556de8"
-icon_conflict = "#e45349"
-
-[ui.active_tab]
-extends = "$ui.tab"
-background = "$surfaces.3"
-text = "$text_colors.bright"
-
-[ui.selector]
-background = "$surfaces.4"
-text = "$text_colors.bright"
-padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 }
-margin.top = 12.0
-corner_radius = 6.0
-shadow = { offset = [0.0, 0.0], blur = 12.0, color = "#00000088" }
-
-[ui.selector.item]
+icon_close = "$text.0.color"
+icon_dirty = "$status.info"
+icon_conflict = "$status.warn"
+
+[workspace.active_tab]
+extends = "$workspace.tab"
+background = "$surface.1"
+text = "$text.0"
+
+[workspace.sidebar.icons]
+padding = { left = 10, right = 10 }
+
+[workspace.sidebar.resize_handle]
+margin = { left = 6 }
+
+[workspace.sidebar_icon]
+color = "$text.2.color"
+
+[workspace.active_sidebar_icon]
+color = "$text.0.color"
+
+[chat_panel]
+channel_name = { extends = "$text.0", weight = "bold" }
+channel_name_hash = { text = "$text.2", padding.right = 5 }
+padding = 10
+
+[chat_panel.message]
+body = "$text.1"
+sender = { extends = "$text.0", weight = "bold", margin.right = 10 }
+timestamp = "$text.2"
+padding.bottom = 10
+
+[chat_panel.channel_select.item]
+padding = 4
+name = "$text.1"
+hash = { extends = "$text.2", margin.right = 5 }
+
+[chat_panel.channel_select.hovered_item]
+extends = "$chat_panel.channel_select.item"
+background = "$surface.2"
+corner_radius = 6
+
+[chat_panel.channel_select.active_item]
+extends = "$chat_panel.channel_select.item"
+name = "$text.0"
+
+[chat_panel.channel_select.hovered_active_item]
+extends = "$chat_panel.channel_select.hovered_item"
+name = "$text.0"
+
+[chat_panel.channel_select.header]
+extends = "$chat_panel.channel_select.active_item"
+padding.bottom = 0
+padding.left = 0
+
+[chat_panel.channel_select.menu]
+padding = 4
+corner_radius = 6
+border = { color = "#000000", width = 1 }
+background = "$surface.0"
+
+[chat_panel.input_editor_container]
+background = "$surface.1"
+corner_radius = 6
+padding = 6
+
+[chat_panel.input_editor]
+text = "$text.1.color"
+placeholder_text = "$text.2.color"
+background = "$surface.1"
+selection = "$selection.host"
+
+[chat_panel.sign_in_prompt]
+extends = "$text.0"
+underline = true
+
+[chat_panel.hovered_sign_in_prompt]
+extends = "$chat_panel.sign_in_prompt"
+color = "$text.1.color"
+
+[selector]
+background = "$surface.2"
+text = "$text.0"
+padding = 6
+margin.top = 12
+corner_radius = 6
+shadow = { offset = [0, 0], blur = 12, color = "#00000088" }
+input_editor = "$chat_panel.input_editor"
+
+[selector.item]
background = "#424344"
-text = "#cccccc"
-highlight_text = { color = "#18a3ff", weight = "bold" }
-border = { color = "#000000", width = 1.0 }
-padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 }
+text = "$text.1"
+highlight_text = { extends = "$text.base", color = "#18a3ff", weight = "bold" }
+border = { color = "#000000", width = 1 }
+padding = 6
-[ui.selector.active_item]
-extends = "$ui.selector.item"
+[selector.active_item]
+extends = "$selector.item"
background = "#094771"
[editor]
-background = "$surfaces.3"
-gutter_background = "$surfaces.3"
-active_line_background = "$surfaces.4"
-line_number = "$text_colors.dull"
-line_number_active = "$text_colors.bright"
-text = "$text_colors.normal"
-replicas = [
- { selection = "#264f78", cursor = "$text_colors.bright" },
- { selection = "#504f31", cursor = "#fcf154" },
-]
+text = "$text.1.color"
+background = "$surface.1"
+gutter_background = "$surface.1"
+active_line_background = "$surface.2"
+line_number = "$text.2.color"
+line_number_active = "$text.0.color"
+selection = "$selection.host"
+guest_selections = "$selection.guests"
@@ -1,19 +1,29 @@
extends = "_base"
-[surfaces]
-1 = "#050101"
+[surface]
+0 = "#222324"
+1 = "#141516"
2 = "#131415"
-3 = "#1c1d1e"
-4 = "#3a3b3c"
-[text_colors]
-dull = "#5a5a5b"
-bright = "#ffffff"
-normal = "#d4d4d4"
+[text]
+base = { family = "Inconsolata", size = 14 }
+0 = { extends = "$text.base", color = "#ffffff" }
+1 = { extends = "$text.base", color = "#b3b3b3" }
+2 = { extends = "$text.base", color = "#7b7d80" }
+
+[selection]
+host = { selection = "#264f78", cursor = "$text.0.color" }
+guests = [{ selection = "#504f31", cursor = "#fcf154" }]
+
+[status]
+good = "#4fac63"
+info = "#3c5dd4"
+warn = "#faca50"
+bad = "#b7372e"
[syntax]
keyword = { color = "#0086c0", weight = "bold" }
-function = "#dcdcaa"
+function = { color = "#dcdcaa", underline = true }
string = "#cb8f77"
type = "#4ec9b0"
number = "#b5cea8"
@@ -1,15 +1,26 @@
extends = "_base"
-[surfaces]
-1 = "#ffffff"
-2 = "#f3f3f3"
-3 = "#ececec"
-4 = "#3a3b3c"
+[surface]
+0 = "#ffffff"
+1 = "#f3f3f3"
+2 = "#ececec"
+3 = "#3a3b3c"
-[text_colors]
-dull = "#acacac"
-bright = "#111111"
-normal = "#333333"
+[text]
+base = { family = "Inconsolata", size = 14 }
+0 = { extends = "$text.base", color = "#acacac" }
+1 = { extends = "$text.base", color = "#111111" }
+2 = { extends = "$text.base", color = "#333333" }
+
+[selection]
+host = { selection = "#264f78", cursor = "$text.0.color" }
+guests = [{ selection = "#504f31", cursor = "#fcf154" }]
+
+[status]
+good = "#4fac63"
+info = "#3c5dd4"
+warn = "#faca50"
+bad = "#b7372e"
[syntax]
keyword = "#0000fa"
@@ -32,7 +32,7 @@
; Assume all-caps names are constants
((identifier) @constant
- (#match? @constant "^[A-Z][A-Z\\d_]+$'"))
+ (#match? @constant "^[A-Z][A-Z\\d_]+$"))
[
"as"
@@ -4,11 +4,14 @@ use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "assets"]
+#[exclude = "*.DS_Store"]
pub struct Assets;
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
- Self::get(path).ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
+ Self::get(path)
+ .map(|f| f.data)
+ .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
}
fn list(&self, path: &str) -> Vec<std::borrow::Cow<'static, str>> {
@@ -0,0 +1,708 @@
+use crate::{
+ rpc::{self, Client},
+ user::{User, UserStore},
+ util::TryFutureExt,
+};
+use anyhow::{anyhow, Context, Result};
+use gpui::{
+ sum_tree::{self, Bias, SumTree},
+ Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle,
+};
+use postage::prelude::Stream;
+use std::{
+ collections::{HashMap, HashSet},
+ ops::Range,
+ sync::Arc,
+};
+use time::OffsetDateTime;
+use zrpc::{
+ proto::{self, ChannelMessageSent},
+ TypedEnvelope,
+};
+
+pub struct ChannelList {
+ available_channels: Option<Vec<ChannelDetails>>,
+ channels: HashMap<u64, WeakModelHandle<Channel>>,
+ rpc: Arc<Client>,
+ user_store: Arc<UserStore>,
+ _task: Task<Option<()>>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct ChannelDetails {
+ pub id: u64,
+ pub name: String,
+}
+
+pub struct Channel {
+ details: ChannelDetails,
+ messages: SumTree<ChannelMessage>,
+ loaded_all_messages: bool,
+ pending_messages: Vec<PendingChannelMessage>,
+ next_local_message_id: u64,
+ user_store: Arc<UserStore>,
+ rpc: Arc<Client>,
+ _subscription: rpc::Subscription,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct ChannelMessage {
+ pub id: u64,
+ pub body: String,
+ pub timestamp: OffsetDateTime,
+ pub sender: Arc<User>,
+}
+
+pub struct PendingChannelMessage {
+ pub body: String,
+ local_id: u64,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct ChannelMessageSummary {
+ max_id: u64,
+ count: usize,
+}
+
+#[derive(Copy, Clone, Debug, Default)]
+struct Count(usize);
+
+pub enum ChannelListEvent {}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum ChannelEvent {
+ MessagesAdded {
+ old_range: Range<usize>,
+ new_count: usize,
+ },
+}
+
+impl Entity for ChannelList {
+ type Event = ChannelListEvent;
+}
+
+impl ChannelList {
+ pub fn new(
+ user_store: Arc<UserStore>,
+ rpc: Arc<rpc::Client>,
+ cx: &mut ModelContext<Self>,
+ ) -> Self {
+ let _task = cx.spawn(|this, mut cx| {
+ let rpc = rpc.clone();
+ async move {
+ let mut user_id = rpc.user_id();
+ loop {
+ let available_channels = if user_id.recv().await.unwrap().is_some() {
+ Some(
+ rpc.request(proto::GetChannels {})
+ .await
+ .context("failed to fetch available channels")?
+ .channels
+ .into_iter()
+ .map(Into::into)
+ .collect(),
+ )
+ } else {
+ None
+ };
+
+ this.update(&mut cx, |this, cx| {
+ if available_channels.is_none() {
+ if this.available_channels.is_none() {
+ return;
+ }
+ this.channels.clear();
+ }
+ this.available_channels = available_channels;
+ cx.notify();
+ });
+ }
+ }
+ .log_err()
+ });
+
+ Self {
+ available_channels: None,
+ channels: Default::default(),
+ user_store,
+ rpc,
+ _task,
+ }
+ }
+
+ pub fn available_channels(&self) -> Option<&[ChannelDetails]> {
+ self.available_channels.as_ref().map(Vec::as_slice)
+ }
+
+ pub fn get_channel(
+ &mut self,
+ id: u64,
+ cx: &mut MutableAppContext,
+ ) -> Option<ModelHandle<Channel>> {
+ if let Some(channel) = self.channels.get(&id).and_then(|c| c.upgrade(cx)) {
+ return Some(channel);
+ }
+
+ let channels = self.available_channels.as_ref()?;
+ let details = channels.iter().find(|details| details.id == id)?.clone();
+ let channel =
+ cx.add_model(|cx| Channel::new(details, self.user_store.clone(), self.rpc.clone(), cx));
+ self.channels.insert(id, channel.downgrade());
+ Some(channel)
+ }
+}
+
+impl Entity for Channel {
+ type Event = ChannelEvent;
+
+ fn release(&mut self, cx: &mut MutableAppContext) {
+ let rpc = self.rpc.clone();
+ let channel_id = self.details.id;
+ cx.foreground()
+ .spawn(async move {
+ if let Err(error) = rpc.send(proto::LeaveChannel { channel_id }).await {
+ log::error!("error leaving channel: {}", error);
+ };
+ })
+ .detach()
+ }
+}
+
+impl Channel {
+ pub fn new(
+ details: ChannelDetails,
+ user_store: Arc<UserStore>,
+ rpc: Arc<Client>,
+ cx: &mut ModelContext<Self>,
+ ) -> Self {
+ let _subscription = rpc.subscribe_from_model(details.id, cx, Self::handle_message_sent);
+
+ {
+ let user_store = user_store.clone();
+ let rpc = rpc.clone();
+ let channel_id = details.id;
+ cx.spawn(|channel, 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| {
+ channel.insert_messages(messages, cx);
+ channel.loaded_all_messages = loaded_all_messages;
+ });
+
+ Ok(())
+ }
+ .log_err()
+ })
+ .detach();
+ }
+
+ Self {
+ details,
+ user_store,
+ rpc,
+ messages: Default::default(),
+ pending_messages: Default::default(),
+ loaded_all_messages: false,
+ next_local_message_id: 0,
+ _subscription,
+ }
+ }
+
+ pub fn name(&self) -> &str {
+ &self.details.name
+ }
+
+ pub fn send_message(
+ &mut self,
+ body: String,
+ cx: &mut ModelContext<Self>,
+ ) -> Result<Task<Result<()>>> {
+ if body.is_empty() {
+ Err(anyhow!("message body can't be empty"))?;
+ }
+
+ 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 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 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| {
+ 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);
+ }
+ Ok(())
+ })
+ }))
+ }
+
+ pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> bool {
+ if !self.loaded_all_messages {
+ 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) {
+ cx.spawn(|this, mut cx| {
+ async move {
+ let response = rpc
+ .request(proto::GetChannelMessages {
+ channel_id,
+ before_message_id,
+ })
+ .await?;
+ let loaded_all_messages = response.done;
+ let messages = messages_from_proto(response.messages, &user_store).await?;
+ this.update(&mut cx, |this, cx| {
+ this.loaded_all_messages = loaded_all_messages;
+ this.insert_messages(messages, cx);
+ });
+ Ok(())
+ }
+ .log_err()
+ })
+ .detach();
+ return true;
+ }
+ }
+ false
+ }
+
+ pub fn message_count(&self) -> usize {
+ self.messages.summary().count
+ }
+
+ pub fn messages(&self) -> &SumTree<ChannelMessage> {
+ &self.messages
+ }
+
+ pub fn message(&self, ix: usize) -> &ChannelMessage {
+ let mut cursor = self.messages.cursor::<Count, ()>();
+ cursor.seek(&Count(ix), Bias::Right, &());
+ cursor.item().unwrap()
+ }
+
+ pub fn messages_in_range(&self, range: Range<usize>) -> impl Iterator<Item = &ChannelMessage> {
+ let mut cursor = self.messages.cursor::<Count, ()>();
+ cursor.seek(&Count(range.start), Bias::Right, &());
+ cursor.take(range.len())
+ }
+
+ pub fn pending_messages(&self) -> &[PendingChannelMessage] {
+ &self.pending_messages
+ }
+
+ fn handle_message_sent(
+ &mut self,
+ message: TypedEnvelope<ChannelMessageSent>,
+ _: Arc<rpc::Client>,
+ cx: &mut ModelContext<Self>,
+ ) -> Result<()> {
+ let user_store = self.user_store.clone();
+ let message = message
+ .payload
+ .message
+ .ok_or_else(|| anyhow!("empty message"))?;
+
+ cx.spawn(|this, mut cx| {
+ async move {
+ let message = ChannelMessage::from_proto(message, &user_store).await?;
+ this.update(&mut cx, |this, cx| {
+ this.insert_messages(SumTree::from_item(message, &()), cx)
+ });
+ Ok(())
+ }
+ .log_err()
+ })
+ .detach();
+ Ok(())
+ }
+
+ 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 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, &());
+ let removed_count = removed_messages.summary().count;
+ let new_count = messages.summary().count;
+ let end_ix = start_ix + removed_count;
+
+ new_messages.push_tree(messages, &());
+ new_messages.push_tree(old_cursor.suffix(&()), &());
+ drop(old_cursor);
+ self.messages = new_messages;
+
+ cx.emit(ChannelEvent::MessagesAdded {
+ old_range: start_ix..end_ix,
+ new_count,
+ });
+ cx.notify();
+ }
+ }
+}
+
+async fn messages_from_proto(
+ proto_messages: Vec<proto::ChannelMessage>,
+ user_store: &UserStore,
+) -> Result<SumTree<ChannelMessage>> {
+ let unique_user_ids = proto_messages
+ .iter()
+ .map(|m| m.sender_id)
+ .collect::<HashSet<_>>()
+ .into_iter()
+ .collect();
+ user_store.load_users(unique_user_ids).await?;
+
+ let mut messages = Vec::with_capacity(proto_messages.len());
+ for message in proto_messages {
+ messages.push(ChannelMessage::from_proto(message, &user_store).await?);
+ }
+ let mut result = SumTree::new();
+ result.extend(messages, &());
+ Ok(result)
+}
+
+impl From<proto::Channel> for ChannelDetails {
+ fn from(message: proto::Channel) -> Self {
+ Self {
+ id: message.id,
+ name: message.name,
+ }
+ }
+}
+
+impl ChannelMessage {
+ pub async fn from_proto(
+ message: proto::ChannelMessage,
+ user_store: &UserStore,
+ ) -> Result<Self> {
+ let sender = user_store.get_user(message.sender_id).await?;
+ Ok(ChannelMessage {
+ id: message.id,
+ body: message.body,
+ timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
+ sender,
+ })
+ }
+}
+
+impl sum_tree::Item for ChannelMessage {
+ type Summary = ChannelMessageSummary;
+
+ fn summary(&self) -> Self::Summary {
+ ChannelMessageSummary {
+ max_id: self.id,
+ count: 1,
+ }
+ }
+}
+
+impl sum_tree::Summary for ChannelMessageSummary {
+ type Context = ();
+
+ fn add_summary(&mut self, summary: &Self, _: &()) {
+ self.max_id = summary.max_id;
+ self.count += summary.count;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for u64 {
+ fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
+ debug_assert!(summary.max_id > *self);
+ *self = summary.max_id;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
+ fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
+ self.0 += summary.count;
+ }
+}
+
+impl<'a> sum_tree::SeekDimension<'a, ChannelMessageSummary> for Count {
+ fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
+ Ord::cmp(&self.0, &other.0)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::TestAppContext;
+ use postage::mpsc::Receiver;
+ use zrpc::{test::Channel, ConnectionId, Peer, Receipt};
+
+ #[gpui::test]
+ async fn test_channel_messages(mut cx: TestAppContext) {
+ let user_id = 5;
+ let client = Client::new();
+ let mut server = FakeServer::for_client(user_id, &client, &cx).await;
+ let user_store = Arc::new(UserStore::new(client.clone()));
+
+ 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));
+
+ // Get the available channels.
+ let get_channels = server.receive::<proto::GetChannels>().await;
+ server
+ .respond(
+ get_channels.receipt(),
+ proto::GetChannelsResponse {
+ channels: vec![proto::Channel {
+ id: 5,
+ name: "the-channel".to_string(),
+ }],
+ },
+ )
+ .await;
+ channel_list.next_notification(&cx).await;
+ channel_list.read_with(&cx, |list, _| {
+ assert_eq!(
+ list.available_channels().unwrap(),
+ &[ChannelDetails {
+ id: 5,
+ name: "the-channel".into(),
+ }]
+ )
+ });
+
+ // Join a channel and populate its existing messages.
+ let channel = channel_list
+ .update(&mut cx, |list, cx| {
+ let channel_id = list.available_channels().unwrap()[0].id;
+ list.get_channel(channel_id, cx)
+ })
+ .unwrap();
+ channel.read_with(&cx, |channel, _| assert!(channel.messages().is_empty()));
+ let join_channel = server.receive::<proto::JoinChannel>().await;
+ server
+ .respond(
+ join_channel.receipt(),
+ proto::JoinChannelResponse {
+ messages: vec![
+ proto::ChannelMessage {
+ id: 10,
+ body: "a".into(),
+ timestamp: 1000,
+ sender_id: 5,
+ },
+ proto::ChannelMessage {
+ id: 11,
+ body: "b".into(),
+ timestamp: 1001,
+ sender_id: 6,
+ },
+ ],
+ done: false,
+ },
+ )
+ .await;
+
+ // Client requests all users for the received messages
+ let mut get_users = server.receive::<proto::GetUsers>().await;
+ get_users.payload.user_ids.sort();
+ assert_eq!(get_users.payload.user_ids, vec![5, 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(),
+ },
+ ],
+ },
+ )
+ .await;
+
+ assert_eq!(
+ channel.next_event(&cx).await,
+ ChannelEvent::MessagesAdded {
+ old_range: 0..0,
+ new_count: 2,
+ }
+ );
+ channel.read_with(&cx, |channel, _| {
+ assert_eq!(
+ channel
+ .messages_in_range(0..2)
+ .map(|message| (message.sender.github_login.clone(), message.body.clone()))
+ .collect::<Vec<_>>(),
+ &[
+ ("nathansobo".into(), "a".into()),
+ ("maxbrunsfeld".into(), "b".into())
+ ]
+ );
+ });
+
+ // Receive a new message.
+ server
+ .send(proto::ChannelMessageSent {
+ channel_id: channel.read_with(&cx, |channel, _| channel.details.id),
+ message: Some(proto::ChannelMessage {
+ id: 12,
+ body: "c".into(),
+ timestamp: 1002,
+ sender_id: 7,
+ }),
+ })
+ .await;
+
+ // Client requests user for message since they haven't seen them yet
+ let get_users = server.receive::<proto::GetUsers>().await;
+ assert_eq!(get_users.payload.user_ids, vec![7]);
+ server
+ .respond(
+ get_users.receipt(),
+ proto::GetUsersResponse {
+ users: vec![proto::User {
+ id: 7,
+ github_login: "as-cii".into(),
+ avatar_url: "http://avatar.com/as-cii".into(),
+ }],
+ },
+ )
+ .await;
+
+ assert_eq!(
+ channel.next_event(&cx).await,
+ ChannelEvent::MessagesAdded {
+ old_range: 2..2,
+ new_count: 1,
+ }
+ );
+ channel.read_with(&cx, |channel, _| {
+ assert_eq!(
+ channel
+ .messages_in_range(2..3)
+ .map(|message| (message.sender.github_login.clone(), message.body.clone()))
+ .collect::<Vec<_>>(),
+ &[("as-cii".into(), "c".into())]
+ )
+ });
+
+ // Scroll up to view older messages.
+ channel.update(&mut cx, |channel, cx| {
+ assert!(channel.load_more_messages(cx));
+ });
+ let get_messages = server.receive::<proto::GetChannelMessages>().await;
+ assert_eq!(get_messages.payload.channel_id, 5);
+ assert_eq!(get_messages.payload.before_message_id, 10);
+ server
+ .respond(
+ get_messages.receipt(),
+ proto::GetChannelMessagesResponse {
+ done: true,
+ messages: vec![
+ proto::ChannelMessage {
+ id: 8,
+ body: "y".into(),
+ timestamp: 998,
+ sender_id: 5,
+ },
+ proto::ChannelMessage {
+ id: 9,
+ body: "z".into(),
+ timestamp: 999,
+ sender_id: 6,
+ },
+ ],
+ },
+ )
+ .await;
+
+ assert_eq!(
+ channel.next_event(&cx).await,
+ ChannelEvent::MessagesAdded {
+ old_range: 0..0,
+ new_count: 2,
+ }
+ );
+ channel.read_with(&cx, |channel, _| {
+ assert_eq!(
+ channel
+ .messages_in_range(0..2)
+ .map(|message| (message.sender.github_login.clone(), message.body.clone()))
+ .collect::<Vec<_>>(),
+ &[
+ ("nathansobo".into(), "y".into()),
+ ("maxbrunsfeld".into(), "z".into())
+ ]
+ );
+ });
+ }
+
+ struct FakeServer {
+ peer: Arc<Peer>,
+ incoming: Receiver<Box<dyn proto::AnyTypedEnvelope>>,
+ connection_id: ConnectionId,
+ }
+
+ impl FakeServer {
+ async fn for_client(user_id: u64, client: &Arc<Client>, cx: &TestAppContext) -> Self {
+ let (client_conn, server_conn) = Channel::bidirectional();
+ let peer = Peer::new();
+ let (connection_id, io, incoming) = peer.add_connection(server_conn).await;
+ cx.background().spawn(io).detach();
+
+ client
+ .add_connection(user_id, client_conn, &cx.to_async())
+ .await
+ .unwrap();
+
+ Self {
+ peer,
+ incoming,
+ connection_id,
+ }
+ }
+
+ async fn send<T: proto::EnvelopedMessage>(&self, message: T) {
+ self.peer.send(self.connection_id, message).await.unwrap();
+ }
+
+ async fn receive<M: proto::EnvelopedMessage>(&mut self) -> TypedEnvelope<M> {
+ *self
+ .incoming
+ .recv()
+ .await
+ .unwrap()
+ .into_any()
+ .downcast::<TypedEnvelope<M>>()
+ .unwrap()
+ }
+
+ async fn respond<T: proto::RequestMessage>(
+ &self,
+ receipt: Receipt<T>,
+ response: T::Response,
+ ) {
+ self.peer.respond(receipt, response).await.unwrap()
+ }
+ }
+}
@@ -0,0 +1,421 @@
+use std::sync::Arc;
+
+use crate::{
+ channel::{Channel, ChannelEvent, ChannelList, ChannelMessage},
+ editor::Editor,
+ rpc::Client,
+ theme,
+ util::{ResultExt, TryFutureExt},
+ Settings,
+};
+use gpui::{
+ action,
+ elements::*,
+ keymap::Binding,
+ platform::CursorStyle,
+ views::{ItemType, Select, SelectStyle},
+ AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, View,
+ ViewContext, ViewHandle,
+};
+use postage::watch;
+use time::{OffsetDateTime, UtcOffset};
+
+const MESSAGE_LOADING_THRESHOLD: usize = 50;
+
+pub struct ChatPanel {
+ rpc: Arc<Client>,
+ channel_list: ModelHandle<ChannelList>,
+ active_channel: Option<(ModelHandle<Channel>, Subscription)>,
+ message_list: ListState,
+ input_editor: ViewHandle<Editor>,
+ channel_select: ViewHandle<Select>,
+ settings: watch::Receiver<Settings>,
+ local_timezone: UtcOffset,
+}
+
+pub enum Event {}
+
+action!(Send);
+action!(LoadMoreMessages);
+
+pub fn init(cx: &mut MutableAppContext) {
+ cx.add_action(ChatPanel::send);
+ cx.add_action(ChatPanel::load_more_messages);
+
+ cx.add_bindings(vec![Binding::new("enter", Send, Some("ChatPanel"))]);
+}
+
+impl ChatPanel {
+ pub fn new(
+ rpc: Arc<Client>,
+ channel_list: ModelHandle<ChannelList>,
+ settings: watch::Receiver<Settings>,
+ 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()
+ })
+ });
+ let channel_select = cx.add_view(|cx| {
+ let channel_list = channel_list.clone();
+ Select::new(0, cx, {
+ let settings = settings.clone();
+ move |ix, item_type, is_hovered, cx| {
+ Self::render_channel_name(
+ &channel_list,
+ ix,
+ item_type,
+ is_hovered,
+ &settings.borrow().theme.chat_panel.channel_select,
+ cx,
+ )
+ }
+ })
+ .with_style({
+ let settings = settings.clone();
+ move |_| {
+ let theme = &settings.borrow().theme.chat_panel.channel_select;
+ SelectStyle {
+ header: theme.header.container.clone(),
+ menu: theme.menu.clone(),
+ }
+ }
+ })
+ });
+
+ let mut message_list = ListState::new(0, Orientation::Bottom, 1000., {
+ let this = cx.handle().downgrade();
+ move |ix, cx| {
+ let this = this.upgrade(cx).unwrap().read(cx);
+ let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix);
+ this.render_message(message)
+ }
+ });
+ message_list.set_scroll_handler(|visible_range, cx| {
+ if visible_range.start < MESSAGE_LOADING_THRESHOLD {
+ cx.dispatch_action(LoadMoreMessages);
+ }
+ });
+
+ let mut this = Self {
+ rpc,
+ channel_list,
+ active_channel: Default::default(),
+ message_list,
+ input_editor,
+ channel_select,
+ settings,
+ local_timezone: cx.platform().local_timezone(),
+ };
+
+ this.init_active_channel(cx);
+ cx.observe(&this.channel_list, |this, _, cx| {
+ this.init_active_channel(cx);
+ })
+ .detach();
+ cx.observe(&this.channel_select, |this, channel_select, cx| {
+ let selected_ix = channel_select.read(cx).selected_index();
+ let selected_channel = this.channel_list.update(cx, |channel_list, cx| {
+ let available_channels = channel_list.available_channels()?;
+ let channel_id = available_channels.get(selected_ix)?.id;
+ channel_list.get_channel(channel_id, cx)
+ });
+ if let Some(selected_channel) = selected_channel {
+ this.set_active_channel(selected_channel, cx);
+ }
+ })
+ .detach();
+
+ this
+ }
+
+ fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
+ let (active_channel, channel_count) = self.channel_list.update(cx, |list, cx| {
+ let channel_count;
+ let mut active_channel = None;
+
+ if let Some(available_channels) = list.available_channels() {
+ channel_count = available_channels.len();
+ if self.active_channel.is_none() {
+ if let Some(channel_id) = available_channels.first().map(|channel| channel.id) {
+ active_channel = list.get_channel(channel_id, cx);
+ }
+ }
+ } else {
+ channel_count = 0;
+ }
+
+ (active_channel, channel_count)
+ });
+
+ if let Some(active_channel) = active_channel {
+ self.set_active_channel(active_channel, cx);
+ } else {
+ self.active_channel = None;
+ }
+
+ self.channel_select.update(cx, |select, cx| {
+ select.set_item_count(channel_count, cx);
+ });
+ }
+
+ fn set_active_channel(&mut self, channel: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
+ if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
+ {
+ let channel = channel.read(cx);
+ self.message_list.reset(channel.message_count());
+ let placeholder = format!("Message #{}", channel.name());
+ self.input_editor.update(cx, move |editor, cx| {
+ editor.set_placeholder_text(placeholder, cx);
+ });
+ }
+ let subscription = cx.subscribe(&channel, Self::channel_did_change);
+ self.active_channel = Some((channel, subscription));
+ }
+ }
+
+ fn channel_did_change(
+ &mut self,
+ _: ModelHandle<Channel>,
+ event: &ChannelEvent,
+ cx: &mut ViewContext<Self>,
+ ) {
+ match event {
+ ChannelEvent::MessagesAdded {
+ old_range,
+ new_count,
+ } => {
+ self.message_list.splice(old_range.clone(), *new_count);
+ }
+ }
+ cx.notify();
+ }
+
+ fn render_channel(&self) -> ElementBox {
+ let theme = &self.settings.borrow().theme;
+ Flex::column()
+ .with_child(
+ Container::new(ChildView::new(self.channel_select.id()).boxed())
+ .with_style(&theme.chat_panel.channel_select.container)
+ .boxed(),
+ )
+ .with_child(self.render_active_channel_messages())
+ .with_child(self.render_input_box())
+ .boxed()
+ }
+
+ fn render_active_channel_messages(&self) -> ElementBox {
+ let messages = if self.active_channel.is_some() {
+ List::new(self.message_list.clone()).boxed()
+ } else {
+ Empty::new().boxed()
+ };
+
+ Expanded::new(1., messages).boxed()
+ }
+
+ fn render_message(&self, message: &ChannelMessage) -> ElementBox {
+ let now = OffsetDateTime::now_utc();
+ let settings = self.settings.borrow();
+ let theme = &settings.theme.chat_panel.message;
+ Container::new(
+ Flex::column()
+ .with_child(
+ Flex::row()
+ .with_child(
+ Container::new(
+ Label::new(
+ message.sender.github_login.clone(),
+ theme.sender.text.clone(),
+ )
+ .boxed(),
+ )
+ .with_style(&theme.sender.container)
+ .boxed(),
+ )
+ .with_child(
+ Container::new(
+ Label::new(
+ format_timestamp(message.timestamp, now, self.local_timezone),
+ theme.timestamp.text.clone(),
+ )
+ .boxed(),
+ )
+ .with_style(&theme.timestamp.container)
+ .boxed(),
+ )
+ .boxed(),
+ )
+ .with_child(Text::new(message.body.clone(), theme.body.clone()).boxed())
+ .boxed(),
+ )
+ .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)
+ .boxed()
+ }
+
+ fn render_channel_name(
+ channel_list: &ModelHandle<ChannelList>,
+ ix: usize,
+ item_type: ItemType,
+ is_hovered: bool,
+ theme: &theme::ChannelSelect,
+ cx: &AppContext,
+ ) -> ElementBox {
+ let channel = &channel_list.read(cx).available_channels().unwrap()[ix];
+ let theme = match (item_type, is_hovered) {
+ (ItemType::Header, _) => &theme.header,
+ (ItemType::Selected, false) => &theme.active_item,
+ (ItemType::Selected, true) => &theme.hovered_active_item,
+ (ItemType::Unselected, false) => &theme.item,
+ (ItemType::Unselected, true) => &theme.hovered_item,
+ };
+ Container::new(
+ Flex::row()
+ .with_child(
+ Container::new(Label::new("#".to_string(), theme.hash.text.clone()).boxed())
+ .with_style(&theme.hash.container)
+ .boxed(),
+ )
+ .with_child(Label::new(channel.name.clone(), theme.name.clone()).boxed())
+ .boxed(),
+ )
+ .with_style(&theme.container)
+ .boxed()
+ }
+
+ fn render_sign_in_prompt(&self, cx: &mut RenderContext<Self>) -> ElementBox {
+ let theme = &self.settings.borrow().theme;
+ let rpc = self.rpc.clone();
+ let this = cx.handle();
+
+ enum SignInPromptLabel {}
+
+ Align::new(
+ MouseEventHandler::new::<SignInPromptLabel, _, _, _>(0, cx, |mouse_state, _| {
+ Label::new(
+ "Sign in to use chat".to_string(),
+ if mouse_state.hovered {
+ theme.chat_panel.hovered_sign_in_prompt.clone()
+ } else {
+ theme.chat_panel.sign_in_prompt.clone()
+ },
+ )
+ .boxed()
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(move |cx| {
+ let rpc = rpc.clone();
+ let this = this.clone();
+ cx.spawn(|mut cx| async move {
+ if rpc.authenticate_and_connect(&cx).log_err().await.is_some() {
+ cx.update(|cx| {
+ if let Some(this) = this.upgrade(cx) {
+ if this.is_focused(cx) {
+ this.update(cx, |this, cx| cx.focus(&this.input_editor));
+ }
+ }
+ })
+ }
+ })
+ .detach();
+ })
+ .boxed(),
+ )
+ .boxed()
+ }
+
+ fn send(&mut self, _: &Send, cx: &mut ViewContext<Self>) {
+ if let Some((channel, _)) = self.active_channel.as_ref() {
+ let body = self.input_editor.update(cx, |editor, cx| {
+ let body = editor.text(cx);
+ editor.clear(cx);
+ body
+ });
+
+ if let Some(task) = channel
+ .update(cx, |channel, cx| channel.send_message(body, cx))
+ .log_err()
+ {
+ task.detach();
+ }
+ }
+ }
+
+ fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
+ if let Some((channel, _)) = self.active_channel.as_ref() {
+ channel.update(cx, |channel, cx| {
+ channel.load_more_messages(cx);
+ })
+ }
+ }
+
+ fn is_signed_in(&self) -> bool {
+ self.rpc.user_id().borrow().is_some()
+ }
+}
+
+impl Entity for ChatPanel {
+ type Event = Event;
+}
+
+impl View for ChatPanel {
+ fn ui_name() -> &'static str {
+ "ChatPanel"
+ }
+
+ fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+ let theme = &self.settings.borrow().theme;
+ let element = if self.is_signed_in() {
+ self.render_channel()
+ } else {
+ self.render_sign_in_prompt(cx)
+ };
+ ConstrainedBox::new(
+ Container::new(element)
+ .with_style(&theme.chat_panel.container)
+ .boxed(),
+ )
+ .with_min_width(150.)
+ .boxed()
+ }
+
+ fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+ if self.is_signed_in() {
+ cx.focus(&self.input_editor);
+ }
+ }
+}
+
+fn format_timestamp(
+ mut timestamp: OffsetDateTime,
+ mut now: OffsetDateTime,
+ local_timezone: UtcOffset,
+) -> String {
+ timestamp = timestamp.to_offset(local_timezone);
+ now = now.to_offset(local_timezone);
+
+ let today = now.date();
+ let date = timestamp.date();
+ let mut hour = timestamp.hour();
+ let mut part = "am";
+ if hour > 12 {
+ hour -= 12;
+ part = "pm";
+ }
+ if date == today {
+ format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
+ } else if date.next_day() == Some(today) {
+ format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
+ } else {
+ format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
+ }
+}
@@ -4,7 +4,8 @@ mod element;
pub mod movement;
use crate::{
- settings::{HighlightId, Settings, Theme},
+ settings::{HighlightId, Settings},
+ theme::{EditorStyle, Theme},
time::ReplicaId,
util::{post_inc, Bias},
workspace,
@@ -16,16 +17,22 @@ pub use display_map::DisplayPoint;
use display_map::*;
pub use element::*;
use gpui::{
- color::Color, font_cache::FamilyId, fonts::Properties as FontProperties,
- geometry::vector::Vector2F, keymap::Binding, text_layout, AppContext, ClipboardItem, Element,
- ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, RenderContext, Task,
- TextLayoutCache, View, ViewContext, WeakViewHandle,
+ 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,
};
use postage::watch;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use smol::Timer;
use std::{
+ cell::RefCell,
cmp::{self, Ordering},
collections::BTreeMap,
fmt::Write,
@@ -33,6 +40,7 @@ use std::{
mem,
ops::{Range, RangeInclusive},
path::Path,
+ rc::Rc,
sync::Arc,
time::Duration,
};
@@ -40,324 +48,218 @@ use std::{
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const MAX_LINE_LEN: usize = 1024;
+action!(Cancel);
+action!(Backspace);
+action!(Delete);
+action!(Insert, String);
+action!(DeleteLine);
+action!(DeleteToPreviousWordBoundary);
+action!(DeleteToNextWordBoundary);
+action!(DeleteToBeginningOfLine);
+action!(DeleteToEndOfLine);
+action!(CutToEndOfLine);
+action!(DuplicateLine);
+action!(MoveLineUp);
+action!(MoveLineDown);
+action!(Cut);
+action!(Copy);
+action!(Paste);
+action!(Undo);
+action!(Redo);
+action!(MoveUp);
+action!(MoveDown);
+action!(MoveLeft);
+action!(MoveRight);
+action!(MoveToPreviousWordBoundary);
+action!(MoveToNextWordBoundary);
+action!(MoveToBeginningOfLine);
+action!(MoveToEndOfLine);
+action!(MoveToBeginning);
+action!(MoveToEnd);
+action!(SelectUp);
+action!(SelectDown);
+action!(SelectLeft);
+action!(SelectRight);
+action!(SelectToPreviousWordBoundary);
+action!(SelectToNextWordBoundary);
+action!(SelectToBeginningOfLine, bool);
+action!(SelectToEndOfLine);
+action!(SelectToBeginning);
+action!(SelectToEnd);
+action!(SelectAll);
+action!(SelectLine);
+action!(SplitSelectionIntoLines);
+action!(AddSelectionAbove);
+action!(AddSelectionBelow);
+action!(SelectLargerSyntaxNode);
+action!(SelectSmallerSyntaxNode);
+action!(MoveToEnclosingBracket);
+action!(PageUp);
+action!(PageDown);
+action!(Fold);
+action!(Unfold);
+action!(FoldSelectedRanges);
+action!(Scroll, Vector2F);
+action!(Select, SelectPhase);
+
pub fn init(cx: &mut MutableAppContext) {
cx.add_bindings(vec![
- Binding::new("escape", "buffer:cancel", Some("BufferView")),
- Binding::new("backspace", "buffer:backspace", Some("BufferView")),
- Binding::new("ctrl-h", "buffer:backspace", Some("BufferView")),
- Binding::new("delete", "buffer:delete", Some("BufferView")),
- Binding::new("ctrl-d", "buffer:delete", Some("BufferView")),
- Binding::new("enter", "buffer:newline", Some("BufferView")),
- Binding::new("tab", "buffer:insert", Some("BufferView")).with_arg("\t".to_string()),
- Binding::new("ctrl-shift-K", "buffer:delete_line", Some("BufferView")),
- Binding::new(
- "alt-backspace",
- "buffer:delete_to_previous_word_boundary",
- Some("BufferView"),
- ),
- Binding::new(
- "alt-h",
- "buffer:delete_to_previous_word_boundary",
- Some("BufferView"),
- ),
- Binding::new(
- "alt-delete",
- "buffer:delete_to_next_word_boundary",
- Some("BufferView"),
- ),
- Binding::new(
- "alt-d",
- "buffer:delete_to_next_word_boundary",
- Some("BufferView"),
- ),
- Binding::new(
- "cmd-backspace",
- "buffer:delete_to_beginning_of_line",
- Some("BufferView"),
- ),
- Binding::new(
- "cmd-delete",
- "buffer:delete_to_end_of_line",
- Some("BufferView"),
- ),
- Binding::new("ctrl-k", "buffer:cut_to_end_of_line", Some("BufferView")),
- Binding::new("cmd-shift-D", "buffer:duplicate_line", Some("BufferView")),
- Binding::new("ctrl-cmd-up", "buffer:move_line_up", Some("BufferView")),
- Binding::new("ctrl-cmd-down", "buffer:move_line_down", Some("BufferView")),
- Binding::new("cmd-x", "buffer:cut", Some("BufferView")),
- Binding::new("cmd-c", "buffer:copy", Some("BufferView")),
- Binding::new("cmd-v", "buffer:paste", Some("BufferView")),
- Binding::new("cmd-z", "buffer:undo", Some("BufferView")),
- Binding::new("cmd-shift-Z", "buffer:redo", Some("BufferView")),
- Binding::new("up", "buffer:move_up", Some("BufferView")),
- Binding::new("down", "buffer:move_down", Some("BufferView")),
- Binding::new("left", "buffer:move_left", Some("BufferView")),
- Binding::new("right", "buffer:move_right", Some("BufferView")),
- Binding::new("ctrl-p", "buffer:move_up", Some("BufferView")),
- Binding::new("ctrl-n", "buffer:move_down", Some("BufferView")),
- Binding::new("ctrl-b", "buffer:move_left", Some("BufferView")),
- Binding::new("ctrl-f", "buffer:move_right", Some("BufferView")),
- Binding::new(
- "alt-left",
- "buffer:move_to_previous_word_boundary",
- Some("BufferView"),
- ),
- Binding::new(
- "alt-b",
- "buffer:move_to_previous_word_boundary",
- Some("BufferView"),
- ),
- Binding::new(
- "alt-right",
- "buffer:move_to_next_word_boundary",
- Some("BufferView"),
- ),
- Binding::new(
- "alt-f",
- "buffer:move_to_next_word_boundary",
- Some("BufferView"),
- ),
- Binding::new(
- "cmd-left",
- "buffer:move_to_beginning_of_line",
- Some("BufferView"),
- ),
+ Binding::new("escape", Cancel, Some("Editor")),
+ Binding::new("backspace", Backspace, Some("Editor")),
+ Binding::new("ctrl-h", Backspace, Some("Editor")),
+ Binding::new("delete", Delete, Some("Editor")),
+ Binding::new("ctrl-d", Delete, Some("Editor")),
+ Binding::new("enter", Insert("\n".into()), Some("Editor && mode == full")),
Binding::new(
- "ctrl-a",
- "buffer:move_to_beginning_of_line",
- Some("BufferView"),
+ "alt-enter",
+ Insert("\n".into()),
+ Some("Editor && mode == auto_height"),
),
+ Binding::new("tab", Insert("\t".into()), Some("Editor")),
+ Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")),
Binding::new(
- "cmd-right",
- "buffer:move_to_end_of_line",
- Some("BufferView"),
+ "alt-backspace",
+ DeleteToPreviousWordBoundary,
+ Some("Editor"),
),
- Binding::new("ctrl-e", "buffer:move_to_end_of_line", Some("BufferView")),
- Binding::new("cmd-up", "buffer:move_to_beginning", Some("BufferView")),
- Binding::new("cmd-down", "buffer:move_to_end", Some("BufferView")),
- Binding::new("shift-up", "buffer:select_up", Some("BufferView")),
- Binding::new("ctrl-shift-P", "buffer:select_up", Some("BufferView")),
- Binding::new("shift-down", "buffer:select_down", Some("BufferView")),
- Binding::new("ctrl-shift-N", "buffer:select_down", Some("BufferView")),
- Binding::new("shift-left", "buffer:select_left", Some("BufferView")),
- Binding::new("ctrl-shift-B", "buffer:select_left", Some("BufferView")),
- Binding::new("shift-right", "buffer:select_right", Some("BufferView")),
- Binding::new("ctrl-shift-F", "buffer:select_right", Some("BufferView")),
+ Binding::new("alt-h", DeleteToPreviousWordBoundary, Some("Editor")),
+ Binding::new("alt-delete", DeleteToNextWordBoundary, Some("Editor")),
+ Binding::new("alt-d", DeleteToNextWordBoundary, Some("Editor")),
+ Binding::new("cmd-backspace", DeleteToBeginningOfLine, Some("Editor")),
+ Binding::new("cmd-delete", DeleteToEndOfLine, Some("Editor")),
+ Binding::new("ctrl-k", CutToEndOfLine, Some("Editor")),
+ Binding::new("cmd-shift-D", DuplicateLine, Some("Editor")),
+ Binding::new("ctrl-cmd-up", MoveLineUp, Some("Editor")),
+ Binding::new("ctrl-cmd-down", MoveLineDown, Some("Editor")),
+ Binding::new("cmd-x", Cut, Some("Editor")),
+ Binding::new("cmd-c", Copy, Some("Editor")),
+ Binding::new("cmd-v", Paste, Some("Editor")),
+ Binding::new("cmd-z", Undo, Some("Editor")),
+ Binding::new("cmd-shift-Z", Redo, Some("Editor")),
+ Binding::new("up", MoveUp, Some("Editor")),
+ Binding::new("down", MoveDown, Some("Editor")),
+ Binding::new("left", MoveLeft, Some("Editor")),
+ Binding::new("right", MoveRight, Some("Editor")),
+ Binding::new("ctrl-p", MoveUp, Some("Editor")),
+ Binding::new("ctrl-n", MoveDown, Some("Editor")),
+ Binding::new("ctrl-b", MoveLeft, Some("Editor")),
+ Binding::new("ctrl-f", MoveRight, Some("Editor")),
+ Binding::new("alt-left", MoveToPreviousWordBoundary, Some("Editor")),
+ Binding::new("alt-b", MoveToPreviousWordBoundary, Some("Editor")),
+ Binding::new("alt-right", MoveToNextWordBoundary, Some("Editor")),
+ Binding::new("alt-f", MoveToNextWordBoundary, Some("Editor")),
+ Binding::new("cmd-left", MoveToBeginningOfLine, Some("Editor")),
+ Binding::new("ctrl-a", MoveToBeginningOfLine, Some("Editor")),
+ Binding::new("cmd-right", MoveToEndOfLine, Some("Editor")),
+ Binding::new("ctrl-e", MoveToEndOfLine, Some("Editor")),
+ Binding::new("cmd-up", MoveToBeginning, Some("Editor")),
+ Binding::new("cmd-down", MoveToEnd, Some("Editor")),
+ Binding::new("shift-up", SelectUp, Some("Editor")),
+ Binding::new("ctrl-shift-P", SelectUp, Some("Editor")),
+ Binding::new("shift-down", SelectDown, Some("Editor")),
+ Binding::new("ctrl-shift-N", SelectDown, Some("Editor")),
+ Binding::new("shift-left", SelectLeft, Some("Editor")),
+ Binding::new("ctrl-shift-B", SelectLeft, Some("Editor")),
+ Binding::new("shift-right", SelectRight, Some("Editor")),
+ Binding::new("ctrl-shift-F", SelectRight, Some("Editor")),
Binding::new(
"alt-shift-left",
- "buffer:select_to_previous_word_boundary",
- Some("BufferView"),
- ),
- Binding::new(
- "alt-shift-B",
- "buffer:select_to_previous_word_boundary",
- Some("BufferView"),
- ),
- Binding::new(
- "alt-shift-right",
- "buffer:select_to_next_word_boundary",
- Some("BufferView"),
- ),
- Binding::new(
- "alt-shift-F",
- "buffer:select_to_next_word_boundary",
- Some("BufferView"),
+ SelectToPreviousWordBoundary,
+ Some("Editor"),
),
+ Binding::new("alt-shift-B", SelectToPreviousWordBoundary, Some("Editor")),
+ Binding::new("alt-shift-right", SelectToNextWordBoundary, Some("Editor")),
+ Binding::new("alt-shift-F", SelectToNextWordBoundary, Some("Editor")),
Binding::new(
"cmd-shift-left",
- "buffer:select_to_beginning_of_line",
- Some("BufferView"),
- )
- .with_arg(true),
- Binding::new(
- "ctrl-shift-A",
- "buffer:select_to_beginning_of_line",
- Some("BufferView"),
- )
- .with_arg(true),
- Binding::new(
- "cmd-shift-right",
- "buffer:select_to_end_of_line",
- Some("BufferView"),
- ),
- Binding::new(
- "ctrl-shift-E",
- "buffer:select_to_end_of_line",
- Some("BufferView"),
- ),
- Binding::new(
- "cmd-shift-up",
- "buffer:select_to_beginning",
- Some("BufferView"),
- ),
- Binding::new("cmd-shift-down", "buffer:select_to_end", Some("BufferView")),
- Binding::new("cmd-a", "buffer:select_all", Some("BufferView")),
- Binding::new("cmd-l", "buffer:select_line", Some("BufferView")),
- Binding::new(
- "cmd-shift-L",
- "buffer:split_selection_into_lines",
- Some("BufferView"),
- ),
- Binding::new(
- "cmd-alt-up",
- "buffer:add_selection_above",
- Some("BufferView"),
- ),
- Binding::new(
- "cmd-ctrl-p",
- "buffer:add_selection_above",
- Some("BufferView"),
+ SelectToBeginningOfLine(true),
+ Some("Editor"),
),
Binding::new(
- "cmd-alt-down",
- "buffer:add_selection_below",
- Some("BufferView"),
- ),
- Binding::new(
- "cmd-ctrl-n",
- "buffer:add_selection_below",
- Some("BufferView"),
- ),
- Binding::new(
- "alt-up",
- "buffer:select_larger_syntax_node",
- Some("BufferView"),
- ),
- Binding::new(
- "ctrl-w",
- "buffer:select_larger_syntax_node",
- Some("BufferView"),
- ),
- Binding::new(
- "alt-down",
- "buffer:select_smaller_syntax_node",
- Some("BufferView"),
- ),
- Binding::new(
- "ctrl-shift-W",
- "buffer:select_smaller_syntax_node",
- Some("BufferView"),
- ),
- Binding::new(
- "ctrl-m",
- "buffer:move_to_enclosing_bracket",
- Some("BufferView"),
- ),
- Binding::new("pageup", "buffer:page_up", Some("BufferView")),
- Binding::new("pagedown", "buffer:page_down", Some("BufferView")),
- Binding::new("alt-cmd-[", "buffer:fold", Some("BufferView")),
- Binding::new("alt-cmd-]", "buffer:unfold", Some("BufferView")),
- Binding::new(
- "alt-cmd-f",
- "buffer:fold_selected_ranges",
- Some("BufferView"),
+ "ctrl-shift-A",
+ SelectToBeginningOfLine(true),
+ Some("Editor"),
),
+ Binding::new("cmd-shift-right", SelectToEndOfLine, Some("Editor")),
+ Binding::new("ctrl-shift-E", SelectToEndOfLine, Some("Editor")),
+ Binding::new("cmd-shift-up", SelectToBeginning, Some("Editor")),
+ Binding::new("cmd-shift-down", SelectToEnd, Some("Editor")),
+ Binding::new("cmd-a", SelectAll, Some("Editor")),
+ Binding::new("cmd-l", SelectLine, Some("Editor")),
+ Binding::new("cmd-shift-L", SplitSelectionIntoLines, Some("Editor")),
+ Binding::new("cmd-alt-up", AddSelectionAbove, Some("Editor")),
+ Binding::new("cmd-ctrl-p", AddSelectionAbove, Some("Editor")),
+ Binding::new("cmd-alt-down", AddSelectionBelow, Some("Editor")),
+ Binding::new("cmd-ctrl-n", AddSelectionBelow, Some("Editor")),
+ Binding::new("alt-up", SelectLargerSyntaxNode, Some("Editor")),
+ Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")),
+ Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")),
+ Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")),
+ Binding::new("ctrl-m", MoveToEnclosingBracket, Some("Editor")),
+ Binding::new("pageup", PageUp, Some("Editor")),
+ Binding::new("pagedown", PageDown, Some("Editor")),
+ Binding::new("alt-cmd-[", Fold, Some("Editor")),
+ Binding::new("alt-cmd-]", Unfold, Some("Editor")),
+ Binding::new("alt-cmd-f", FoldSelectedRanges, Some("Editor")),
]);
- cx.add_action("buffer:scroll", |this: &mut Editor, scroll_position, cx| {
- this.set_scroll_position(*scroll_position, cx)
- });
- cx.add_action("buffer:select", Editor::select);
- cx.add_action("buffer:cancel", Editor::cancel);
- cx.add_action("buffer:insert", Editor::insert);
- cx.add_action("buffer:newline", Editor::newline);
- cx.add_action("buffer:backspace", Editor::backspace);
- cx.add_action("buffer:delete", Editor::delete);
- cx.add_action("buffer:delete_line", Editor::delete_line);
- cx.add_action(
- "buffer:delete_to_previous_word_boundary",
- Editor::delete_to_previous_word_boundary,
- );
- cx.add_action(
- "buffer:delete_to_next_word_boundary",
- Editor::delete_to_next_word_boundary,
- );
- cx.add_action(
- "buffer:delete_to_beginning_of_line",
- Editor::delete_to_beginning_of_line,
- );
- cx.add_action(
- "buffer:delete_to_end_of_line",
- Editor::delete_to_end_of_line,
- );
- cx.add_action("buffer:cut_to_end_of_line", Editor::cut_to_end_of_line);
- cx.add_action("buffer:duplicate_line", Editor::duplicate_line);
- cx.add_action("buffer:move_line_up", Editor::move_line_up);
- cx.add_action("buffer:move_line_down", Editor::move_line_down);
- cx.add_action("buffer:cut", Editor::cut);
- cx.add_action("buffer:copy", Editor::copy);
- cx.add_action("buffer:paste", Editor::paste);
- cx.add_action("buffer:undo", Editor::undo);
- cx.add_action("buffer:redo", Editor::redo);
- cx.add_action("buffer:move_up", Editor::move_up);
- cx.add_action("buffer:move_down", Editor::move_down);
- cx.add_action("buffer:move_left", Editor::move_left);
- cx.add_action("buffer:move_right", Editor::move_right);
- cx.add_action(
- "buffer:move_to_previous_word_boundary",
- Editor::move_to_previous_word_boundary,
- );
- cx.add_action(
- "buffer:move_to_next_word_boundary",
- Editor::move_to_next_word_boundary,
- );
- cx.add_action(
- "buffer:move_to_beginning_of_line",
- Editor::move_to_beginning_of_line,
- );
- cx.add_action("buffer:move_to_end_of_line", Editor::move_to_end_of_line);
- cx.add_action("buffer:move_to_beginning", Editor::move_to_beginning);
- cx.add_action("buffer:move_to_end", Editor::move_to_end);
- cx.add_action("buffer:select_up", Editor::select_up);
- cx.add_action("buffer:select_down", Editor::select_down);
- cx.add_action("buffer:select_left", Editor::select_left);
- cx.add_action("buffer:select_right", Editor::select_right);
- cx.add_action(
- "buffer:select_to_previous_word_boundary",
- Editor::select_to_previous_word_boundary,
- );
- cx.add_action(
- "buffer:select_to_next_word_boundary",
- Editor::select_to_next_word_boundary,
- );
- cx.add_action(
- "buffer:select_to_beginning_of_line",
- Editor::select_to_beginning_of_line,
- );
- cx.add_action(
- "buffer:select_to_end_of_line",
- Editor::select_to_end_of_line,
- );
- cx.add_action("buffer:select_to_beginning", Editor::select_to_beginning);
- cx.add_action("buffer:select_to_end", Editor::select_to_end);
- cx.add_action("buffer:select_all", Editor::select_all);
- cx.add_action("buffer:select_line", Editor::select_line);
- cx.add_action(
- "buffer:split_selection_into_lines",
- Editor::split_selection_into_lines,
- );
- cx.add_action("buffer:add_selection_above", Editor::add_selection_above);
- cx.add_action("buffer:add_selection_below", Editor::add_selection_below);
- cx.add_action(
- "buffer:select_larger_syntax_node",
- Editor::select_larger_syntax_node,
- );
- cx.add_action(
- "buffer:select_smaller_syntax_node",
- Editor::select_smaller_syntax_node,
- );
- cx.add_action(
- "buffer:move_to_enclosing_bracket",
- Editor::move_to_enclosing_bracket,
- );
- cx.add_action("buffer:page_up", Editor::page_up);
- cx.add_action("buffer:page_down", Editor::page_down);
- cx.add_action("buffer:fold", Editor::fold);
- cx.add_action("buffer:unfold", Editor::unfold);
- cx.add_action("buffer:fold_selected_ranges", Editor::fold_selected_ranges);
+ cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx));
+ cx.add_action(Editor::select);
+ cx.add_action(Editor::cancel);
+ cx.add_action(Editor::insert);
+ cx.add_action(Editor::backspace);
+ cx.add_action(Editor::delete);
+ cx.add_action(Editor::delete_line);
+ cx.add_action(Editor::delete_to_previous_word_boundary);
+ cx.add_action(Editor::delete_to_next_word_boundary);
+ cx.add_action(Editor::delete_to_beginning_of_line);
+ cx.add_action(Editor::delete_to_end_of_line);
+ cx.add_action(Editor::cut_to_end_of_line);
+ cx.add_action(Editor::duplicate_line);
+ cx.add_action(Editor::move_line_up);
+ cx.add_action(Editor::move_line_down);
+ cx.add_action(Editor::cut);
+ cx.add_action(Editor::copy);
+ cx.add_action(Editor::paste);
+ cx.add_action(Editor::undo);
+ cx.add_action(Editor::redo);
+ cx.add_action(Editor::move_up);
+ cx.add_action(Editor::move_down);
+ cx.add_action(Editor::move_left);
+ cx.add_action(Editor::move_right);
+ cx.add_action(Editor::move_to_previous_word_boundary);
+ cx.add_action(Editor::move_to_next_word_boundary);
+ cx.add_action(Editor::move_to_beginning_of_line);
+ cx.add_action(Editor::move_to_end_of_line);
+ cx.add_action(Editor::move_to_beginning);
+ cx.add_action(Editor::move_to_end);
+ cx.add_action(Editor::select_up);
+ cx.add_action(Editor::select_down);
+ cx.add_action(Editor::select_left);
+ cx.add_action(Editor::select_right);
+ cx.add_action(Editor::select_to_previous_word_boundary);
+ cx.add_action(Editor::select_to_next_word_boundary);
+ cx.add_action(Editor::select_to_beginning_of_line);
+ cx.add_action(Editor::select_to_end_of_line);
+ cx.add_action(Editor::select_to_beginning);
+ cx.add_action(Editor::select_to_end);
+ cx.add_action(Editor::select_all);
+ cx.add_action(Editor::select_line);
+ cx.add_action(Editor::split_selection_into_lines);
+ cx.add_action(Editor::add_selection_above);
+ cx.add_action(Editor::add_selection_below);
+ cx.add_action(Editor::select_larger_syntax_node);
+ cx.add_action(Editor::select_smaller_syntax_node);
+ cx.add_action(Editor::move_to_enclosing_bracket);
+ cx.add_action(Editor::page_up);
+ cx.add_action(Editor::page_down);
+ cx.add_action(Editor::fold);
+ cx.add_action(Editor::unfold);
+ cx.add_action(Editor::fold_selected_ranges);
}
-pub enum SelectAction {
+#[derive(Clone, Debug)]
+pub enum SelectPhase {
Begin {
position: DisplayPoint,
add: bool,
@@ -369,6 +271,13 @@ pub enum SelectAction {
End,
}
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum EditorMode {
+ SingleLine,
+ AutoHeight { max_lines: usize },
+ Full,
+}
+
pub struct Editor {
handle: WeakViewHandle<Self>,
buffer: ModelHandle<Buffer>,
@@ -381,20 +290,24 @@ pub struct Editor {
scroll_position: Vector2F,
scroll_top_anchor: Anchor,
autoscroll_requested: bool,
+ build_style: Option<Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>>,
settings: watch::Receiver<Settings>,
focused: bool,
cursors_visible: bool,
blink_epoch: usize,
blinking_paused: bool,
- single_line: bool,
+ mode: EditorMode,
+ placeholder_text: Option<Arc<str>>,
}
pub struct Snapshot {
+ pub mode: EditorMode,
pub display_snapshot: DisplayMapSnapshot,
- pub gutter_visible: bool,
+ 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,
}
@@ -414,7 +327,18 @@ impl Editor {
pub fn single_line(settings: watch::Receiver<Settings>, 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);
- view.single_line = true;
+ view.mode = EditorMode::SingleLine;
+ view
+ }
+
+ pub fn auto_height(
+ max_lines: usize,
+ settings: watch::Receiver<Settings>,
+ 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);
+ view.mode = EditorMode::AutoHeight { max_lines };
view
}
@@ -425,9 +349,10 @@ impl Editor {
) -> Self {
let display_map =
cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.borrow().clone(), None, cx));
- cx.observe_model(&buffer, Self::on_buffer_changed);
- cx.subscribe_to_model(&buffer, Self::on_buffer_event);
- cx.observe_model(&display_map, Self::on_display_map_changed);
+ 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)
+ .detach();
let mut next_selection_id = 0;
let selection_set_id = buffer.update(cx, |buffer, cx| {
@@ -451,6 +376,7 @@ impl Editor {
next_selection_id,
add_selections_state: None,
select_larger_syntax_node_stack: Vec::new(),
+ build_style: None,
scroll_position: Vector2F::zero(),
scroll_top_anchor: Anchor::min(),
autoscroll_requested: false,
@@ -459,10 +385,19 @@ impl Editor {
cursors_visible: false,
blink_epoch: 0,
blinking_paused: false,
- single_line: false,
+ mode: EditorMode::Full,
+ placeholder_text: None,
}
}
+ 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()
}
@@ -475,16 +410,30 @@ impl Editor {
let settings = self.settings.borrow();
Snapshot {
+ mode: self.mode,
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
- gutter_visible: !self.single_line,
scroll_position: self.scroll_position,
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)
+ .map_or(false, |handle| handle.is_focused(cx)),
}
}
+ pub fn set_placeholder_text(
+ &mut self,
+ placeholder_text: impl Into<Arc<str>>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.placeholder_text = Some(placeholder_text.into());
+ cx.notify();
+ }
+
fn set_scroll_position(&mut self, mut scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let scroll_top_buffer_offset =
@@ -513,10 +462,15 @@ impl Editor {
line_height: f32,
cx: &mut ViewContext<Self>,
) -> bool {
+ let visible_lines = viewport_height / line_height;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut scroll_position =
compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor);
- let max_scroll_top = display_map.max_point().row().saturating_sub(1) as f32;
+ let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
+ (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
+ } else {
+ display_map.max_point().row().saturating_sub(1) as f32
+ };
if scroll_position.y() > max_scroll_top {
scroll_position.set_y(max_scroll_top);
self.set_scroll_position(scroll_position, cx);
@@ -528,7 +482,6 @@ impl Editor {
return false;
}
- let visible_lines = viewport_height / line_height;
let first_cursor_top = self
.selections(cx)
.first()
@@ -545,9 +498,13 @@ impl Editor {
.row() as f32
+ 1.0;
- let margin = ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0)
- .floor()
- .min(3.0);
+ let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
+ 0.
+ } else {
+ ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0)
+ .floor()
+ .min(3.0)
+ };
if margin < 0.0 {
return false;
}
@@ -612,14 +569,14 @@ impl Editor {
}
}
- fn select(&mut self, arg: &SelectAction, cx: &mut ViewContext<Self>) {
- match arg {
- SelectAction::Begin { position, add } => self.begin_selection(*position, *add, cx),
- SelectAction::Update {
+ fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext<Self>) {
+ match phase {
+ SelectPhase::Begin { position, add } => self.begin_selection(*position, *add, cx),
+ SelectPhase::Update {
position,
scroll_position,
} => self.update_selection(*position, *scroll_position, cx),
- SelectAction::End => self.end_selection(cx),
+ SelectPhase::End => self.end_selection(cx),
}
}
@@ -682,7 +639,7 @@ impl Editor {
self.pending_selection.is_some()
}
- pub fn cancel(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
let selections = self.selections(cx.as_ref());
if let Some(pending_selection) = self.pending_selection.take() {
if selections.is_empty() {
@@ -754,7 +711,7 @@ impl Editor {
Ok(())
}
- pub fn insert(&mut self, text: &String, cx: &mut ViewContext<Self>) {
+ pub fn insert(&mut self, action: &Insert, cx: &mut ViewContext<Self>) {
let mut old_selections = SmallVec::<[_; 32]>::new();
{
let buffer = self.buffer.read(cx);
@@ -769,8 +726,8 @@ impl Editor {
let mut new_selections = Vec::new();
self.buffer.update(cx, |buffer, cx| {
let edit_ranges = old_selections.iter().map(|(_, range)| range.clone());
- buffer.edit(edit_ranges, text.as_str(), cx);
- let text_len = text.len() as isize;
+ buffer.edit(edit_ranges, action.0.as_str(), cx);
+ let text_len = action.0.len() as isize;
let mut delta = 0_isize;
new_selections = old_selections
.into_iter()
@@ -795,15 +752,14 @@ impl Editor {
self.end_transaction(cx);
}
- fn newline(&mut self, _: &(), cx: &mut ViewContext<Self>) {
- if self.single_line {
- cx.propagate_action();
- } else {
- self.insert(&"\n".into(), cx);
- }
+ pub fn clear(&mut self, cx: &mut ViewContext<Self>) {
+ self.start_transaction(cx);
+ self.select_all(&SelectAll, cx);
+ self.insert(&Insert(String::new()), cx);
+ self.end_transaction(cx);
}
- pub fn backspace(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
self.start_transaction(cx);
let mut selections = self.selections(cx.as_ref()).to_vec();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -822,11 +778,11 @@ impl Editor {
}
self.update_selections(selections, true, cx);
- self.insert(&String::new(), cx);
+ self.insert(&Insert(String::new()), cx);
self.end_transaction(cx);
}
- pub fn delete(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
self.start_transaction(cx);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut selections = self.selections(cx.as_ref()).to_vec();
@@ -845,11 +801,11 @@ impl Editor {
}
self.update_selections(selections, true, cx);
- self.insert(&String::new(), cx);
+ self.insert(&Insert(String::new()), cx);
self.end_transaction(cx);
}
- pub fn delete_line(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext<Self>) {
self.start_transaction(cx);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -926,7 +882,7 @@ impl Editor {
self.end_transaction(cx);
}
- pub fn duplicate_line(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
self.start_transaction(cx);
let mut selections = self.selections(cx.as_ref()).to_vec();
@@ -986,7 +942,7 @@ impl Editor {
self.end_transaction(cx);
}
- pub fn move_line_up(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn move_line_up(&mut self, _: &MoveLineUp, cx: &mut ViewContext<Self>) {
self.start_transaction(cx);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -1076,7 +1032,7 @@ impl Editor {
self.end_transaction(cx);
}
- pub fn move_line_down(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn move_line_down(&mut self, _: &MoveLineDown, cx: &mut ViewContext<Self>) {
self.start_transaction(cx);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -1163,7 +1119,7 @@ impl Editor {
self.end_transaction(cx);
}
- pub fn cut(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
self.start_transaction(cx);
let mut text = String::new();
let mut selections = self.selections(cx.as_ref()).to_vec();
@@ -1193,14 +1149,14 @@ impl Editor {
}
}
self.update_selections(selections, true, cx);
- self.insert(&String::new(), cx);
+ self.insert(&Insert(String::new()), cx);
self.end_transaction(cx);
cx.as_mut()
.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
}
- pub fn copy(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
let buffer = self.buffer.read(cx);
let max_point = buffer.max_point();
let mut text = String::new();
@@ -1229,7 +1185,7 @@ impl Editor {
.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
}
- pub fn paste(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
if let Some(item) = cx.as_mut().read_from_clipboard() {
let clipboard_text = item.text();
if let Some(mut clipboard_selections) = item.metadata::<Vec<ClipboardSelection>>() {
@@ -1281,20 +1237,20 @@ impl Editor {
self.update_selections(new_selections, true, cx);
self.end_transaction(cx);
} else {
- self.insert(clipboard_text, cx);
+ self.insert(&Insert(clipboard_text.into()), cx);
}
}
}
- pub fn undo(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
self.buffer.update(cx, |buffer, cx| buffer.undo(cx));
}
- pub fn redo(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext<Self>) {
self.buffer.update(cx, |buffer, cx| buffer.redo(cx));
}
- pub fn move_left(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let app = cx.as_ref();
let mut selections = self.selections(app).to_vec();
@@ -1318,7 +1274,7 @@ impl Editor {
self.update_selections(selections, true, cx);
}
- pub fn select_left(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut selections = self.selections(cx.as_ref()).to_vec();
{
@@ -1334,7 +1290,7 @@ impl Editor {
self.update_selections(selections, true, cx);
}
- pub fn move_right(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut selections = self.selections(cx.as_ref()).to_vec();
{
@@ -1357,7 +1313,7 @@ impl Editor {
self.update_selections(selections, true, cx);
}
- pub fn select_right(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut selections = self.selections(cx.as_ref()).to_vec();
{
@@ -1374,9 +1330,9 @@ impl Editor {
self.update_selections(selections, true, cx);
}
- pub fn move_up(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- if self.single_line {
+ if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
} else {
let mut selections = self.selections(cx.as_ref()).to_vec();
@@ -1400,7 +1356,7 @@ impl Editor {
}
}
- pub fn select_up(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut selections = self.selections(cx.as_ref()).to_vec();
{
@@ -1416,8 +1372,8 @@ impl Editor {
self.update_selections(selections, true, cx);
}
- pub fn move_down(&mut self, _: &(), cx: &mut ViewContext<Self>) {
- if self.single_line {
+ pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
+ if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
} else {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -1442,7 +1398,7 @@ impl Editor {
}
}
- pub fn select_down(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut selections = self.selections(cx).to_vec();
{
@@ -1458,7 +1414,11 @@ impl Editor {
self.update_selections(selections, true, cx);
}
- pub fn move_to_previous_word_boundary(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn move_to_previous_word_boundary(
+ &mut self,
+ _: &MoveToPreviousWordBoundary,
+ cx: &mut ViewContext<Self>,
+ ) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut selections = self.selections(cx).to_vec();
{
@@ -1,30 +1,30 @@
mod anchor;
+mod operation_queue;
mod point;
pub mod rope;
mod selection;
-pub use anchor::*;
-use parking_lot::Mutex;
-pub use point::*;
-pub use rope::{Chunks, Rope, TextSummary};
-use seahash::SeaHasher;
-pub use selection::*;
-use similar::{ChangeTag, TextDiff};
-use tree_sitter::{InputEdit, Parser, QueryCursor};
-use zrpc::proto;
-
use crate::{
language::{Language, Tree},
- operation_queue::{self, OperationQueue},
settings::{HighlightId, HighlightMap},
- sum_tree::{self, FilterCursor, SumTree},
time::{self, ReplicaId},
util::Bias,
worktree::{File, Worktree},
};
+pub use anchor::*;
use anyhow::{anyhow, Result};
-use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
+use gpui::{
+ sum_tree::{self, FilterCursor, SumTree},
+ AppContext, Entity, ModelContext, ModelHandle, Task,
+};
use lazy_static::lazy_static;
+use operation_queue::OperationQueue;
+use parking_lot::Mutex;
+pub use point::*;
+pub use rope::{Chunks, Rope, TextSummary};
+use seahash::SeaHasher;
+pub use selection::*;
+use similar::{ChangeTag, TextDiff};
use std::{
cell::RefCell,
cmp,
@@ -37,6 +37,8 @@ use std::{
sync::Arc,
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
+use tree_sitter::{InputEdit, Parser, QueryCursor};
+use zrpc::proto;
#[derive(Clone, Default)]
struct DeterministicState;
@@ -118,9 +120,10 @@ pub struct Buffer {
file: Option<File>,
language: Option<Arc<Language>>,
syntax_tree: Mutex<Option<SyntaxTree>>,
- is_parsing: bool,
+ parsing_in_background: bool,
+ parse_count: usize,
selections: HashMap<SelectionSetId, SelectionSet>,
- deferred_ops: OperationQueue<Operation>,
+ deferred_ops: OperationQueue,
deferred_replicas: HashSet<ReplicaId>,
replica_id: ReplicaId,
remote_id: u64,
@@ -139,7 +142,7 @@ pub struct SelectionSet {
#[derive(Clone)]
struct SyntaxTree {
tree: Tree,
- parsed: bool,
+ dirty: bool,
version: time::Global,
}
@@ -483,6 +486,8 @@ pub enum Operation {
set_id: Option<SelectionSetId>,
lamport_timestamp: time::Lamport,
},
+ #[cfg(test)]
+ Test(time::Lamport),
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -576,7 +581,8 @@ impl Buffer {
history,
file,
syntax_tree: Mutex::new(None),
- is_parsing: false,
+ parsing_in_background: false,
+ parse_count: 0,
language,
saved_mtime,
selections: HashMap::default(),
@@ -604,7 +610,7 @@ impl Buffer {
fragments: self.fragments.clone(),
version: self.version.clone(),
tree: self.syntax_tree(),
- is_parsing: self.is_parsing,
+ is_parsing: self.parsing_in_background,
language: self.language.clone(),
query_cursor: QueryCursorHandle::new(),
}
@@ -786,9 +792,12 @@ impl Buffer {
cx.emit(Event::FileHandleChanged);
}
+ pub fn parse_count(&self) -> usize {
+ self.parse_count
+ }
+
pub fn syntax_tree(&self) -> Option<Tree> {
if let Some(syntax_tree) = self.syntax_tree.lock().as_mut() {
- let mut edited = false;
let mut delta = 0_isize;
for edit in self.edits_since(syntax_tree.version.clone()) {
let start_offset = (edit.old_bytes.start as isize + delta) as usize;
@@ -805,9 +814,8 @@ impl Buffer {
.into(),
});
delta += edit.inserted_bytes() as isize - edit.deleted_bytes() as isize;
- edited = true;
+ syntax_tree.dirty = true;
}
- syntax_tree.parsed &= !edited;
syntax_tree.version = self.version();
Some(syntax_tree.tree.clone())
} else {
@@ -815,58 +823,69 @@ impl Buffer {
}
}
+ #[cfg(test)]
pub fn is_parsing(&self) -> bool {
- self.is_parsing
- }
-
- fn should_reparse(&self) -> bool {
- if let Some(syntax_tree) = self.syntax_tree.lock().as_ref() {
- !syntax_tree.parsed || syntax_tree.version != self.version
- } else {
- self.language.is_some()
- }
+ self.parsing_in_background
}
- fn reparse(&mut self, cx: &mut ModelContext<Self>) {
- // Avoid spawning a new parsing task if the buffer is already being reparsed
- // due to an earlier edit.
- if self.is_parsing {
- return;
+ fn reparse(&mut self, cx: &mut ModelContext<Self>) -> bool {
+ if self.parsing_in_background {
+ return false;
}
if let Some(language) = self.language.clone() {
- self.is_parsing = true;
- cx.spawn(|handle, mut cx| async move {
- while handle.read_with(&cx, |this, _| this.should_reparse()) {
- // The parse tree is out of date, so grab the syntax tree to synchronously
- // splice all the edits that have happened since the last parse.
- let new_tree = handle.update(&mut cx, |this, _| this.syntax_tree());
- let (new_text, new_version) = handle
- .read_with(&cx, |this, _| (this.visible_text.clone(), this.version()));
-
- // Parse the current text in a background thread.
- let new_tree = cx
- .background()
- .spawn({
- let language = language.clone();
- async move { Self::parse_text(&new_text, new_tree, &language) }
- })
- .await;
+ // The parse tree is out of date, so grab the syntax tree to synchronously
+ // splice all the edits that have happened since the last parse.
+ let old_tree = self.syntax_tree();
+ let parsed_text = self.visible_text.clone();
+ let parsed_version = self.version();
+ let parse_task = cx.background().spawn({
+ let language = language.clone();
+ async move { Self::parse_text(&parsed_text, old_tree, &language) }
+ });
- handle.update(&mut cx, |this, cx| {
- *this.syntax_tree.lock() = Some(SyntaxTree {
- tree: new_tree,
- parsed: true,
- version: new_version,
- });
- cx.emit(Event::Reparsed);
- cx.notify();
+ match cx
+ .background()
+ .block_with_timeout(Duration::from_millis(1), parse_task)
+ {
+ Ok(new_tree) => {
+ *self.syntax_tree.lock() = Some(SyntaxTree {
+ tree: new_tree,
+ dirty: false,
+ version: parsed_version,
});
+ self.parse_count += 1;
+ cx.emit(Event::Reparsed);
+ cx.notify();
+ return true;
}
- handle.update(&mut cx, |this, _| this.is_parsing = false);
- })
- .detach();
+ Err(parse_task) => {
+ self.parsing_in_background = true;
+ 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;
+ *this.syntax_tree.lock() = Some(SyntaxTree {
+ tree: new_tree,
+ dirty: false,
+ version: parsed_version,
+ });
+ this.parse_count += 1;
+ this.parsing_in_background = false;
+
+ if parse_again && this.reparse(cx) {
+ return;
+ }
+
+ cx.emit(Event::Reparsed);
+ cx.notify();
+ });
+ })
+ .detach();
+ }
+ }
}
+ false
}
fn parse_text(text: &Rope, old_tree: Option<Tree>, language: &Language) -> Tree {
@@ -1390,6 +1409,8 @@ impl Buffer {
}
self.lamport_clock.observe(lamport_timestamp);
}
+ #[cfg(test)]
+ Operation::Test(_) => {}
}
Ok(())
}
@@ -1731,6 +1752,8 @@ impl Buffer {
Operation::SetActiveSelections { set_id, .. } => {
set_id.map_or(true, |set_id| self.selections.contains_key(&set_id))
}
+ #[cfg(test)]
+ Operation::Test(_) => true,
}
}
}
@@ -1907,7 +1930,8 @@ impl Clone for Buffer {
file: self.file.clone(),
language: self.language.clone(),
syntax_tree: Mutex::new(self.syntax_tree.lock().clone()),
- is_parsing: false,
+ parsing_in_background: false,
+ parse_count: self.parse_count,
deferred_replicas: self.deferred_replicas.clone(),
replica_id: self.replica_id,
remote_id: self.remote_id.clone(),
@@ -2564,6 +2588,8 @@ impl Operation {
Operation::SetActiveSelections {
lamport_timestamp, ..
} => *lamport_timestamp,
+ #[cfg(test)]
+ Operation::Test(lamport_timestamp) => *lamport_timestamp,
}
}
@@ -2630,6 +2656,8 @@ impl<'a> Into<proto::Operation> for &'a Operation {
lamport_timestamp: lamport_timestamp.value,
},
),
+ #[cfg(test)]
+ Operation::Test(_) => unimplemented!(),
}),
}
}
@@ -2834,12 +2862,6 @@ impl TryFrom<proto::Selection> for Selection {
}
}
-impl operation_queue::Operation for Operation {
- fn timestamp(&self) -> time::Lamport {
- self.lamport_timestamp()
- }
-}
-
pub trait ToOffset {
fn to_offset<'a>(&self, content: impl Into<Content<'a>>) -> usize;
}
@@ -2889,7 +2911,8 @@ mod tests {
use super::*;
use crate::{
fs::RealFs,
- test::{build_app_state, temp_tree},
+ language::LanguageRegistry,
+ test::temp_tree,
util::RandomCharIter,
worktree::{Worktree, WorktreeHandle as _},
};
@@ -2934,13 +2957,15 @@ mod tests {
let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx));
let buffer_ops = buffer1.update(cx, |buffer, cx| {
let buffer_1_events = buffer_1_events.clone();
- cx.subscribe(&buffer1, move |_, event, _| {
+ cx.subscribe(&buffer1, move |_, _, event, _| {
buffer_1_events.borrow_mut().push(event.clone())
- });
+ })
+ .detach();
let buffer_2_events = buffer_2_events.clone();
- cx.subscribe(&buffer2, move |_, event, _| {
+ cx.subscribe(&buffer2, move |_, _, event, _| {
buffer_2_events.borrow_mut().push(event.clone())
- });
+ })
+ .detach();
// An edit emits an edited event, followed by a dirtied event,
// since the buffer was previously in a clean state.
@@ -3374,8 +3399,9 @@ mod tests {
buffer1.update(&mut cx, |buffer, cx| {
cx.subscribe(&buffer1, {
let events = events.clone();
- move |_, event, _| events.borrow_mut().push(event.clone())
- });
+ move |_, _, event, _| events.borrow_mut().push(event.clone())
+ })
+ .detach();
assert!(!buffer.is_dirty());
assert!(events.borrow().is_empty());
@@ -3430,8 +3456,9 @@ mod tests {
buffer2.update(&mut cx, |_, cx| {
cx.subscribe(&buffer2, {
let events = events.clone();
- move |_, event, _| events.borrow_mut().push(event.clone())
- });
+ move |_, _, event, _| events.borrow_mut().push(event.clone())
+ })
+ .detach();
});
fs::remove_file(dir.path().join("file2")).unwrap();
@@ -3450,8 +3477,9 @@ mod tests {
buffer3.update(&mut cx, |_, cx| {
cx.subscribe(&buffer3, {
let events = events.clone();
- move |_, event, _| events.borrow_mut().push(event.clone())
- });
+ move |_, _, event, _| events.borrow_mut().push(event.clone())
+ })
+ .detach();
});
tree.flush_fs_events(&cx).await;
@@ -3819,8 +3847,8 @@ mod tests {
#[gpui::test]
async fn test_reparse(mut cx: gpui::TestAppContext) {
- let app_state = cx.read(build_app_state);
- let rust_lang = app_state.languages.select_language("test.rs");
+ let languages = LanguageRegistry::new();
+ let rust_lang = languages.select_language("test.rs");
assert!(rust_lang.is_some());
let buffer = cx.add_model(|cx| {
@@ -3960,8 +3988,8 @@ mod tests {
async fn test_enclosing_bracket_ranges(mut cx: gpui::TestAppContext) {
use unindent::Unindent as _;
- let app_state = cx.read(build_app_state);
- let rust_lang = app_state.languages.select_language("test.rs");
+ let languages = LanguageRegistry::new();
+ let rust_lang = languages.select_language("test.rs");
assert!(rust_lang.is_some());
let buffer = cx.add_model(|cx| {
@@ -1,26 +1,27 @@
-use crate::{
- sum_tree::{Cursor, Dimension, Edit, Item, KeyedItem, SumTree, Summary},
- time,
-};
+use super::Operation;
+use crate::time;
+use gpui::sum_tree::{Cursor, Dimension, Edit, Item, KeyedItem, SumTree, Summary};
use std::{fmt::Debug, ops::Add};
-pub trait Operation: Clone + Debug + Eq {
- fn timestamp(&self) -> time::Lamport;
-}
-
#[derive(Clone, Debug)]
-pub struct OperationQueue<T: Operation>(SumTree<T>);
+pub struct OperationQueue(SumTree<Operation>);
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
pub struct OperationKey(time::Lamport);
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct OperationSummary {
- key: OperationKey,
- len: usize,
+ pub key: OperationKey,
+ pub len: usize,
+}
+
+impl OperationKey {
+ pub fn new(timestamp: time::Lamport) -> Self {
+ Self(timestamp)
+ }
}
-impl<T: Operation> OperationQueue<T> {
+impl OperationQueue {
pub fn new() -> Self {
OperationQueue(SumTree::new())
}
@@ -29,9 +30,9 @@ impl<T: Operation> OperationQueue<T> {
self.0.summary().len
}
- pub fn insert(&mut self, mut ops: Vec<T>) {
- ops.sort_by_key(|op| op.timestamp());
- ops.dedup_by_key(|op| op.timestamp());
+ pub fn insert(&mut self, mut ops: Vec<Operation>) {
+ ops.sort_by_key(|op| op.lamport_timestamp());
+ ops.dedup_by_key(|op| op.lamport_timestamp());
self.0
.edit(ops.into_iter().map(Edit::Insert).collect(), &());
}
@@ -42,30 +43,11 @@ impl<T: Operation> OperationQueue<T> {
clone
}
- pub fn cursor(&self) -> Cursor<T, (), ()> {
+ pub fn cursor(&self) -> Cursor<Operation, (), ()> {
self.0.cursor()
}
}
-impl<T: Operation> Item for T {
- type Summary = OperationSummary;
-
- fn summary(&self) -> Self::Summary {
- OperationSummary {
- key: OperationKey(self.timestamp()),
- len: 1,
- }
- }
-}
-
-impl<T: Operation> KeyedItem for T {
- type Key = OperationKey;
-
- fn key(&self) -> Self::Key {
- OperationKey(self.timestamp())
- }
-}
-
impl Summary for OperationSummary {
type Context = ();
@@ -95,6 +77,25 @@ impl<'a> Dimension<'a, OperationSummary> for OperationKey {
}
}
+impl Item for Operation {
+ type Summary = OperationSummary;
+
+ fn summary(&self) -> Self::Summary {
+ OperationSummary {
+ key: OperationKey::new(self.lamport_timestamp()),
+ len: 1,
+ }
+ }
+}
+
+impl KeyedItem for Operation {
+ type Key = OperationKey;
+
+ fn key(&self) -> Self::Key {
+ OperationKey::new(self.lamport_timestamp())
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -107,27 +108,21 @@ mod tests {
assert_eq!(queue.len(), 0);
queue.insert(vec![
- TestOperation(clock.tick()),
- TestOperation(clock.tick()),
+ Operation::Test(clock.tick()),
+ Operation::Test(clock.tick()),
]);
assert_eq!(queue.len(), 2);
- queue.insert(vec![TestOperation(clock.tick())]);
+ queue.insert(vec![Operation::Test(clock.tick())]);
assert_eq!(queue.len(), 3);
drop(queue.drain());
assert_eq!(queue.len(), 0);
- queue.insert(vec![TestOperation(clock.tick())]);
+ queue.insert(vec![Operation::Test(clock.tick())]);
assert_eq!(queue.len(), 1);
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct TestOperation(time::Lamport);
-
- impl Operation for TestOperation {
- fn timestamp(&self) -> time::Lamport {
- self.0
- }
- }
}
@@ -1,9 +1,7 @@
use super::Point;
-use crate::{
- sum_tree::{self, SumTree},
- util::Bias,
-};
+use crate::util::Bias;
use arrayvec::ArrayString;
+use gpui::sum_tree::{self, SumTree};
use smallvec::SmallVec;
use std::{cmp, ops::Range, str};
@@ -61,7 +59,7 @@ impl Rope {
if last_chunk.0.len() + first_new_chunk_ref.0.len() <= 2 * CHUNK_BASE {
last_chunk.0.push_str(&first_new_chunk.take().unwrap().0);
} else {
- let mut text = ArrayString::<[_; 4 * CHUNK_BASE]>::new();
+ let mut text = ArrayString::<{ 4 * CHUNK_BASE }>::new();
text.push_str(&last_chunk.0);
text.push_str(&first_new_chunk_ref.0);
let (left, right) = text.split_at(find_split_ix(&text));
@@ -330,7 +328,7 @@ impl<'a> Iterator for Chunks<'a> {
}
#[derive(Clone, Debug, Default)]
-struct Chunk(ArrayString<[u8; 2 * CHUNK_BASE]>);
+struct Chunk(ArrayString<{ 2 * CHUNK_BASE }>);
impl Chunk {
fn to_point(&self, target: usize) -> Point {
@@ -1,5 +1,4 @@
mod fold_map;
-mod line_wrapper;
mod tab_map;
mod wrap_map;
@@ -32,7 +31,7 @@ impl DisplayMap {
let (fold_map, snapshot) = FoldMap::new(buffer.clone(), cx);
let (tab_map, snapshot) = TabMap::new(snapshot, settings.tab_size);
let wrap_map = cx.add_model(|cx| WrapMap::new(snapshot, settings, wrap_width, cx));
- cx.observe(&wrap_map, |_, _, cx| cx.notify());
+ cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
DisplayMap {
buffer,
fold_map,
@@ -109,6 +108,10 @@ impl DisplayMapSnapshot {
self.folds_snapshot.fold_count()
}
+ pub fn is_empty(&self) -> bool {
+ self.buffer_snapshot.len() == 0
+ }
+
pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
self.wraps_snapshot.buffer_rows(start_row)
}
@@ -344,8 +347,8 @@ mod tests {
use crate::{
editor::movement,
language::{Language, LanguageConfig},
- settings::Theme,
test::*,
+ theme::SyntaxTheme,
util::RandomCharIter,
};
use buffer::{History, SelectionGoal};
@@ -367,7 +370,7 @@ mod tests {
tab_size: rng.gen_range(1..=4),
buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
buffer_font_size: 14.0,
- ..Settings::new(&font_cache).unwrap()
+ ..cx.read(Settings::test)
};
let max_wrap_width = 300.0;
let mut wrap_width = if rng.gen_bool(0.1) {
@@ -532,11 +535,9 @@ mod tests {
let settings = Settings {
buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
- ui_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
buffer_font_size: 12.0,
- ui_font_size: 12.0,
tab_size: 4,
- theme: Arc::new(Theme::default()),
+ ..cx.read(Settings::test)
};
let wrap_width = Some(64.);
@@ -607,7 +608,10 @@ mod tests {
let map = cx.add_model(|cx| {
DisplayMap::new(
buffer.clone(),
- Settings::new(cx.font_cache()).unwrap().with_tab_size(4),
+ Settings {
+ tab_size: 4,
+ ..Settings::test(cx)
+ },
None,
cx,
)
@@ -661,13 +665,10 @@ mod tests {
(function_item name: (identifier) @fn.name)"#,
)
.unwrap();
- let theme = Theme {
- syntax: vec![
- ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
- ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
- ],
- ..Default::default()
- };
+ let theme = SyntaxTheme::new(vec![
+ ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
+ ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
+ ]);
let lang = Arc::new(Language {
config: LanguageConfig {
name: "Test".to_string(),
@@ -689,7 +690,10 @@ mod tests {
let map = cx.add_model(|cx| {
DisplayMap::new(
buffer,
- Settings::new(cx.font_cache()).unwrap().with_tab_size(2),
+ Settings {
+ tab_size: 2,
+ ..Settings::test(cx)
+ },
None,
cx,
)
@@ -751,13 +755,10 @@ mod tests {
(function_item name: (identifier) @fn.name)"#,
)
.unwrap();
- let theme = Theme {
- syntax: vec![
- ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
- ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
- ],
- ..Default::default()
- };
+ let theme = SyntaxTheme::new(vec![
+ ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
+ ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
+ ]);
let lang = Arc::new(Language {
config: LanguageConfig {
name: "Test".to_string(),
@@ -781,7 +782,7 @@ mod tests {
tab_size: 4,
buffer_font_family: font_cache.load_family(&["Courier"]).unwrap(),
buffer_font_size: 16.0,
- ..Settings::new(&font_cache).unwrap()
+ ..cx.read(Settings::test)
};
let map = cx.add_model(|cx| DisplayMap::new(buffer, settings, Some(40.0), cx));
assert_eq!(
@@ -821,7 +822,10 @@ mod tests {
let map = cx.add_model(|cx| {
DisplayMap::new(
buffer.clone(),
- Settings::new(cx.font_cache()).unwrap().with_tab_size(4),
+ Settings {
+ tab_size: 4,
+ ..Settings::test(cx)
+ },
None,
cx,
)
@@ -862,7 +866,10 @@ mod tests {
let map = cx.add_model(|cx| {
DisplayMap::new(
buffer.clone(),
- Settings::new(cx.font_cache()).unwrap().with_tab_size(4),
+ Settings {
+ tab_size: 4,
+ ..Settings::test(cx)
+ },
None,
cx,
)
@@ -926,7 +933,10 @@ mod tests {
let map = cx.add_model(|cx| {
DisplayMap::new(
buffer.clone(),
- Settings::new(cx.font_cache()).unwrap().with_tab_size(4),
+ Settings {
+ tab_size: 4,
+ ..Settings::test(cx)
+ },
None,
cx,
)
@@ -940,7 +950,7 @@ mod tests {
fn highlighted_chunks<'a>(
rows: Range<u32>,
map: &ModelHandle<DisplayMap>,
- theme: &'a Theme,
+ theme: &'a SyntaxTheme,
cx: &mut MutableAppContext,
) -> Vec<(String, Option<&'a str>)> {
let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx));
@@ -2,14 +2,11 @@ use super::{
buffer::{AnchorRangeExt, TextSummary},
Anchor, Buffer, Point, ToOffset,
};
-use crate::{
- editor::buffer,
- settings::HighlightId,
+use crate::{editor::buffer, settings::HighlightId, time, util::Bias};
+use gpui::{
sum_tree::{self, Cursor, FilterCursor, SumTree},
- time,
- util::Bias,
+ AppContext, ModelHandle,
};
-use gpui::{AppContext, ModelHandle};
use parking_lot::Mutex;
use std::{
cmp::{self, Ordering},
@@ -204,7 +201,7 @@ pub struct FoldMap {
#[derive(Clone)]
struct SyncState {
version: time::Global,
- is_parsing: bool,
+ parse_count: usize,
}
impl FoldMap {
@@ -225,7 +222,7 @@ impl FoldMap {
)),
last_sync: Mutex::new(SyncState {
version: buffer.version(),
- is_parsing: buffer.is_parsing(),
+ parse_count: buffer.parse_count(),
}),
version: AtomicUsize::new(0),
};
@@ -256,7 +253,7 @@ impl FoldMap {
&mut *self.last_sync.lock(),
SyncState {
version: buffer.version(),
- is_parsing: buffer.is_parsing(),
+ parse_count: buffer.parse_count(),
},
);
let edits = buffer
@@ -264,7 +261,7 @@ impl FoldMap {
.map(Into::into)
.collect::<Vec<_>>();
if edits.is_empty() {
- if last_sync.is_parsing != buffer.is_parsing() {
+ if last_sync.parse_count != buffer.parse_count() {
self.version.fetch_add(1, SeqCst);
}
Vec::new()
@@ -1,265 +0,0 @@
-use crate::Settings;
-use gpui::{fonts::FontId, FontCache, FontSystem};
-use std::{
- cell::RefCell,
- collections::HashMap,
- iter,
- ops::{Deref, DerefMut},
- sync::Arc,
-};
-
-thread_local! {
- static WRAPPERS: RefCell<Vec<LineWrapper>> = Default::default();
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub struct Boundary {
- pub ix: usize,
- pub next_indent: u32,
-}
-
-impl Boundary {
- fn new(ix: usize, next_indent: u32) -> Self {
- Self { ix, next_indent }
- }
-}
-
-pub struct LineWrapper {
- font_system: Arc<dyn FontSystem>,
- font_id: FontId,
- font_size: f32,
- cached_ascii_char_widths: [f32; 128],
- cached_other_char_widths: HashMap<char, f32>,
-}
-
-impl LineWrapper {
- pub const MAX_INDENT: u32 = 256;
-
- pub fn thread_local(
- font_system: Arc<dyn FontSystem>,
- font_cache: &FontCache,
- settings: Settings,
- ) -> LineWrapperHandle {
- let wrapper =
- if let Some(mut wrapper) = WRAPPERS.with(|wrappers| wrappers.borrow_mut().pop()) {
- let font_id = font_cache
- .select_font(settings.buffer_font_family, &Default::default())
- .unwrap();
- let font_size = settings.buffer_font_size;
- if wrapper.font_id != font_id || wrapper.font_size != font_size {
- wrapper.cached_ascii_char_widths = [f32::NAN; 128];
- wrapper.cached_other_char_widths.clear();
- }
- wrapper
- } else {
- LineWrapper::new(font_system, font_cache, settings)
- };
- LineWrapperHandle(Some(wrapper))
- }
-
- pub fn new(
- font_system: Arc<dyn FontSystem>,
- font_cache: &FontCache,
- settings: Settings,
- ) -> Self {
- let font_id = font_cache
- .select_font(settings.buffer_font_family, &Default::default())
- .unwrap();
- let font_size = settings.buffer_font_size;
- Self {
- font_system,
- font_id,
- font_size,
- cached_ascii_char_widths: [f32::NAN; 128],
- cached_other_char_widths: HashMap::new(),
- }
- }
-
- pub fn wrap_line<'a>(
- &'a mut self,
- line: &'a str,
- wrap_width: f32,
- ) -> impl Iterator<Item = Boundary> + 'a {
- let mut width = 0.0;
- let mut first_non_whitespace_ix = None;
- let mut indent = None;
- let mut last_candidate_ix = 0;
- let mut last_candidate_width = 0.0;
- let mut last_wrap_ix = 0;
- let mut prev_c = '\0';
- let mut char_indices = line.char_indices();
- iter::from_fn(move || {
- while let Some((ix, c)) = char_indices.next() {
- if c == '\n' {
- continue;
- }
-
- if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() {
- last_candidate_ix = ix;
- last_candidate_width = width;
- }
-
- if c != ' ' && first_non_whitespace_ix.is_none() {
- first_non_whitespace_ix = Some(ix);
- }
-
- let char_width = self.width_for_char(c);
- width += char_width;
- if width > wrap_width && ix > last_wrap_ix {
- if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
- {
- indent = Some(
- Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
- );
- }
-
- if last_candidate_ix > 0 {
- last_wrap_ix = last_candidate_ix;
- width -= last_candidate_width;
- last_candidate_ix = 0;
- } else {
- last_wrap_ix = ix;
- width = char_width;
- }
-
- let indent_width =
- indent.map(|indent| indent as f32 * self.width_for_char(' '));
- width += indent_width.unwrap_or(0.);
-
- return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
- }
- prev_c = c;
- }
-
- None
- })
- }
-
- fn is_boundary(&self, prev: char, next: char) -> bool {
- (prev == ' ') && (next != ' ')
- }
-
- #[inline(always)]
- fn width_for_char(&mut self, c: char) -> f32 {
- if (c as u32) < 128 {
- let mut width = self.cached_ascii_char_widths[c as usize];
- if width.is_nan() {
- width = self.compute_width_for_char(c);
- self.cached_ascii_char_widths[c as usize] = width;
- }
- width
- } else {
- let mut width = self
- .cached_other_char_widths
- .get(&c)
- .copied()
- .unwrap_or(f32::NAN);
- if width.is_nan() {
- width = self.compute_width_for_char(c);
- self.cached_other_char_widths.insert(c, width);
- }
- width
- }
- }
-
- fn compute_width_for_char(&self, c: char) -> f32 {
- self.font_system
- .layout_line(
- &c.to_string(),
- self.font_size,
- &[(1, self.font_id, Default::default())],
- )
- .width
- }
-}
-
-pub struct LineWrapperHandle(Option<LineWrapper>);
-
-impl Drop for LineWrapperHandle {
- fn drop(&mut self) {
- let wrapper = self.0.take().unwrap();
- WRAPPERS.with(|wrappers| wrappers.borrow_mut().push(wrapper))
- }
-}
-
-impl Deref for LineWrapperHandle {
- type Target = LineWrapper;
-
- fn deref(&self) -> &Self::Target {
- self.0.as_ref().unwrap()
- }
-}
-
-impl DerefMut for LineWrapperHandle {
- fn deref_mut(&mut self) -> &mut Self::Target {
- self.0.as_mut().unwrap()
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[gpui::test]
- fn test_line_wrapper(cx: &mut gpui::MutableAppContext) {
- let font_cache = cx.font_cache().clone();
- let font_system = cx.platform().fonts();
- let settings = Settings {
- tab_size: 4,
- buffer_font_family: font_cache.load_family(&["Courier"]).unwrap(),
- buffer_font_size: 16.0,
- ..Settings::new(&font_cache).unwrap()
- };
-
- let mut wrapper = LineWrapper::new(font_system, &font_cache, settings);
- assert_eq!(
- wrapper
- .wrap_line("aa bbb cccc ddddd eeee", 72.0)
- .collect::<Vec<_>>(),
- &[
- Boundary::new(7, 0),
- Boundary::new(12, 0),
- Boundary::new(18, 0)
- ],
- );
- assert_eq!(
- wrapper
- .wrap_line("aaa aaaaaaaaaaaaaaaaaa", 72.0)
- .collect::<Vec<_>>(),
- &[
- Boundary::new(4, 0),
- Boundary::new(11, 0),
- Boundary::new(18, 0)
- ],
- );
- assert_eq!(
- wrapper.wrap_line(" aaaaaaa", 72.).collect::<Vec<_>>(),
- &[
- Boundary::new(7, 5),
- Boundary::new(9, 5),
- Boundary::new(11, 5),
- ]
- );
- assert_eq!(
- wrapper
- .wrap_line(" ", 72.)
- .collect::<Vec<_>>(),
- &[
- Boundary::new(7, 0),
- Boundary::new(14, 0),
- Boundary::new(21, 0)
- ]
- );
- assert_eq!(
- wrapper
- .wrap_line(" aaaaaaaaaaaaaa", 72.)
- .collect::<Vec<_>>(),
- &[
- Boundary::new(7, 0),
- Boundary::new(14, 3),
- Boundary::new(18, 3),
- Boundary::new(22, 3),
- ]
- );
- }
-}
@@ -1,16 +1,13 @@
use super::{
fold_map,
- line_wrapper::LineWrapper,
tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint, TextSummary},
};
-use crate::{
- editor::Point,
- settings::HighlightId,
+use crate::{editor::Point, settings::HighlightId, util::Bias, Settings};
+use gpui::{
sum_tree::{self, Cursor, SumTree},
- util::Bias,
- Settings,
+ text_layout::LineWrapper,
+ Entity, ModelContext, Task,
};
-use gpui::{Entity, ModelContext, Task};
use lazy_static::lazy_static;
use smol::future::yield_now;
use std::{collections::VecDeque, ops::Range, time::Duration};
@@ -118,12 +115,13 @@ impl WrapMap {
if let Some(wrap_width) = wrap_width {
let mut new_snapshot = self.snapshot.clone();
- let font_system = cx.platform().fonts();
let font_cache = cx.font_cache().clone();
let settings = self.settings.clone();
let task = cx.background().spawn(async move {
- let mut line_wrapper =
- LineWrapper::thread_local(font_system, &font_cache, settings);
+ let font_id = font_cache
+ .select_font(settings.buffer_font_family, &Default::default())
+ .unwrap();
+ let mut line_wrapper = font_cache.line_wrapper(font_id, settings.buffer_font_size);
let tab_snapshot = new_snapshot.tab_snapshot.clone();
let range = TabPoint::zero()..tab_snapshot.max_point();
new_snapshot
@@ -193,12 +191,14 @@ impl WrapMap {
if self.background_task.is_none() {
let pending_edits = self.pending_edits.clone();
let mut snapshot = self.snapshot.clone();
- let font_system = cx.platform().fonts();
let font_cache = cx.font_cache().clone();
let settings = self.settings.clone();
let update_task = cx.background().spawn(async move {
+ let font_id = font_cache
+ .select_font(settings.buffer_font_family, &Default::default())
+ .unwrap();
let mut line_wrapper =
- LineWrapper::thread_local(font_system, &font_cache, settings);
+ font_cache.line_wrapper(font_id, settings.buffer_font_size);
for (tab_snapshot, edits) in pending_edits {
snapshot
.update(tab_snapshot, &edits, wrap_width, &mut line_wrapper)
@@ -816,8 +816,12 @@ fn push_isomorphic(transforms: &mut Vec<Transform>, summary: TextSummary) {
transforms.push(Transform::isomorphic(summary));
}
-impl SumTree<Transform> {
- pub fn push_or_extend(&mut self, transform: Transform) {
+trait SumTreeExt {
+ fn push_or_extend(&mut self, transform: Transform);
+}
+
+impl SumTreeExt for SumTree<Transform> {
+ fn push_or_extend(&mut self, transform: Transform) {
let mut transform = Some(transform);
self.update_last(
|last_transform| {
@@ -917,7 +921,7 @@ mod tests {
tab_size: rng.gen_range(1..=4),
buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
buffer_font_size: 14.0,
- ..Settings::new(&font_cache).unwrap()
+ ..cx.read(Settings::test)
};
log::info!("Tab size: {}", settings.tab_size);
log::info!("Wrap width: {:?}", wrap_width);
@@ -942,7 +946,10 @@ mod tests {
.add_model(|cx| WrapMap::new(tabs_snapshot.clone(), settings.clone(), wrap_width, cx));
let (_observer, notifications) = Observer::new(&wrap_map, &mut cx);
- let mut line_wrapper = LineWrapper::new(font_system, &font_cache, settings);
+ 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 unwrapped_text = tabs_snapshot.text();
let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
@@ -1,5 +1,5 @@
-use super::{DisplayPoint, Editor, SelectAction, Snapshot};
-use crate::time::ReplicaId;
+use super::{DisplayPoint, Editor, EditorMode, Insert, Scroll, Select, SelectPhase, Snapshot};
+use crate::{theme::EditorStyle, time::ReplicaId};
use gpui::{
color::Color,
geometry::{
@@ -9,7 +9,7 @@ use gpui::{
},
json::{self, ToJson},
text_layout::{self, TextLayoutCache},
- AfterLayoutContext, AppContext, Border, Element, Event, EventContext, FontCache, LayoutContext,
+ AppContext, Axis, Border, Element, Event, EventContext, FontCache, LayoutContext,
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
};
use json::json;
@@ -22,11 +22,12 @@ use std::{
pub struct EditorElement {
view: WeakViewHandle<Editor>,
+ style: EditorStyle,
}
impl EditorElement {
- pub fn new(view: WeakViewHandle<Editor>) -> Self {
- Self { view }
+ pub fn new(view: WeakViewHandle<Editor>, style: EditorStyle) -> Self {
+ Self { view, style }
}
fn view<'a>(&self, cx: &'a AppContext) -> &'a Editor {
@@ -55,7 +56,7 @@ impl EditorElement {
if paint.text_bounds.contains_point(position) {
let snapshot = self.snapshot(cx.app);
let position = paint.point_for_position(&snapshot, layout, position);
- cx.dispatch_action("buffer:select", SelectAction::Begin { position, add: cmd });
+ cx.dispatch_action(Select(SelectPhase::Begin { position, add: cmd }));
true
} else {
false
@@ -64,7 +65,7 @@ impl EditorElement {
fn mouse_up(&self, _position: Vector2F, cx: &mut EventContext) -> bool {
if self.view(cx.app.as_ref()).is_selecting() {
- cx.dispatch_action("buffer:select", SelectAction::End);
+ cx.dispatch_action(Select(SelectPhase::End));
true
} else {
false
@@ -113,16 +114,13 @@ impl EditorElement {
let snapshot = self.snapshot(cx.app);
let position = paint.point_for_position(&snapshot, layout, position);
- cx.dispatch_action(
- "buffer:select",
- SelectAction::Update {
- position,
- scroll_position: (snapshot.scroll_position() + scroll_delta).clamp(
- Vector2F::zero(),
- layout.scroll_max(&font_cache, &text_layout_cache),
- ),
- },
- );
+ cx.dispatch_action(Select(SelectPhase::Update {
+ position,
+ scroll_position: (snapshot.scroll_position() + scroll_delta).clamp(
+ Vector2F::zero(),
+ layout.scroll_max(&font_cache, &text_layout_cache),
+ ),
+ }));
true
} else {
false
@@ -139,7 +137,7 @@ impl EditorElement {
if chars.chars().any(|c| c.is_control()) {
false
} else {
- cx.dispatch_action("buffer:insert", chars.to_string());
+ cx.dispatch_action(Insert(chars.to_string()));
true
}
}
@@ -177,7 +175,7 @@ impl EditorElement {
layout.scroll_max(font_cache, layout_cache),
);
- cx.dispatch_action("buffer:scroll", scroll_position);
+ cx.dispatch_action(Scroll(scroll_position));
true
}
@@ -192,22 +190,20 @@ impl EditorElement {
let bounds = gutter_bounds.union_rect(text_bounds);
let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height;
let editor = self.view(cx.app);
- let settings = editor.settings.borrow();
- let theme = &settings.theme;
cx.scene.push_quad(Quad {
bounds: gutter_bounds,
- background: Some(theme.editor.gutter_background),
+ background: Some(self.style.gutter_background),
border: Border::new(0., Color::transparent_black()),
corner_radius: 0.,
});
cx.scene.push_quad(Quad {
bounds: text_bounds,
- background: Some(theme.editor.background),
+ background: Some(self.style.background),
border: Border::new(0., Color::transparent_black()),
corner_radius: 0.,
});
- if !editor.single_line {
+ if let EditorMode::Full = editor.mode {
let mut active_rows = layout.active_rows.iter().peekable();
while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
let mut end_row = *start_row;
@@ -229,7 +225,7 @@ impl EditorElement {
);
cx.scene.push_quad(Quad {
bounds: RectF::new(origin, size),
- background: Some(theme.editor.active_line_background),
+ background: Some(self.style.active_line_background),
border: Border::default(),
corner_radius: 0.,
});
@@ -238,25 +234,33 @@ impl EditorElement {
}
}
- fn paint_gutter(&mut self, rect: RectF, layout: &LayoutState, cx: &mut PaintContext) {
+ fn paint_gutter(
+ &mut self,
+ bounds: RectF,
+ visible_bounds: RectF,
+ layout: &LayoutState,
+ cx: &mut PaintContext,
+ ) {
let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height;
for (ix, line) in layout.line_number_layouts.iter().enumerate() {
if let Some(line) = line {
- let line_origin = rect.origin()
+ let line_origin = bounds.origin()
+ vec2f(
- rect.width() - line.width() - layout.gutter_padding,
+ bounds.width() - line.width() - layout.gutter_padding,
ix as f32 * layout.line_height - (scroll_top % layout.line_height),
);
- line.paint(
- line_origin,
- RectF::new(vec2f(0., 0.), vec2f(line.width(), layout.line_height)),
- cx,
- );
+ line.paint(line_origin, visible_bounds, layout.line_height, cx);
}
}
}
- fn paint_text(&mut self, bounds: RectF, layout: &LayoutState, cx: &mut PaintContext) {
+ fn paint_text(
+ &mut self,
+ bounds: RectF,
+ visible_bounds: RectF,
+ layout: &LayoutState,
+ cx: &mut PaintContext,
+ ) {
let view = self.view(cx.app);
let settings = self.view(cx.app).settings.borrow();
let theme = &settings.theme.editor;
@@ -276,7 +280,12 @@ impl EditorElement {
let content_origin = bounds.origin() + layout.text_offset;
for (replica_id, selections) in &layout.selections {
- let replica_theme = theme.replicas[*replica_id as usize % theme.replicas.len()];
+ let style_ix = *replica_id as usize % (theme.guest_selections.len() + 1);
+ let style = if style_ix == 0 {
+ &theme.selection
+ } else {
+ &theme.guest_selections[style_ix - 1]
+ };
for selection in selections {
if selection.start != selection.end {
@@ -290,7 +299,7 @@ impl EditorElement {
};
let selection = Selection {
- color: replica_theme.selection,
+ color: style.selection,
line_height: layout.line_height,
start_y: content_origin.y() + row_range.start as f32 * layout.line_height
- scroll_top,
@@ -333,7 +342,7 @@ impl EditorElement {
- scroll_left;
let y = selection.end.row() as f32 * layout.line_height - scroll_top;
cursors.push(Cursor {
- color: replica_theme.cursor,
+ color: style.cursor,
origin: content_origin + vec2f(x, y),
line_height: layout.line_height,
});
@@ -342,17 +351,18 @@ impl EditorElement {
}
}
- // Draw glyphs
- for (ix, line) in layout.line_layouts.iter().enumerate() {
- let row = start_row + ix as u32;
- line.paint(
- content_origin + vec2f(-scroll_left, row as f32 * layout.line_height - scroll_top),
- RectF::new(
- vec2f(scroll_left, 0.),
- vec2f(bounds.width(), layout.line_height),
- ),
- cx,
- );
+ if let Some(visible_text_bounds) = bounds.intersection(visible_bounds) {
+ // Draw glyphs
+ for (ix, line) in layout.line_layouts.iter().enumerate() {
+ let row = start_row + ix as u32;
+ line.paint(
+ content_origin
+ + vec2f(-scroll_left, row as f32 * layout.line_height - scroll_top),
+ visible_text_bounds,
+ layout.line_height,
+ cx,
+ );
+ }
}
cx.scene.push_layer(Some(bounds));
@@ -386,7 +396,7 @@ impl Element for EditorElement {
let gutter_padding;
let gutter_width;
- if snapshot.gutter_visible {
+ 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) => {
@@ -412,8 +422,17 @@ impl Element for EditorElement {
snapshot
}
});
- if size.y().is_infinite() {
- size.set_y((snapshot.max_point().row() + 1) as f32 * line_height);
+
+ let scroll_height = (snapshot.max_point().row() + 1) as f32 * line_height;
+ if let EditorMode::AutoHeight { max_lines } = snapshot.mode {
+ size.set_y(
+ scroll_height
+ .min(constraint.max_along(Axis::Vertical))
+ .max(constraint.min_along(Axis::Vertical))
+ .min(line_height * max_lines as f32),
+ )
+ } else if size.y().is_infinite() {
+ size.set_y(scroll_height);
}
let gutter_size = vec2f(gutter_width, size.y());
let text_size = vec2f(text_width, size.y());
@@ -432,7 +451,6 @@ impl Element for EditorElement {
let mut selections = HashMap::new();
let mut active_rows = BTreeMap::new();
self.update_view(cx.app, |view, cx| {
- let replica_id = view.replica_id(cx);
for selection_set_id in view.active_selection_sets(cx).collect::<Vec<_>>() {
let mut set = Vec::new();
for selection in view.selections_in_range(
@@ -441,7 +459,7 @@ impl Element for EditorElement {
cx,
) {
set.push(selection.clone());
- if selection_set_id.replica_id == replica_id {
+ if selection_set_id == view.selection_set_id {
let is_empty = selection.start == selection.end;
let mut selection_start;
let mut selection_end;
@@ -468,7 +486,7 @@ impl Element for EditorElement {
}
});
- let line_number_layouts = if snapshot.gutter_visible {
+ let line_number_layouts = if snapshot.mode == EditorMode::Full {
let settings = self
.view
.upgrade(cx.app)
@@ -494,8 +512,12 @@ impl Element for EditorElement {
};
let mut max_visible_line_width = 0.0;
- let line_layouts = match snapshot.layout_lines(start_row..end_row, font_cache, layout_cache)
- {
+ 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);
@@ -552,12 +574,10 @@ impl Element for EditorElement {
(size, Some(layout))
}
- fn after_layout(&mut self, _: Vector2F, _: &mut Self::LayoutState, _: &mut AfterLayoutContext) {
- }
-
fn paint(
&mut self,
bounds: RectF,
+ visible_bounds: RectF,
layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
@@ -572,9 +592,9 @@ impl Element for EditorElement {
self.paint_background(gutter_bounds, text_bounds, layout, cx);
if layout.gutter_size.x() > 0. {
- self.paint_gutter(gutter_bounds, layout, cx);
+ self.paint_gutter(gutter_bounds, visible_bounds, layout, cx);
}
- self.paint_text(text_bounds, layout, cx);
+ self.paint_text(text_bounds, visible_bounds, layout, cx);
cx.scene.pop_layer();
@@ -182,12 +182,12 @@ mod tests {
use super::*;
use crate::{
editor::{display_map::DisplayMap, Buffer},
- test::build_app_state,
+ test::test_app_state,
};
#[gpui::test]
fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
- let settings = build_app_state(cx).settings.borrow().clone();
+ let settings = test_app_state(cx).settings.borrow().clone();
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 snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -6,8 +6,13 @@ use crate::{
worktree::{match_paths, PathMatch},
};
use gpui::{
+ action,
elements::*,
- keymap::{self, Binding},
+ keymap::{
+ self,
+ menu::{SelectNext, SelectPrev},
+ Binding,
+ },
AppContext, Axis, Entity, MutableAppContext, RenderContext, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
};
@@ -25,7 +30,7 @@ pub struct FileFinder {
handle: WeakViewHandle<Self>,
settings: watch::Receiver<Settings>,
workspace: WeakViewHandle<Workspace>,
- query_buffer: ViewHandle<Editor>,
+ query_editor: ViewHandle<Editor>,
search_count: usize,
latest_search_id: usize,
latest_search_did_cancel: bool,
@@ -36,17 +41,27 @@ pub struct FileFinder {
list_state: UniformListState,
}
+action!(Toggle);
+action!(Confirm);
+action!(Select, Entry);
+
+#[derive(Clone)]
+pub struct Entry {
+ worktree_id: usize,
+ path: Arc<Path>,
+}
+
pub fn init(cx: &mut MutableAppContext) {
- cx.add_action("file_finder:toggle", FileFinder::toggle);
- cx.add_action("file_finder:confirm", FileFinder::confirm);
- cx.add_action("file_finder:select", FileFinder::select);
- cx.add_action("menu:select_prev", FileFinder::select_prev);
- cx.add_action("menu:select_next", FileFinder::select_next);
+ cx.add_action(FileFinder::toggle);
+ cx.add_action(FileFinder::confirm);
+ cx.add_action(FileFinder::select);
+ cx.add_action(FileFinder::select_prev);
+ cx.add_action(FileFinder::select_next);
cx.add_bindings(vec![
- Binding::new("cmd-p", "file_finder:toggle", None),
- Binding::new("escape", "file_finder:toggle", Some("FileFinder")),
- Binding::new("enter", "file_finder:confirm", Some("FileFinder")),
+ Binding::new("cmd-p", Toggle, None),
+ Binding::new("escape", Toggle, Some("FileFinder")),
+ Binding::new("enter", Confirm, Some("FileFinder")),
]);
}
@@ -64,18 +79,18 @@ impl View for FileFinder {
"FileFinder"
}
- fn render(&self, _: &RenderContext<Self>) -> ElementBox {
+ fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
let settings = self.settings.borrow();
Align::new(
ConstrainedBox::new(
Container::new(
Flex::new(Axis::Vertical)
- .with_child(ChildView::new(self.query_buffer.id()).boxed())
- .with_child(Expanded::new(1.0, self.render_matches()).boxed())
+ .with_child(ChildView::new(self.query_editor.id()).boxed())
+ .with_child(Flexible::new(1.0, self.render_matches()).boxed())
.boxed(),
)
- .with_style(&settings.theme.ui.selector.container)
+ .with_style(&settings.theme.selector.container)
.boxed(),
)
.with_max_width(600.0)
@@ -87,7 +102,7 @@ impl View for FileFinder {
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
- cx.focus(&self.query_buffer);
+ cx.focus(&self.query_editor);
}
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
@@ -102,13 +117,7 @@ impl FileFinder {
if self.matches.is_empty() {
let settings = self.settings.borrow();
return Container::new(
- Label::new(
- "No matches".into(),
- settings.ui_font_family,
- settings.ui_font_size,
- )
- .with_style(&settings.theme.ui.selector.label)
- .boxed(),
+ Label::new("No matches".into(), settings.theme.selector.label.clone()).boxed(),
)
.with_margin_top(6.0)
.named("empty matches");
@@ -142,9 +151,9 @@ impl FileFinder {
let selected_index = self.selected_index();
let settings = self.settings.borrow();
let style = if index == selected_index {
- &settings.theme.ui.selector.active_item
+ &settings.theme.selector.active_item
} else {
- &settings.theme.ui.selector.item
+ &settings.theme.selector.item
};
let (file_name, file_name_positions, full_path, full_path_positions) =
self.labels_for_match(path_match);
@@ -153,11 +162,10 @@ impl FileFinder {
.with_child(
Container::new(
LineBox::new(
- settings.ui_font_family,
- settings.ui_font_size,
Svg::new("icons/file-16.svg")
.with_color(style.label.text.color)
.boxed(),
+ style.label.text.clone(),
)
.boxed(),
)
@@ -165,28 +173,18 @@ impl FileFinder {
.boxed(),
)
.with_child(
- Expanded::new(
+ Flexible::new(
1.0,
Flex::column()
.with_child(
- Label::new(
- file_name.to_string(),
- settings.ui_font_family,
- settings.ui_font_size,
- )
- .with_style(&style.label)
- .with_highlights(file_name_positions)
- .boxed(),
+ Label::new(file_name.to_string(), style.label.clone())
+ .with_highlights(file_name_positions)
+ .boxed(),
)
.with_child(
- Label::new(
- full_path,
- settings.ui_font_family,
- settings.ui_font_size,
- )
- .with_style(&style.label)
- .with_highlights(full_path_positions)
- .boxed(),
+ Label::new(full_path, style.label.clone())
+ .with_highlights(full_path_positions)
+ .boxed(),
)
.boxed(),
)
@@ -196,10 +194,13 @@ impl FileFinder {
)
.with_style(&style.container);
- let entry = (path_match.tree_id, path_match.path.clone());
+ let action = Select(Entry {
+ worktree_id: path_match.tree_id,
+ path: path_match.path.clone(),
+ });
EventHandler::new(container.boxed())
.on_mouse_down(move |cx| {
- cx.dispatch_action("file_finder:select", entry.clone());
+ cx.dispatch_action(action.clone());
true
})
.named("match")
@@ -230,11 +231,11 @@ impl FileFinder {
(file_name, file_name_positions, full_path, path_positions)
}
- fn toggle(workspace: &mut Workspace, _: &(), cx: &mut ViewContext<Workspace>) {
+ fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |cx, workspace| {
let handle = cx.handle();
let finder = cx.add_view(|cx| Self::new(workspace.settings.clone(), handle, cx));
- cx.subscribe_to_view(&finder, Self::on_event);
+ cx.subscribe(&finder, Self::on_event).detach();
finder
});
}
@@ -263,16 +264,22 @@ impl FileFinder {
workspace: ViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
- cx.observe_view(&workspace, Self::workspace_updated);
+ cx.observe(&workspace, Self::workspace_updated).detach();
- let query_buffer = cx.add_view(|cx| Editor::single_line(settings.clone(), cx));
- cx.subscribe_to_view(&query_buffer, Self::on_query_editor_event);
+ 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()
+ })
+ });
+ cx.subscribe(&query_editor, Self::on_query_editor_event)
+ .detach();
Self {
handle: cx.handle().downgrade(),
settings,
workspace: workspace.downgrade(),
- query_buffer,
+ query_editor,
search_count: 0,
latest_search_id: 0,
latest_search_did_cancel: false,
@@ -285,7 +292,7 @@ impl FileFinder {
}
fn workspace_updated(&mut self, _: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) {
- let query = self.query_buffer.update(cx, |buffer, cx| buffer.text(cx));
+ let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
if let Some(task) = self.spawn_search(query, cx) {
task.detach();
}
@@ -299,7 +306,7 @@ impl FileFinder {
) {
match event {
editor::Event::Edited => {
- let query = self.query_buffer.update(cx, |buffer, cx| buffer.text(cx));
+ let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
if query.is_empty() {
self.latest_search_id = util::post_inc(&mut self.search_count);
self.matches.clear();
@@ -328,7 +335,7 @@ impl FileFinder {
0
}
- fn select_prev(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
let mut selected_index = self.selected_index();
if selected_index > 0 {
selected_index -= 1;
@@ -339,7 +346,7 @@ impl FileFinder {
cx.notify();
}
- fn select_next(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
let mut selected_index = self.selected_index();
if selected_index + 1 < self.matches.len() {
selected_index += 1;
@@ -350,14 +357,14 @@ impl FileFinder {
cx.notify();
}
- fn confirm(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some(m) = self.matches.get(self.selected_index()) {
cx.emit(Event::Selected(m.tree_id, m.path.clone()));
}
}
- fn select(&mut self, (tree_id, path): &(usize, Arc<Path>), cx: &mut ViewContext<Self>) {
- cx.emit(Event::Selected(*tree_id, path.clone()));
+ fn select(&mut self, Select(entry): &Select, cx: &mut ViewContext<Self>) {
+ cx.emit(Event::Selected(entry.worktree_id, entry.path.clone()));
}
#[must_use]
@@ -417,9 +424,9 @@ impl FileFinder {
mod tests {
use super::*;
use crate::{
- editor,
+ editor::{self, Insert},
fs::FakeFs,
- test::{build_app_state, temp_tree},
+ test::{temp_tree, test_app_state},
workspace::Workspace,
};
use serde_json::json;
@@ -437,7 +444,7 @@ mod tests {
editor::init(cx);
});
- let app_state = cx.read(build_app_state);
+ let app_state = cx.update(test_app_state);
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
workspace
.update(&mut cx, |workspace, cx| {
@@ -447,12 +454,7 @@ mod tests {
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
- cx.dispatch_action(
- window_id,
- vec![workspace.id()],
- "file_finder:toggle".into(),
- (),
- );
+ cx.dispatch_action(window_id, vec![workspace.id()], Toggle);
let finder = cx.read(|cx| {
workspace
@@ -463,29 +465,19 @@ mod tests {
.downcast::<FileFinder>()
.unwrap()
});
- let query_buffer = cx.read(|cx| finder.read(cx).query_buffer.clone());
+ let query_buffer = cx.read(|cx| finder.read(cx).query_editor.clone());
let chain = vec![finder.id(), query_buffer.id()];
- cx.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string());
- cx.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string());
- cx.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string());
+ cx.dispatch_action(window_id, chain.clone(), Insert("b".into()));
+ cx.dispatch_action(window_id, chain.clone(), Insert("n".into()));
+ cx.dispatch_action(window_id, chain.clone(), Insert("a".into()));
finder
.condition(&cx, |finder, _| finder.matches.len() == 2)
.await;
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
- cx.dispatch_action(
- window_id,
- vec![workspace.id(), finder.id()],
- "menu:select_next",
- (),
- );
- cx.dispatch_action(
- window_id,
- vec![workspace.id(), finder.id()],
- "file_finder:confirm",
- (),
- );
+ cx.dispatch_action(window_id, vec![workspace.id(), finder.id()], SelectNext);
+ cx.dispatch_action(window_id, vec![workspace.id(), finder.id()], Confirm);
active_pane
.condition(&cx, |pane, _| pane.active_item().is_some())
.await;
@@ -512,7 +504,7 @@ mod tests {
)
.await;
- let mut app_state = cx.read(build_app_state);
+ let mut app_state = cx.update(test_app_state);
Arc::get_mut(&mut app_state).unwrap().fs = fs;
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
@@ -574,7 +566,7 @@ mod tests {
fs::create_dir(&dir_path).unwrap();
fs::write(&file_path, "").unwrap();
- let app_state = cx.read(build_app_state);
+ let app_state = cx.update(test_app_state);
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
workspace
.update(&mut cx, |workspace, cx| {
@@ -621,7 +613,7 @@ mod tests {
"dir2": { "a.txt": "" }
}));
- let app_state = cx.read(build_app_state);
+ let app_state = cx.update(test_app_state);
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
workspace
@@ -648,9 +640,9 @@ mod tests {
finder.update(&mut cx, |f, cx| {
assert_eq!(f.matches.len(), 2);
assert_eq!(f.selected_index(), 0);
- f.select_next(&(), cx);
+ f.select_next(&SelectNext, cx);
assert_eq!(f.selected_index(), 1);
- f.select_prev(&(), cx);
+ f.select_prev(&SelectPrev, cx);
assert_eq!(f.selected_index(), 0);
});
}
@@ -1,4 +1,4 @@
-use crate::settings::{HighlightMap, Theme};
+use crate::{settings::HighlightMap, theme::SyntaxTheme};
use parking_lot::Mutex;
use rust_embed::RustEmbed;
use serde::Deserialize;
@@ -39,7 +39,7 @@ impl Language {
self.highlight_map.lock().clone()
}
- pub fn set_theme(&self, theme: &Theme) {
+ pub fn set_theme(&self, theme: &SyntaxTheme) {
*self.highlight_map.lock() = HighlightMap::new(self.highlight_query.capture_names(), theme);
}
}
@@ -47,7 +47,8 @@ impl Language {
impl LanguageRegistry {
pub fn new() -> Self {
let grammar = tree_sitter_rust::language();
- let rust_config = toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap()).unwrap();
+ let rust_config =
+ toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap().data).unwrap();
let rust_language = Language {
config: rust_config,
grammar,
@@ -61,7 +62,7 @@ impl LanguageRegistry {
}
}
- pub fn set_theme(&self, theme: &Theme) {
+ pub fn set_theme(&self, theme: &SyntaxTheme) {
for language in &self.languages {
language.set_theme(theme);
}
@@ -84,7 +85,7 @@ impl LanguageRegistry {
fn load_query(grammar: tree_sitter::Language, path: &str) -> Query {
Query::new(
grammar,
- str::from_utf8(LanguageDir::get(path).unwrap().as_ref()).unwrap(),
+ str::from_utf8(&LanguageDir::get(path).unwrap().data).unwrap(),
)
.unwrap()
}
@@ -1,44 +1,61 @@
pub mod assets;
+pub mod channel;
+pub mod chat_panel;
pub mod editor;
pub mod file_finder;
pub mod fs;
mod fuzzy;
pub mod language;
pub mod menus;
-mod operation_queue;
+pub mod project_browser;
pub mod rpc;
pub mod settings;
-mod sum_tree;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
pub mod theme;
pub mod theme_selector;
mod time;
+pub mod user;
mod util;
pub mod workspace;
pub mod worktree;
-pub use settings::Settings;
-
+use crate::util::TryFutureExt;
+use channel::ChannelList;
+use gpui::{action, ModelHandle};
use parking_lot::Mutex;
use postage::watch;
use std::sync::Arc;
-use zrpc::ForegroundRouter;
+
+pub use settings::Settings;
+
+action!(About);
+action!(Quit);
+action!(Authenticate);
pub struct AppState {
pub settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
pub settings: watch::Receiver<Settings>,
pub languages: Arc<language::LanguageRegistry>,
pub themes: Arc<settings::ThemeRegistry>,
- pub rpc_router: Arc<ForegroundRouter>,
- pub rpc: rpc::Client,
+ pub rpc: Arc<rpc::Client>,
pub fs: Arc<dyn fs::Fs>,
+ pub channel_list: ModelHandle<ChannelList>,
}
-pub fn init(cx: &mut gpui::MutableAppContext) {
- cx.add_global_action("app:quit", quit);
+pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
+ cx.add_global_action(quit);
+
+ cx.add_global_action({
+ let rpc = app_state.rpc.clone();
+ move |_: &Authenticate, cx| {
+ let rpc = rpc.clone();
+ cx.spawn(|cx| async move { rpc.authenticate_and_connect(&cx).log_err().await })
+ .detach();
+ }
+ });
}
-fn quit(_: &(), cx: &mut gpui::MutableAppContext) {
+fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
cx.platform().quit();
}
@@ -2,54 +2,58 @@
#![allow(non_snake_case)]
use fs::OpenOptions;
+use gpui::AssetSource;
use log::LevelFilter;
use parking_lot::Mutex;
use simplelog::SimpleLogger;
use std::{fs, path::PathBuf, sync::Arc};
use zed::{
- self, assets, editor, file_finder,
+ self,
+ assets::Assets,
+ channel::ChannelList,
+ chat_panel, editor, file_finder,
fs::RealFs,
language, menus, rpc, settings, theme_selector,
- workspace::{self, OpenParams},
- worktree::{self},
+ user::UserStore,
+ workspace::{self, OpenNew, OpenParams, OpenPaths},
AppState,
};
-use zrpc::ForegroundRouter;
fn main() {
init_logger();
- let app = gpui::App::new(assets::Assets).unwrap();
+ let app = gpui::App::new(Assets).unwrap();
+ let embedded_fonts = Assets
+ .list("fonts")
+ .into_iter()
+ .map(|f| Arc::new(Assets.load(&f).unwrap().to_vec()))
+ .collect::<Vec<_>>();
+ app.platform().fonts().add_fonts(&embedded_fonts).unwrap();
- let themes = settings::ThemeRegistry::new(assets::Assets);
- let (settings_tx, settings) =
- settings::channel_with_themes(&app.font_cache(), &themes).unwrap();
+ let themes = settings::ThemeRegistry::new(Assets, app.font_cache());
+ let (settings_tx, settings) = settings::channel(&app.font_cache(), &themes).unwrap();
let languages = Arc::new(language::LanguageRegistry::new());
- languages.set_theme(&settings.borrow().theme);
-
- let mut app_state = AppState {
- languages: languages.clone(),
- settings_tx: Arc::new(Mutex::new(settings_tx)),
- settings,
- themes,
- rpc_router: Arc::new(ForegroundRouter::new()),
- rpc: rpc::Client::new(languages),
- fs: Arc::new(RealFs),
- };
+ languages.set_theme(&settings.borrow().theme.syntax);
app.run(move |cx| {
- worktree::init(
- cx,
- &app_state.rpc,
- Arc::get_mut(&mut app_state.rpc_router).unwrap(),
- );
- let app_state = Arc::new(app_state);
+ let rpc = rpc::Client::new();
+ let user_store = Arc::new(UserStore::new(rpc.clone()));
+ 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)),
+ rpc,
+ fs: Arc::new(RealFs),
+ });
- zed::init(cx);
+ zed::init(&app_state, cx);
workspace::init(cx);
editor::init(cx);
file_finder::init(cx);
- theme_selector::init(cx, &app_state);
+ chat_panel::init(cx);
+ theme_selector::init(&app_state, cx);
cx.set_menus(menus::menus(&app_state.clone()));
@@ -58,14 +62,10 @@ fn main() {
}
let paths = collect_path_args();
- if !paths.is_empty() {
- cx.dispatch_global_action(
- "workspace:open_paths",
- OpenParams {
- paths,
- app_state: app_state.clone(),
- },
- );
+ if paths.is_empty() {
+ cx.dispatch_global_action(OpenNew(app_state));
+ } else {
+ cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state }));
}
});
}
@@ -1,9 +1,11 @@
-use crate::AppState;
+use crate::{workspace, AppState};
use gpui::{Menu, MenuItem};
use std::sync::Arc;
#[cfg(target_os = "macos")]
pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
+ use crate::editor;
+
vec![
Menu {
name: "Zed",
@@ -11,27 +13,28 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
MenuItem::Action {
name: "About Zed…",
keystroke: None,
- action: "app:about-zed",
- arg: None,
+ action: Box::new(super::About),
},
MenuItem::Separator,
+ MenuItem::Action {
+ name: "Sign In",
+ keystroke: None,
+ action: Box::new(super::Authenticate),
+ },
MenuItem::Action {
name: "Share",
keystroke: None,
- action: "workspace:share_worktree",
- arg: Some(Box::new(state.clone())),
+ action: Box::new(workspace::ShareWorktree),
},
MenuItem::Action {
name: "Join",
keystroke: None,
- action: "workspace:join_worktree",
- arg: Some(Box::new(state.clone())),
+ action: Box::new(workspace::JoinWorktree(state.clone())),
},
MenuItem::Action {
name: "Quit",
keystroke: Some("cmd-q"),
- action: "app:quit",
- arg: None,
+ action: Box::new(super::Quit),
},
],
},
@@ -41,15 +44,13 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
MenuItem::Action {
name: "New",
keystroke: Some("cmd-n"),
- action: "workspace:new_file",
- arg: Some(Box::new(state.clone())),
+ action: Box::new(workspace::OpenNew(state.clone())),
},
MenuItem::Separator,
MenuItem::Action {
name: "Open…",
keystroke: Some("cmd-o"),
- action: "workspace:open",
- arg: Some(Box::new(state.clone())),
+ action: Box::new(workspace::Open(state.clone())),
},
],
},
@@ -59,33 +60,28 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
MenuItem::Action {
name: "Undo",
keystroke: Some("cmd-z"),
- action: "buffer:undo",
- arg: None,
+ action: Box::new(editor::Undo),
},
MenuItem::Action {
name: "Redo",
keystroke: Some("cmd-Z"),
- action: "buffer:redo",
- arg: None,
+ action: Box::new(editor::Redo),
},
MenuItem::Separator,
MenuItem::Action {
name: "Cut",
keystroke: Some("cmd-x"),
- action: "buffer:cut",
- arg: None,
+ action: Box::new(editor::Cut),
},
MenuItem::Action {
name: "Copy",
keystroke: Some("cmd-c"),
- action: "buffer:copy",
- arg: None,
+ action: Box::new(editor::Copy),
},
MenuItem::Action {
name: "Paste",
keystroke: Some("cmd-v"),
- action: "buffer:paste",
- arg: None,
+ action: Box::new(editor::Paste),
},
],
},
@@ -0,0 +1,19 @@
+use gpui::{elements::Empty, Element, Entity, View};
+
+pub struct ProjectBrowser;
+
+pub enum Event {}
+
+impl Entity for ProjectBrowser {
+ type Event = Event;
+}
+
+impl View for ProjectBrowser {
+ fn ui_name() -> &'static str {
+ "ProjectBrowser"
+ }
+
+ fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
+ Empty::new().boxed()
+ }
+}
@@ -1,18 +1,24 @@
-use crate::{language::LanguageRegistry, worktree::Worktree};
+use crate::util::ResultExt;
use anyhow::{anyhow, Context, Result};
use async_tungstenite::tungstenite::http::Request;
use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage};
-use gpui::{AsyncAppContext, ModelHandle, Task, WeakModelHandle};
+use gpui::{AsyncAppContext, Entity, ModelContext, Task};
use lazy_static::lazy_static;
-use smol::lock::RwLock;
+use parking_lot::RwLock;
+use postage::prelude::Stream;
+use postage::sink::Sink;
+use postage::watch;
+use std::any::TypeId;
use std::collections::HashMap;
-use std::time::Duration;
+use std::sync::Weak;
+use std::time::{Duration, Instant};
use std::{convert::TryFrom, future::Future, sync::Arc};
use surf::Url;
+use zrpc::proto::{AnyTypedEnvelope, EntityMessage};
pub use zrpc::{proto, ConnectionId, PeerId, TypedEnvelope};
use zrpc::{
proto::{EnvelopedMessage, RequestMessage},
- ForegroundRouter, Peer, Receipt,
+ Peer, Receipt,
};
lazy_static! {
@@ -20,78 +26,127 @@ lazy_static! {
std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev:443".to_string());
}
-#[derive(Clone)]
pub struct Client {
peer: Arc<Peer>,
- pub state: Arc<RwLock<ClientState>>,
+ state: RwLock<ClientState>,
}
-pub struct ClientState {
+struct ClientState {
connection_id: Option<ConnectionId>,
- pub shared_worktrees: HashMap<u64, WeakModelHandle<Worktree>>,
- pub languages: Arc<LanguageRegistry>,
+ user_id: (watch::Sender<Option<u64>>, watch::Receiver<Option<u64>>),
+ entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>,
+ model_handlers: HashMap<
+ (TypeId, u64),
+ Box<dyn Send + Sync + FnMut(Box<dyn AnyTypedEnvelope>, &mut AsyncAppContext)>,
+ >,
}
-impl ClientState {
- pub fn shared_worktree(
- &self,
- id: u64,
- cx: &mut AsyncAppContext,
- ) -> Result<ModelHandle<Worktree>> {
- if let Some(worktree) = self.shared_worktrees.get(&id) {
- if let Some(worktree) = cx.read(|cx| worktree.upgrade(cx)) {
- Ok(worktree)
- } else {
- Err(anyhow!("worktree {} was dropped", id))
- }
- } else {
- Err(anyhow!("worktree {} does not exist", id))
+impl Default for ClientState {
+ fn default() -> Self {
+ Self {
+ connection_id: Default::default(),
+ user_id: watch::channel(),
+ entity_id_extractors: Default::default(),
+ model_handlers: Default::default(),
+ }
+ }
+}
+
+pub struct Subscription {
+ client: Weak<Client>,
+ id: (TypeId, u64),
+}
+
+impl Drop for Subscription {
+ fn drop(&mut self) {
+ if let Some(client) = self.client.upgrade() {
+ drop(
+ client
+ .state
+ .write()
+ .model_handlers
+ .remove(&self.id)
+ .unwrap(),
+ );
}
}
}
impl Client {
- pub fn new(languages: Arc<LanguageRegistry>) -> Self {
- Self {
+ pub fn new() -> Arc<Self> {
+ Arc::new(Self {
peer: Peer::new(),
- state: Arc::new(RwLock::new(ClientState {
- connection_id: None,
- shared_worktrees: Default::default(),
- languages,
- })),
- }
+ state: Default::default(),
+ })
}
- pub fn on_message<H, M>(
- &self,
- router: &mut ForegroundRouter,
- handler: H,
- cx: &mut gpui::MutableAppContext,
- ) where
- H: 'static + Clone + for<'a> MessageHandler<'a, M>,
- M: proto::EnvelopedMessage,
+ pub fn user_id(&self) -> watch::Receiver<Option<u64>> {
+ self.state.read().user_id.1.clone()
+ }
+
+ pub fn subscribe_from_model<T, M, F>(
+ self: &Arc<Self>,
+ remote_id: u64,
+ cx: &mut ModelContext<M>,
+ mut handler: F,
+ ) -> Subscription
+ where
+ T: EntityMessage,
+ M: Entity,
+ F: 'static
+ + Send
+ + Sync
+ + FnMut(&mut M, TypedEnvelope<T>, Arc<Self>, &mut ModelContext<M>) -> Result<()>,
{
- let this = self.clone();
- let cx = cx.to_async();
- router.add_message_handler(move |message| {
- let this = this.clone();
- let mut cx = cx.clone();
- let handler = handler.clone();
- async move { handler.handle(message, &this, &mut cx).await }
- });
+ let subscription_id = (TypeId::of::<T>(), remote_id);
+ let client = self.clone();
+ let mut state = self.state.write();
+ let model = cx.handle().downgrade();
+ state
+ .entity_id_extractors
+ .entry(subscription_id.0)
+ .or_insert_with(|| {
+ Box::new(|envelope| {
+ let envelope = envelope
+ .as_any()
+ .downcast_ref::<TypedEnvelope<T>>()
+ .unwrap();
+ envelope.payload.remote_entity_id()
+ })
+ });
+ let prev_handler = state.model_handlers.insert(
+ subscription_id,
+ Box::new(move |envelope, cx| {
+ if let Some(model) = model.upgrade(cx) {
+ let envelope = envelope.into_any().downcast::<TypedEnvelope<T>>().unwrap();
+ model.update(cx, |model, cx| {
+ if let Err(error) = handler(model, *envelope, client.clone(), cx) {
+ log::error!("error handling message: {}", error)
+ }
+ });
+ }
+ }),
+ );
+ if prev_handler.is_some() {
+ panic!("registered a handler for the same entity twice")
+ }
+
+ Subscription {
+ client: Arc::downgrade(self),
+ id: subscription_id,
+ }
}
- pub async fn log_in_and_connect(
- &self,
- router: Arc<ForegroundRouter>,
- cx: AsyncAppContext,
- ) -> surf::Result<()> {
- if self.state.read().await.connection_id.is_some() {
+ pub async fn authenticate_and_connect(
+ self: &Arc<Self>,
+ cx: &AsyncAppContext,
+ ) -> anyhow::Result<()> {
+ if self.state.read().connection_id.is_some() {
return Ok(());
}
let (user_id, access_token) = Self::login(cx.platform(), &cx.background()).await?;
- let user_id: i32 = user_id.parse()?;
+ let user_id = user_id.parse::<u64>()?;
let request =
Request::builder().header("Authorization", format!("{} {}", user_id, access_token));
@@ -101,27 +156,28 @@ impl Client {
let (stream, _) = async_tungstenite::async_tls::client_async_tls(request, stream)
.await
.context("websocket handshake")?;
- log::info!("connected to rpc address {}", *ZED_SERVER_URL);
- self.add_connection(stream, router, cx).await?;
+ self.add_connection(user_id, stream, cx).await?;
} 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?;
- log::info!("connected to rpc address {}", *ZED_SERVER_URL);
- self.add_connection(stream, router, cx).await?;
+ let (stream, _) = async_tungstenite::client_async(request, stream)
+ .await
+ .context("websocket handshake")?;
+ self.add_connection(user_id, stream, cx).await?;
} else {
return Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL))?;
};
+ log::info!("connected to rpc address {}", *ZED_SERVER_URL);
Ok(())
}
pub async fn add_connection<Conn>(
- &self,
+ self: &Arc<Self>,
+ user_id: u64,
conn: Conn,
- router: Arc<ForegroundRouter>,
- cx: AsyncAppContext,
- ) -> surf::Result<()>
+ cx: &AsyncAppContext,
+ ) -> anyhow::Result<()>
where
Conn: 'static
+ futures::Sink<WebSocketMessage, Error = WebSocketError>
@@ -129,9 +185,39 @@ impl Client {
+ Unpin
+ Send,
{
- let (connection_id, handle_io, handle_messages) =
- self.peer.add_connection(conn, router).await;
- cx.foreground().spawn(handle_messages).detach();
+ let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn).await;
+ {
+ let mut cx = cx.clone();
+ let this = self.clone();
+ cx.foreground()
+ .spawn(async move {
+ while let Some(message) = incoming.recv().await {
+ let mut state = this.state.write();
+ if let Some(extract_entity_id) =
+ state.entity_id_extractors.get(&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 start_time = Instant::now();
+ log::info!("RPC client message {}", message.payload_type_name());
+ (handler)(message, &mut cx);
+ log::info!(
+ "RPC message handled. duration:{:?}",
+ start_time.elapsed()
+ );
+ } else {
+ log::info!("unhandled message {}", message.payload_type_name());
+ }
+ } else {
+ log::info!("unhandled message {}", message.payload_type_name());
+ }
+ }
+ })
+ .detach();
+ }
cx.background()
.spawn(async move {
if let Err(error) = handle_io.await {
@@ -139,7 +225,9 @@ impl Client {
}
})
.detach();
- self.state.write().await.connection_id = Some(connection_id);
+ let mut state = self.state.write();
+ state.connection_id = Some(connection_id);
+ state.user_id.0.send(Some(user_id)).await?;
Ok(())
}
@@ -149,7 +237,11 @@ impl Client {
) -> Task<Result<(String, String)>> {
let executor = executor.clone();
executor.clone().spawn(async move {
- if let Some((user_id, access_token)) = platform.read_credentials(&ZED_SERVER_URL) {
+ 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, String::from_utf8(access_token).unwrap()));
}
@@ -214,33 +306,32 @@ 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());
+ platform
+ .write_credentials(&ZED_SERVER_URL, &user_id, access_token.as_bytes())
+ .log_err();
Ok((user_id.to_string(), access_token))
})
}
pub async fn disconnect(&self) -> Result<()> {
- let conn_id = self.connection_id().await?;
+ let conn_id = self.connection_id()?;
self.peer.disconnect(conn_id).await;
Ok(())
}
- async fn connection_id(&self) -> Result<ConnectionId> {
+ fn connection_id(&self) -> Result<ConnectionId> {
self.state
.read()
- .await
.connection_id
.ok_or_else(|| anyhow!("not connected"))
}
pub async fn send<T: EnvelopedMessage>(&self, message: T) -> Result<()> {
- self.peer.send(self.connection_id().await?, message).await
+ self.peer.send(self.connection_id()?, message).await
}
pub async fn request<T: RequestMessage>(&self, request: T) -> Result<T::Response> {
- self.peer
- .request(self.connection_id().await?, request)
- .await
+ self.peer.request(self.connection_id()?, request).await
}
pub fn respond<T: RequestMessage>(
@@ -11,23 +11,45 @@ pub struct Settings {
pub buffer_font_family: FamilyId,
pub buffer_font_size: f32,
pub tab_size: usize,
- pub ui_font_family: FamilyId,
- pub ui_font_size: f32,
pub theme: Arc<Theme>,
}
impl Settings {
- pub fn new(font_cache: &FontCache) -> Result<Self> {
- Self::new_with_theme(font_cache, Arc::new(Theme::default()))
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn test(cx: &gpui::AppContext) -> Self {
+ use crate::assets::Assets;
+ use gpui::AssetSource;
+
+ lazy_static::lazy_static! {
+ static ref DEFAULT_THEME: parking_lot::Mutex<Option<Arc<Theme>>> = Default::default();
+ static ref FONTS: Vec<Arc<Vec<u8>>> = Assets
+ .list("fonts")
+ .into_iter()
+ .map(|f| Arc::new(Assets.load(&f).unwrap().to_vec()))
+ .collect();
+ }
+
+ cx.platform().fonts().add_fonts(&FONTS).unwrap();
+
+ let mut theme_guard = DEFAULT_THEME.lock();
+ let theme = if let Some(theme) = theme_guard.as_ref() {
+ theme.clone()
+ } else {
+ let theme = ThemeRegistry::new(Assets, cx.font_cache().clone())
+ .get(DEFAULT_THEME_NAME)
+ .expect("failed to load default theme in tests");
+ *theme_guard = Some(theme.clone());
+ theme
+ };
+
+ Self::new(cx.font_cache(), theme).unwrap()
}
- pub fn new_with_theme(font_cache: &FontCache, theme: Arc<Theme>) -> Result<Self> {
+ pub fn new(font_cache: &FontCache, theme: Arc<Theme>) -> Result<Self> {
Ok(Self {
- buffer_font_family: font_cache.load_family(&["Fira Code", "Monaco"])?,
- buffer_font_size: 14.0,
+ buffer_font_family: font_cache.load_family(&["Inconsolata"])?,
+ buffer_font_size: 16.,
tab_size: 4,
- ui_font_family: font_cache.load_family(&["SF Pro", "Helvetica"])?,
- ui_font_size: 12.0,
theme,
})
}
@@ -38,13 +60,12 @@ impl Settings {
}
}
-pub fn channel(
- font_cache: &FontCache,
-) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
- Ok(watch::channel_with(Settings::new(font_cache)?))
+#[cfg(any(test, feature = "test-support"))]
+pub fn test(cx: &gpui::AppContext) -> (watch::Sender<Settings>, watch::Receiver<Settings>) {
+ watch::channel_with(Settings::test(cx))
}
-pub fn channel_with_themes(
+pub fn channel(
font_cache: &FontCache,
themes: &ThemeRegistry,
) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
@@ -54,7 +75,5 @@ pub fn channel_with_themes(
panic!("failed to deserialize default theme: {:?}", err)
}
};
- Ok(watch::channel_with(Settings::new_with_theme(
- font_cache, theme,
- )?))
+ Ok(watch::channel_with(Settings::new(font_cache, theme)?))
}
@@ -1,12 +1,15 @@
use crate::{
+ assets::Assets,
+ channel::ChannelList,
fs::RealFs,
language::LanguageRegistry,
rpc,
settings::{self, ThemeRegistry},
time::ReplicaId,
+ user::UserStore,
AppState,
};
-use gpui::{AppContext, Entity, ModelHandle};
+use gpui::{Entity, ModelHandle, MutableAppContext};
use parking_lot::Mutex;
use smol::channel;
use std::{
@@ -15,7 +18,6 @@ use std::{
sync::Arc,
};
use tempdir::TempDir;
-use zrpc::ForegroundRouter;
#[cfg(feature = "test-support")]
pub use zrpc::test::Channel;
@@ -154,24 +156,26 @@ fn write_tree(path: &Path, tree: serde_json::Value) {
}
}
-pub fn build_app_state(cx: &AppContext) -> Arc<AppState> {
- let (settings_tx, settings) = settings::channel(&cx.font_cache()).unwrap();
+pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
+ let (settings_tx, settings) = settings::test(cx);
let languages = Arc::new(LanguageRegistry::new());
- let themes = ThemeRegistry::new(());
+ let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
+ let rpc = rpc::Client::new();
+ let user_store = Arc::new(UserStore::new(rpc.clone()));
Arc::new(AppState {
settings_tx: Arc::new(Mutex::new(settings_tx)),
settings,
themes,
languages: languages.clone(),
- rpc_router: Arc::new(ForegroundRouter::new()),
- rpc: rpc::Client::new(languages),
+ channel_list: cx.add_model(|cx| ChannelList::new(user_store, rpc.clone(), cx)),
+ rpc,
fs: Arc::new(RealFs),
})
}
pub struct Observer<T>(PhantomData<T>);
-impl<T: 'static + Send + Sync> Entity for Observer<T> {
+impl<T: 'static> Entity for Observer<T> {
type Event = ();
}
@@ -184,7 +188,8 @@ impl<T: Entity> Observer<T> {
let observer = cx.add_model(|cx| {
cx.observe(handle, move |_, _, _| {
let _ = notify_tx.try_send(());
- });
+ })
+ .detach();
Observer(PhantomData)
});
(observer, notify_rx)
@@ -1,67 +1,46 @@
-use anyhow::{anyhow, Context, Result};
+mod highlight_map;
+mod theme_registry;
+
+use anyhow::Result;
use gpui::{
color::Color,
elements::{ContainerStyle, LabelStyle},
- fonts::TextStyle,
- AssetSource,
+ fonts::{HighlightStyle, TextStyle},
};
-use json::{Map, Value};
-use parking_lot::Mutex;
-use serde::{Deserialize, Deserializer};
-use serde_json as json;
-use std::{collections::HashMap, fmt, mem, sync::Arc};
-
-const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
-pub const DEFAULT_THEME_NAME: &'static str = "dark";
-
-pub struct ThemeRegistry {
- assets: Box<dyn AssetSource>,
- themes: Mutex<HashMap<String, Arc<Theme>>>,
- theme_data: Mutex<HashMap<String, Arc<Value>>>,
-}
+use serde::Deserialize;
+use std::collections::HashMap;
-#[derive(Clone, Debug)]
-pub struct HighlightMap(Arc<[HighlightId]>);
+pub use highlight_map::*;
+pub use theme_registry::*;
-#[derive(Clone, Copy, Debug)]
-pub struct HighlightId(u32);
+pub const DEFAULT_THEME_NAME: &'static str = "dark";
-#[derive(Debug, Default, Deserialize)]
+#[derive(Deserialize)]
pub struct Theme {
#[serde(default)]
pub name: String,
- pub ui: Ui,
- pub editor: Editor,
- #[serde(deserialize_with = "deserialize_syntax_theme")]
- pub syntax: Vec<(String, TextStyle)>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-pub struct Ui {
- pub background: Color,
- pub tab: Tab,
- pub active_tab: Tab,
+ pub workspace: Workspace,
+ pub chat_panel: ChatPanel,
pub selector: Selector,
+ pub editor: EditorStyle,
+ pub syntax: SyntaxTheme,
}
-#[derive(Debug, Deserialize)]
-pub struct Editor {
- pub background: Color,
- pub gutter_background: Color,
- pub active_line_background: Color,
- pub line_number: Color,
- pub line_number_active: Color,
- pub text: Color,
- pub replicas: Vec<Replica>,
+pub struct SyntaxTheme {
+ highlights: Vec<(String, HighlightStyle)>,
}
-#[derive(Clone, Copy, Debug, Default, Deserialize)]
-pub struct Replica {
- pub cursor: Color,
- pub selection: Color,
+#[derive(Deserialize)]
+pub struct Workspace {
+ pub background: Color,
+ pub tab: Tab,
+ pub active_tab: Tab,
+ pub sidebar: Sidebar,
+ pub sidebar_icon: SidebarIcon,
+ pub active_sidebar_icon: SidebarIcon,
}
-#[derive(Debug, Default, Deserialize)]
+#[derive(Deserialize)]
pub struct Tab {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -72,898 +51,186 @@ pub struct Tab {
pub icon_conflict: Color,
}
-#[derive(Debug, Default, Deserialize)]
-pub struct Selector {
- #[serde(flatten)]
- pub container: ContainerStyle,
- #[serde(flatten)]
- pub label: LabelStyle,
+#[derive(Deserialize)]
+pub struct Sidebar {
+ pub icons: ContainerStyle,
+ pub resize_handle: ContainerStyle,
+}
- pub item: SelectorItem,
- pub active_item: SelectorItem,
+#[derive(Deserialize)]
+pub struct SidebarIcon {
+ pub color: Color,
}
-#[derive(Debug, Default, Deserialize)]
-pub struct SelectorItem {
+#[derive(Deserialize)]
+pub struct ChatPanel {
#[serde(flatten)]
pub container: ContainerStyle,
- #[serde(flatten)]
- pub label: LabelStyle,
-}
-
-#[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>,
+ pub message: ChatMessage,
+ pub channel_select: ChannelSelect,
+ pub input_editor_container: ContainerStyle,
+ pub input_editor: InputEditorStyle,
+ pub sign_in_prompt: TextStyle,
+ pub hovered_sign_in_prompt: TextStyle,
}
-#[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),
+#[derive(Deserialize)]
+pub struct ChatMessage {
+ #[serde(flatten)]
+ pub container: ContainerStyle,
+ pub body: TextStyle,
+ pub sender: ContainedText,
+ pub timestamp: ContainedText,
}
-impl Default for Editor {
- fn default() -> Self {
- Self {
- background: Default::default(),
- gutter_background: Default::default(),
- active_line_background: Default::default(),
- line_number: Default::default(),
- line_number_active: Default::default(),
- text: Default::default(),
- replicas: vec![Replica::default()],
- }
- }
+#[derive(Deserialize)]
+pub struct ChannelSelect {
+ #[serde(flatten)]
+ pub container: ContainerStyle,
+ pub header: ChannelName,
+ pub item: ChannelName,
+ pub active_item: ChannelName,
+ pub hovered_item: ChannelName,
+ pub hovered_active_item: ChannelName,
+ pub menu: ContainerStyle,
}
-impl ThemeRegistry {
- pub fn new(source: impl AssetSource) -> Arc<Self> {
- Arc::new(Self {
- assets: Box::new(source),
- themes: Default::default(),
- theme_data: Default::default(),
- })
- }
-
- pub fn list(&self) -> impl Iterator<Item = String> {
- self.assets.list("themes/").into_iter().filter_map(|path| {
- let filename = path.strip_prefix("themes/")?;
- let theme_name = filename.strip_suffix(".toml")?;
- if theme_name.starts_with('_') {
- None
- } else {
- Some(theme_name.to_string())
- }
- })
- }
-
- pub fn clear(&self) {
- self.theme_data.lock().clear();
- self.themes.lock().clear();
- }
-
- pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
- if let Some(theme) = self.themes.lock().get(name) {
- return Ok(theme.clone());
- }
-
- let theme_data = self.load(name, true)?;
- let mut theme = serde_json::from_value::<Theme>(theme_data.as_ref().clone())?;
- theme.name = name.into();
- let theme = Arc::new(theme);
- self.themes.lock().insert(name.to_string(), theme.clone());
- Ok(theme)
- }
-
- fn load(&self, name: &str, evaluate_references: bool) -> Result<Arc<Value>> {
- if let Some(data) = self.theme_data.lock().get(name) {
- return Ok(data.clone());
- }
-
- let asset_path = format!("themes/{}.toml", name);
- let source_code = self
- .assets
- .load(&asset_path)
- .with_context(|| format!("failed to load theme file {}", asset_path))?;
-
- let mut theme_data: Map<String, Value> = toml::from_slice(source_code.as_ref())
- .with_context(|| format!("failed to parse {}.toml", name))?;
-
- // If this theme extends another base theme, deeply merge it into the base theme's data
- if let Some(base_name) = theme_data
- .get("extends")
- .and_then(|name| name.as_str())
- .map(str::to_string)
- {
- let base_theme_data = self
- .load(&base_name, false)
- .with_context(|| format!("failed to load base theme {}", base_name))?
- .as_ref()
- .clone();
- if let Value::Object(mut base_theme_object) = base_theme_data {
- deep_merge_json(&mut base_theme_object, theme_data);
- theme_data = base_theme_object;
- }
- }
-
- // 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))?;
- }
- }
- }
-
- let result = Arc::new(Value::Object(theme_data));
- self.theme_data
- .lock()
- .insert(name.to_string(), result.clone());
-
- Ok(result)
- }
+#[derive(Deserialize)]
+pub struct ChannelName {
+ #[serde(flatten)]
+ pub container: ContainerStyle,
+ pub hash: ContainedText,
+ pub name: TextStyle,
}
-impl Theme {
- pub fn highlight_style(&self, id: HighlightId) -> TextStyle {
- self.syntax
- .get(id.0 as usize)
- .map(|entry| entry.1.clone())
- .unwrap_or_else(|| TextStyle {
- color: self.editor.text,
- font_properties: Default::default(),
- })
- }
+#[derive(Deserialize)]
+pub struct Selector {
+ #[serde(flatten)]
+ pub container: ContainerStyle,
+ #[serde(flatten)]
+ pub label: LabelStyle,
- #[cfg(test)]
- pub fn highlight_name(&self, id: HighlightId) -> Option<&str> {
- self.syntax.get(id.0 as usize).map(|e| e.0.as_str())
- }
+ pub input_editor: InputEditorStyle,
+ pub item: ContainedLabel,
+ pub active_item: ContainedLabel,
}
-impl HighlightMap {
- pub fn new(capture_names: &[String], theme: &Theme) -> Self {
- // For each capture name in the highlight query, find the longest
- // key in the theme's syntax styles that matches all of the
- // dot-separated components of the capture name.
- HighlightMap(
- capture_names
- .iter()
- .map(|capture_name| {
- theme
- .syntax
- .iter()
- .enumerate()
- .filter_map(|(i, (key, _))| {
- let mut len = 0;
- let capture_parts = capture_name.split('.');
- for key_part in key.split('.') {
- if capture_parts.clone().any(|part| part == key_part) {
- len += 1;
- } else {
- return None;
- }
- }
- Some((i, len))
- })
- .max_by_key(|(_, len)| *len)
- .map_or(DEFAULT_HIGHLIGHT_ID, |(i, _)| HighlightId(i as u32))
- })
- .collect(),
- )
- }
-
- pub fn get(&self, capture_id: u32) -> HighlightId {
- self.0
- .get(capture_id as usize)
- .copied()
- .unwrap_or(DEFAULT_HIGHLIGHT_ID)
- }
+#[derive(Deserialize)]
+pub struct ContainedText {
+ #[serde(flatten)]
+ pub container: ContainerStyle,
+ #[serde(flatten)]
+ pub text: TextStyle,
}
-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;
- }
+#[derive(Deserialize)]
+pub struct ContainedLabel {
+ #[serde(flatten)]
+ pub container: ContainerStyle,
+ #[serde(flatten)]
+ pub label: LabelStyle,
}
-impl KeyPathReference {
- fn source(&self) -> &KeyPath {
- &self.source
- }
-
- fn target(&self) -> &KeyPath {
- &self.target
- }
+#[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>,
}
-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])
- }
+#[derive(Clone, Deserialize)]
+pub struct InputEditorStyle {
+ pub text: HighlightStyle,
+ pub placeholder_text: HighlightStyle,
+ pub background: Color,
+ pub selection: SelectionStyle,
}
-impl PartialEq<[Key]> for KeyPath {
- fn eq(&self, other: &[Key]) -> bool {
- self.0.eq(other)
- }
+#[derive(Clone, Copy, Default, Deserialize)]
+pub struct SelectionStyle {
+ pub cursor: Color,
+ pub selection: Color,
}
-impl fmt::Debug for KeyPathReference {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(
- f,
- "KeyPathReference {{ {} <- {} }}",
- self.target, self.source
- )
+impl SyntaxTheme {
+ pub fn new(highlights: Vec<(String, HighlightStyle)>) -> Self {
+ Self { highlights }
}
-}
-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(())
+ pub fn highlight_style(&self, id: HighlightId) -> Option<HighlightStyle> {
+ self.highlights
+ .get(id.0 as usize)
+ .map(|entry| entry.1.clone())
}
-}
-impl Default for HighlightMap {
- fn default() -> Self {
- Self(Arc::new([]))
+ #[cfg(test)]
+ pub fn highlight_name(&self, id: HighlightId) -> Option<&str> {
+ self.highlights.get(id.0 as usize).map(|e| e.0.as_str())
}
}
-impl Default for HighlightId {
+impl Default for EditorStyle {
fn default() -> Self {
- DEFAULT_HIGHLIGHT_ID
- }
-}
-
-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 {
- if let Some(base_object) = base.get_mut(&key).and_then(|value| value.as_object_mut()) {
- deep_merge_json(base_object, extension_object);
- } else {
- base.insert(key, Value::Object(extension_object));
- }
- } else {
- base.insert(key, extension_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;
- }
+ 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(),
}
- cur_value
- } else {
- None
}
}
-pub fn deserialize_syntax_theme<'de, D>(
- deserializer: D,
-) -> Result<Vec<(String, TextStyle)>, D::Error>
-where
- D: Deserializer<'de>,
-{
- let mut result = Vec::<(String, TextStyle)>::new();
-
- let syntax_data: HashMap<String, TextStyle> = Deserialize::deserialize(deserializer)?;
- for (key, style) in syntax_data {
- match result.binary_search_by(|(needle, _)| needle.cmp(&key)) {
- Ok(i) | Err(i) => {
- result.insert(i, (key, style));
- }
+impl InputEditorStyle {
+ pub fn as_editor(&self) -> EditorStyle {
+ EditorStyle {
+ text: self.text.clone(),
+ placeholder_text: self.placeholder_text.clone(),
+ background: self.background,
+ selection: self.selection,
+ ..Default::default()
}
}
-
- Ok(result)
}
-#[cfg(test)]
-mod tests {
- use rand::{prelude::StdRng, Rng};
-
- use super::*;
- use crate::assets::Assets;
-
- #[test]
- fn test_bundled_themes() {
- let registry = ThemeRegistry::new(Assets);
- let mut has_default_theme = false;
- for theme_name in registry.list() {
- let theme = registry.get(&theme_name).unwrap();
- if theme.name == DEFAULT_THEME_NAME {
- has_default_theme = true;
- }
- assert_eq!(theme.name, theme_name);
- }
- assert!(has_default_theme);
- }
-
- #[test]
- fn test_theme_extension() {
- let assets = TestAssets(&[
- (
- "themes/_base.toml",
- r##"
- [ui.active_tab]
- extends = "$ui.tab"
- border.color = "#666666"
- text = "$text_colors.bright"
-
- [ui.tab]
- extends = "$ui.element"
- text = "$text_colors.dull"
+impl<'de> Deserialize<'de> for SyntaxTheme {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let syntax_data: HashMap<String, HighlightStyle> = Deserialize::deserialize(deserializer)?;
- [ui.element]
- background = "#111111"
- border = {width = 2.0, color = "#00000000"}
-
- [editor]
- background = "#222222"
- default_text = "$text_colors.regular"
- "##,
- ),
- (
- "themes/light.toml",
- r##"
- extends = "_base"
-
- [text_colors]
- bright = "#ffffff"
- regular = "#eeeeee"
- dull = "#dddddd"
-
- [editor]
- background = "#232323"
- "##,
- ),
- ]);
-
- let registry = ThemeRegistry::new(assets);
- let theme_data = registry.load("light", true).unwrap();
- assert_eq!(
- theme_data.as_ref(),
- &serde_json::json!({
- "ui": {
- "active_tab": {
- "background": "#111111",
- "border": {
- "width": 2.0,
- "color": "#666666"
- },
- "extends": "$ui.tab",
- "text": "#ffffff"
- },
- "tab": {
- "background": "#111111",
- "border": {
- "width": 2.0,
- "color": "#00000000"
- },
- "extends": "$ui.element",
- "text": "#dddddd"
- },
- "element": {
- "background": "#111111",
- "border": {
- "width": 2.0,
- "color": "#00000000"
- }
+ let mut result = Self::new(Vec::new());
+ for (key, style) in syntax_data {
+ match result
+ .highlights
+ .binary_search_by(|(needle, _)| needle.cmp(&key))
+ {
+ Ok(i) | Err(i) => {
+ result.highlights.insert(i, (key, style));
}
- },
- "editor": {
- "background": "#232323",
- "default_text": "#eeeeee"
- },
- "extends": "_base",
- "text_colors": {
- "bright": "#ffffff",
- "regular": "#eeeeee",
- "dull": "#dddddd"
- }
- })
- );
- }
-
- #[test]
- fn test_highlight_map() {
- let theme = Theme {
- name: "test".into(),
- ui: Default::default(),
- editor: Default::default(),
- syntax: [
- ("function", Color::from_u32(0x100000ff)),
- ("function.method", Color::from_u32(0x200000ff)),
- ("function.async", Color::from_u32(0x300000ff)),
- ("variable.builtin.self.rust", Color::from_u32(0x400000ff)),
- ("variable.builtin", Color::from_u32(0x500000ff)),
- ("variable", Color::from_u32(0x600000ff)),
- ]
- .iter()
- .map(|(name, color)| (name.to_string(), (*color).into()))
- .collect(),
- };
-
- let capture_names = &[
- "function.special".to_string(),
- "function.async.rust".to_string(),
- "variable.builtin.self".to_string(),
- ];
-
- let map = HighlightMap::new(capture_names, &theme);
- assert_eq!(theme.highlight_name(map.get(0)), Some("function"));
- assert_eq!(theme.highlight_name(map.get(1)), Some("function.async"));
- assert_eq!(theme.highlight_name(map.get(2)), Some("variable.builtin"));
- }
-
- #[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<_>>();
-
- let mut reference_set = KeyPathReferenceSet::default();
- for reference in input_references {
- reference_set.insert(reference);
- }
- assert_eq!(reference_set.top_sort().unwrap(), expected_references);
- }
-
- #[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);
- }
-
- 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"),
- ]
- );
- }
-
- #[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 {
- fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
- if let Some(row) = self.0.iter().find(|e| e.0 == path) {
- Ok(row.1.as_bytes().into())
- } else {
- Err(anyhow!("no such path {}", path))
- }
- }
-
- fn list(&self, prefix: &str) -> Vec<std::borrow::Cow<'static, str>> {
- self.0
- .iter()
- .copied()
- .filter_map(|(path, _)| {
- if path.starts_with(prefix) {
- Some(path.into())
- } else {
- None
- }
- })
- .collect()
- }
+ Ok(result)
}
}
@@ -0,0 +1,96 @@
+use super::SyntaxTheme;
+use std::sync::Arc;
+
+#[derive(Clone, Debug)]
+pub struct HighlightMap(Arc<[HighlightId]>);
+
+#[derive(Clone, Copy, Debug)]
+pub struct HighlightId(pub u32);
+
+const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
+
+impl HighlightMap {
+ pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self {
+ // For each capture name in the highlight query, find the longest
+ // key in the theme's syntax styles that matches all of the
+ // dot-separated components of the capture name.
+ HighlightMap(
+ capture_names
+ .iter()
+ .map(|capture_name| {
+ theme
+ .highlights
+ .iter()
+ .enumerate()
+ .filter_map(|(i, (key, _))| {
+ let mut len = 0;
+ let capture_parts = capture_name.split('.');
+ for key_part in key.split('.') {
+ if capture_parts.clone().any(|part| part == key_part) {
+ len += 1;
+ } else {
+ return None;
+ }
+ }
+ Some((i, len))
+ })
+ .max_by_key(|(_, len)| *len)
+ .map_or(DEFAULT_HIGHLIGHT_ID, |(i, _)| HighlightId(i as u32))
+ })
+ .collect(),
+ )
+ }
+
+ pub fn get(&self, capture_id: u32) -> HighlightId {
+ self.0
+ .get(capture_id as usize)
+ .copied()
+ .unwrap_or(DEFAULT_HIGHLIGHT_ID)
+ }
+}
+
+impl Default for HighlightMap {
+ fn default() -> Self {
+ Self(Arc::new([]))
+ }
+}
+
+impl Default for HighlightId {
+ fn default() -> Self {
+ DEFAULT_HIGHLIGHT_ID
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::color::Color;
+
+ #[test]
+ fn test_highlight_map() {
+ let theme = SyntaxTheme::new(
+ [
+ ("function", Color::from_u32(0x100000ff)),
+ ("function.method", Color::from_u32(0x200000ff)),
+ ("function.async", Color::from_u32(0x300000ff)),
+ ("variable.builtin.self.rust", Color::from_u32(0x400000ff)),
+ ("variable.builtin", Color::from_u32(0x500000ff)),
+ ("variable", Color::from_u32(0x600000ff)),
+ ]
+ .iter()
+ .map(|(name, color)| (name.to_string(), (*color).into()))
+ .collect(),
+ );
+
+ let capture_names = &[
+ "function.special".to_string(),
+ "function.async.rust".to_string(),
+ "variable.builtin.self".to_string(),
+ ];
+
+ let map = HighlightMap::new(capture_names, &theme);
+ assert_eq!(theme.highlight_name(map.get(0)), Some("function"));
+ assert_eq!(theme.highlight_name(map.get(1)), Some("function.async"));
+ assert_eq!(theme.highlight_name(map.get(2)), Some("variable.builtin"));
+ }
+}
@@ -0,0 +1,761 @@
+use anyhow::{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 super::Theme;
+
+pub struct ThemeRegistry {
+ assets: Box<dyn AssetSource>,
+ themes: Mutex<HashMap<String, Arc<Theme>>>,
+ theme_data: Mutex<HashMap<String, Arc<Value>>>,
+ 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 {
+ assets: Box::new(source),
+ themes: Default::default(),
+ theme_data: Default::default(),
+ font_cache,
+ })
+ }
+
+ pub fn list(&self) -> impl Iterator<Item = String> {
+ self.assets.list("themes/").into_iter().filter_map(|path| {
+ let filename = path.strip_prefix("themes/")?;
+ let theme_name = filename.strip_suffix(".toml")?;
+ if theme_name.starts_with('_') {
+ None
+ } else {
+ Some(theme_name.to_string())
+ }
+ })
+ }
+
+ pub fn clear(&self) {
+ self.theme_data.lock().clear();
+ self.themes.lock().clear();
+ }
+
+ pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
+ if let Some(theme) = self.themes.lock().get(name) {
+ return Ok(theme.clone());
+ }
+
+ let theme_data = self.load(name, true)?;
+ let mut theme: Theme = fonts::with_font_cache(self.font_cache.clone(), || {
+ serde_path_to_error::deserialize(theme_data.as_ref())
+ })?;
+
+ theme.name = name.into();
+ let theme = Arc::new(theme);
+ self.themes.lock().insert(name.to_string(), theme.clone());
+ Ok(theme)
+ }
+
+ fn load(&self, name: &str, evaluate_references: bool) -> Result<Arc<Value>> {
+ if let Some(data) = self.theme_data.lock().get(name) {
+ return Ok(data.clone());
+ }
+
+ let asset_path = format!("themes/{}.toml", name);
+ let source_code = self
+ .assets
+ .load(&asset_path)
+ .with_context(|| format!("failed to load theme file {}", asset_path))?;
+
+ let mut theme_data: Map<String, Value> = toml::from_slice(source_code.as_ref())
+ .with_context(|| format!("failed to parse {}.toml", name))?;
+
+ // If this theme extends another base theme, deeply merge it into the base theme's data
+ if let Some(base_name) = theme_data
+ .get("extends")
+ .and_then(|name| name.as_str())
+ .map(str::to_string)
+ {
+ let base_theme_data = self
+ .load(&base_name, false)
+ .with_context(|| format!("failed to load base theme {}", base_name))?
+ .as_ref()
+ .clone();
+ if let Value::Object(mut base_theme_object) = base_theme_data {
+ deep_merge_json(&mut base_theme_object, theme_data);
+ theme_data = base_theme_object;
+ }
+ }
+
+ // 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))?;
+ }
+ }
+ }
+
+ let result = Arc::new(Value::Object(theme_data));
+ self.theme_data
+ .lock()
+ .insert(name.to_string(), result.clone());
+
+ Ok(result)
+ }
+}
+
+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 {
+ if let Some(base_object) = base.get_mut(&key).and_then(|value| value.as_object_mut()) {
+ deep_merge_json(base_object, extension_object);
+ } else {
+ base.insert(key, Value::Object(extension_object));
+ }
+ } else {
+ base.insert(key, extension_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 gpui::MutableAppContext;
+ use rand::{prelude::StdRng, Rng};
+
+ #[gpui::test]
+ fn test_bundled_themes(cx: &mut MutableAppContext) {
+ let app_state = test_app_state(cx);
+ let mut has_default_theme = false;
+ for theme_name in app_state.themes.list() {
+ let theme = app_state.themes.get(&theme_name).unwrap();
+ if theme.name == DEFAULT_THEME_NAME {
+ has_default_theme = true;
+ }
+ assert_eq!(theme.name, theme_name);
+ }
+ assert!(has_default_theme);
+ }
+
+ #[gpui::test]
+ fn test_theme_extension(cx: &mut MutableAppContext) {
+ let assets = TestAssets(&[
+ (
+ "themes/_base.toml",
+ r##"
+ [ui.active_tab]
+ extends = "$ui.tab"
+ border.color = "#666666"
+ text = "$text_colors.bright"
+
+ [ui.tab]
+ extends = "$ui.element"
+ text = "$text_colors.dull"
+
+ [ui.element]
+ background = "#111111"
+ border = {width = 2.0, color = "#00000000"}
+
+ [editor]
+ background = "#222222"
+ default_text = "$text_colors.regular"
+ "##,
+ ),
+ (
+ "themes/light.toml",
+ r##"
+ extends = "_base"
+
+ [text_colors]
+ bright = "#ffffff"
+ regular = "#eeeeee"
+ dull = "#dddddd"
+
+ [editor]
+ background = "#232323"
+ "##,
+ ),
+ ]);
+
+ 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!({
+ "ui": {
+ "active_tab": {
+ "background": "#111111",
+ "border": {
+ "width": 2.0,
+ "color": "#666666"
+ },
+ "extends": "$ui.tab",
+ "text": "#ffffff"
+ },
+ "tab": {
+ "background": "#111111",
+ "border": {
+ "width": 2.0,
+ "color": "#00000000"
+ },
+ "extends": "$ui.element",
+ "text": "#dddddd"
+ },
+ "element": {
+ "background": "#111111",
+ "border": {
+ "width": 2.0,
+ "color": "#00000000"
+ }
+ }
+ },
+ "editor": {
+ "background": "#232323",
+ "default_text": "#eeeeee"
+ },
+ "extends": "_base",
+ "text_colors": {
+ "bright": "#ffffff",
+ "regular": "#eeeeee",
+ "dull": "#dddddd"
+ }
+ })
+ );
+ }
+
+ #[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<_>>();
+
+ let mut reference_set = KeyPathReferenceSet::default();
+ for reference in input_references {
+ reference_set.insert(reference);
+ }
+ assert_eq!(reference_set.top_sort().unwrap(), expected_references);
+ }
+
+ #[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);
+ }
+
+ 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"),
+ ]
+ );
+ }
+
+ #[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 {
+ fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
+ if let Some(row) = self.0.iter().find(|e| e.0 == path) {
+ Ok(row.1.as_bytes().into())
+ } else {
+ Err(anyhow!("no such path {}", path))
+ }
+ }
+
+ fn list(&self, prefix: &str) -> Vec<std::borrow::Cow<'static, str>> {
+ self.0
+ .iter()
+ .copied()
+ .filter_map(|(path, _)| {
+ if path.starts_with(prefix) {
+ Some(path.into())
+ } else {
+ None
+ }
+ })
+ .collect()
+ }
+ }
+}
@@ -8,11 +8,9 @@ use crate::{
AppState, Settings,
};
use gpui::{
- elements::{
- Align, ChildView, ConstrainedBox, Container, Expanded, Flex, Label, ParentElement,
- UniformList, UniformListState,
- },
- keymap::{self, Binding},
+ action,
+ elements::*,
+ keymap::{self, menu, Binding},
AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
ViewContext, ViewHandle,
};
@@ -24,24 +22,27 @@ pub struct ThemeSelector {
settings: watch::Receiver<Settings>,
registry: Arc<ThemeRegistry>,
matches: Vec<StringMatch>,
- query_buffer: ViewHandle<Editor>,
+ query_editor: ViewHandle<Editor>,
list_state: UniformListState,
selected_index: usize,
}
-pub fn init(cx: &mut MutableAppContext, app_state: &Arc<AppState>) {
- cx.add_action("theme_selector:confirm", ThemeSelector::confirm);
- cx.add_action("menu:select_prev", ThemeSelector::select_prev);
- cx.add_action("menu:select_next", ThemeSelector::select_next);
- cx.add_action("theme_selector:toggle", ThemeSelector::toggle);
- cx.add_action("theme_selector:reload", ThemeSelector::reload);
+action!(Confirm);
+action!(Toggle, Arc<AppState>);
+action!(Reload, Arc<AppState>);
+
+pub fn init(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
+ cx.add_action(ThemeSelector::confirm);
+ cx.add_action(ThemeSelector::select_prev);
+ cx.add_action(ThemeSelector::select_next);
+ cx.add_action(ThemeSelector::toggle);
+ cx.add_action(ThemeSelector::reload);
cx.add_bindings(vec![
- Binding::new("cmd-k cmd-t", "theme_selector:toggle", None).with_arg(app_state.clone()),
- Binding::new("cmd-k t", "theme_selector:reload", None).with_arg(app_state.clone()),
- Binding::new("escape", "theme_selector:toggle", Some("ThemeSelector"))
- .with_arg(app_state.clone()),
- Binding::new("enter", "theme_selector:confirm", Some("ThemeSelector")),
+ Binding::new("cmd-k cmd-t", Toggle(app_state.clone()), None),
+ Binding::new("cmd-k t", Reload(app_state.clone()), None),
+ Binding::new("escape", Toggle(app_state.clone()), Some("ThemeSelector")),
+ Binding::new("enter", Confirm, Some("ThemeSelector")),
]);
}
@@ -56,14 +57,21 @@ impl ThemeSelector {
registry: Arc<ThemeRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
- let query_buffer = cx.add_view(|cx| Editor::single_line(settings.clone(), cx));
- cx.subscribe_to_view(&query_buffer, Self::on_query_editor_event);
+ 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()
+ })
+ });
+
+ cx.subscribe(&query_editor, Self::on_query_editor_event)
+ .detach();
let mut this = Self {
settings,
settings_tx,
registry,
- query_buffer,
+ query_editor,
matches: Vec::new(),
list_state: Default::default(),
selected_index: 0,
@@ -72,32 +80,29 @@ impl ThemeSelector {
this
}
- fn toggle(
- workspace: &mut Workspace,
- app_state: &Arc<AppState>,
- cx: &mut ViewContext<Workspace>,
- ) {
+ fn toggle(workspace: &mut Workspace, action: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |cx, _| {
let selector = cx.add_view(|cx| {
Self::new(
- app_state.settings_tx.clone(),
- app_state.settings.clone(),
- app_state.themes.clone(),
+ action.0.settings_tx.clone(),
+ action.0.settings.clone(),
+ action.0.themes.clone(),
cx,
)
});
- cx.subscribe_to_view(&selector, Self::on_event);
+ cx.subscribe(&selector, Self::on_event).detach();
selector
});
}
- fn reload(_: &mut Workspace, app_state: &Arc<AppState>, cx: &mut ViewContext<Workspace>) {
- let current_theme_name = app_state.settings.borrow().theme.name.clone();
- app_state.themes.clear();
- match app_state.themes.get(¤t_theme_name) {
+ fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
+ let current_theme_name = action.0.settings.borrow().theme.name.clone();
+ action.0.themes.clear();
+ match action.0.themes.get(¤t_theme_name) {
Ok(theme) => {
- cx.notify_all();
- app_state.settings_tx.lock().borrow_mut().theme = theme;
+ cx.refresh_windows();
+ action.0.settings_tx.lock().borrow_mut().theme = theme;
+ log::info!("reloaded theme {}", current_theme_name);
}
Err(error) => {
log::error!("failed to load theme {}: {:?}", current_theme_name, error)
@@ -105,12 +110,12 @@ impl ThemeSelector {
}
}
- fn confirm(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some(mat) = self.matches.get(self.selected_index) {
match self.registry.get(&mat.string) {
Ok(theme) => {
self.settings_tx.lock().borrow_mut().theme = theme;
- cx.notify_all();
+ cx.refresh_windows();
cx.emit(Event::Dismissed);
}
Err(error) => log::error!("error loading theme {}: {}", mat.string, error),
@@ -118,7 +123,7 @@ impl ThemeSelector {
}
}
- fn select_prev(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
if self.selected_index > 0 {
self.selected_index -= 1;
}
@@ -126,7 +131,7 @@ impl ThemeSelector {
cx.notify();
}
- fn select_next(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
if self.selected_index + 1 < self.matches.len() {
self.selected_index += 1;
}
@@ -149,7 +154,7 @@ impl ThemeSelector {
string: name,
})
.collect::<Vec<_>>();
- let query = self.query_buffer.update(cx, |buffer, cx| buffer.text(cx));
+ let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
self.matches = if query.is_empty() {
candidates
@@ -170,6 +175,7 @@ impl ThemeSelector {
background,
))
};
+ cx.notify();
}
fn on_event(
@@ -198,17 +204,11 @@ impl ThemeSelector {
}
}
- fn render_matches(&self, cx: &RenderContext<Self>) -> ElementBox {
+ fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
if self.matches.is_empty() {
let settings = self.settings.borrow();
return Container::new(
- Label::new(
- "No matches".into(),
- settings.ui_font_family,
- settings.ui_font_size,
- )
- .with_style(&settings.theme.ui.selector.label)
- .boxed(),
+ Label::new("No matches".into(), settings.theme.selector.label.clone()).boxed(),
)
.with_margin_top(6.0)
.named("empty matches");
@@ -240,19 +240,17 @@ impl ThemeSelector {
fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox {
let settings = self.settings.borrow();
- let theme = &settings.theme.ui;
+ let theme = &settings.theme;
let container = Container::new(
Label::new(
theme_match.string.clone(),
- settings.ui_font_family,
- settings.ui_font_size,
+ if index == self.selected_index {
+ theme.selector.active_item.label.clone()
+ } else {
+ theme.selector.item.label.clone()
+ },
)
- .with_style(if index == self.selected_index {
- &theme.selector.active_item.label
- } else {
- &theme.selector.item.label
- })
.with_highlights(theme_match.positions.clone())
.boxed(),
)
@@ -275,18 +273,18 @@ impl View for ThemeSelector {
"ThemeSelector"
}
- fn render(&self, cx: &RenderContext<Self>) -> ElementBox {
+ fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let settings = self.settings.borrow();
Align::new(
ConstrainedBox::new(
Container::new(
Flex::new(Axis::Vertical)
- .with_child(ChildView::new(self.query_buffer.id()).boxed())
- .with_child(Expanded::new(1.0, self.render_matches(cx)).boxed())
+ .with_child(ChildView::new(self.query_editor.id()).boxed())
+ .with_child(Flexible::new(1.0, self.render_matches(cx)).boxed())
.boxed(),
)
- .with_style(&settings.theme.ui.selector.container)
+ .with_style(&settings.theme.selector.container)
.boxed(),
)
.with_max_width(600.0)
@@ -298,7 +296,7 @@ impl View for ThemeSelector {
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
- cx.focus(&self.query_buffer);
+ cx.focus(&self.query_editor);
}
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
@@ -0,0 +1,59 @@
+use crate::rpc::Client;
+use anyhow::{anyhow, Result};
+use parking_lot::Mutex;
+use std::{collections::HashMap, sync::Arc};
+use zrpc::proto;
+
+pub use proto::User;
+
+pub struct UserStore {
+ users: Mutex<HashMap<u64, Arc<User>>>,
+ rpc: Arc<Client>,
+}
+
+impl UserStore {
+ pub fn new(rpc: Arc<Client>) -> Self {
+ Self {
+ users: Default::default(),
+ rpc,
+ }
+ }
+
+ pub async fn load_users(&self, mut user_ids: Vec<u64>) -> Result<()> {
+ {
+ let users = self.users.lock();
+ user_ids.retain(|id| !users.contains_key(id));
+ }
+
+ if !user_ids.is_empty() {
+ let response = self.rpc.request(proto::GetUsers { user_ids }).await?;
+ let mut users = self.users.lock();
+ for user in response.users {
+ users.insert(user.id, Arc::new(user));
+ }
+ }
+
+ Ok(())
+ }
+
+ pub async fn get_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?;
+
+ 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"))
+ }
+ }
+}
@@ -1,28 +1,11 @@
+use futures::{Future};
+pub use gpui::sum_tree::Bias;
use rand::prelude::*;
-use std::cmp::Ordering;
-
-#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
-pub enum Bias {
- Left,
- Right,
-}
-
-impl PartialOrd for Bias {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- Some(self.cmp(other))
- }
-}
-
-impl Ord for Bias {
- fn cmp(&self, other: &Self) -> Ordering {
- match (self, other) {
- (Self::Left, Self::Left) => Ordering::Equal,
- (Self::Left, Self::Right) => Ordering::Less,
- (Self::Right, Self::Right) => Ordering::Equal,
- (Self::Right, Self::Left) => Ordering::Greater,
- }
- }
-}
+use std::{
+ cmp::Ordering,
+ pin::Pin,
+ task::{Context, Poll},
+};
pub fn post_inc(value: &mut usize) -> usize {
let prev = *value;
@@ -81,6 +64,61 @@ impl<T: Rng> Iterator for RandomCharIter<T> {
}
}
+pub trait ResultExt {
+ type Ok;
+
+ fn log_err(self) -> Option<Self::Ok>;
+}
+
+impl<T> ResultExt for anyhow::Result<T> {
+ type Ok = T;
+
+ fn log_err(self) -> Option<T> {
+ match self {
+ Ok(value) => Some(value),
+ Err(error) => {
+ log::error!("{:?}", error);
+ None
+ }
+ }
+ }
+}
+
+pub trait TryFutureExt {
+ fn log_err(self) -> LogErrorFuture<Self>
+ where
+ Self: Sized;
+}
+
+impl<F, T> TryFutureExt for F
+where
+ F: Future<Output = anyhow::Result<T>>,
+{
+ fn log_err(self) -> LogErrorFuture<Self>
+ where
+ Self: Sized,
+ {
+ LogErrorFuture(self)
+ }
+}
+
+pub struct LogErrorFuture<F>(F);
+
+impl<F, T> Future for LogErrorFuture<F>
+where
+ F: Future<Output = anyhow::Result<T>>,
+{
+ type Output = Option<T>;
+
+ fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+ let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
+ match inner.poll(cx) {
+ Poll::Ready(output) => Poll::Ready(output.log_err()),
+ Poll::Pending => Poll::Pending,
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -1,10 +1,13 @@
pub mod pane;
pub mod pane_group;
+pub mod sidebar;
use crate::{
- editor::{Buffer, Editor},
+ chat_panel::ChatPanel,
+ editor::Buffer,
fs::Fs,
language::LanguageRegistry,
+ project_browser::ProjectBrowser,
rpc,
settings::Settings,
worktree::{File, Worktree},
@@ -12,14 +15,21 @@ use crate::{
};
use anyhow::{anyhow, Result};
use gpui::{
- elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, ClipboardItem,
- Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task,
- View, ViewContext, ViewHandle, WeakModelHandle,
+ action,
+ elements::*,
+ geometry::{rect::RectF, vector::vec2f},
+ json::to_string_pretty,
+ keymap::Binding,
+ platform::WindowOptions,
+ AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext,
+ PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
+ WeakModelHandle,
};
use log::error;
pub use pane::*;
pub use pane_group::*;
use postage::watch;
+use sidebar::{Side, Sidebar, ToggleSidebarItem};
use smol::prelude::*;
use std::{
collections::{hash_map::Entry, HashMap, HashSet},
@@ -28,33 +38,42 @@ use std::{
sync::Arc,
};
+action!(Open, Arc<AppState>);
+action!(OpenPaths, OpenParams);
+action!(OpenNew, Arc<AppState>);
+action!(ShareWorktree);
+action!(JoinWorktree, Arc<AppState>);
+action!(Save);
+action!(DebugElements);
+
pub fn init(cx: &mut MutableAppContext) {
- cx.add_global_action("workspace:open", open);
- cx.add_global_action(
- "workspace:open_paths",
- |params: &OpenParams, cx: &mut MutableAppContext| open_paths(params, cx).detach(),
- );
- cx.add_global_action("workspace:new_file", open_new);
- cx.add_global_action("workspace:join_worktree", join_worktree);
- cx.add_action("workspace:save", Workspace::save_active_item);
- cx.add_action("workspace:debug_elements", Workspace::debug_elements);
- cx.add_action("workspace:new_file", Workspace::open_new_file);
- cx.add_action("workspace:share_worktree", Workspace::share_worktree);
- cx.add_action("workspace:join_worktree", Workspace::join_worktree);
+ cx.add_global_action(open);
+ cx.add_global_action(|action: &OpenPaths, cx: &mut MutableAppContext| {
+ open_paths(action, cx).detach()
+ });
+ cx.add_global_action(open_new);
+ cx.add_global_action(join_worktree);
+ cx.add_action(Workspace::save_active_item);
+ cx.add_action(Workspace::debug_elements);
+ cx.add_action(Workspace::open_new_file);
+ cx.add_action(Workspace::share_worktree);
+ cx.add_action(Workspace::join_worktree);
+ cx.add_action(Workspace::toggle_sidebar_item);
cx.add_bindings(vec![
- Binding::new("cmd-s", "workspace:save", None),
- Binding::new("cmd-alt-i", "workspace:debug_elements", None),
+ Binding::new("cmd-s", Save, None),
+ Binding::new("cmd-alt-i", DebugElements, None),
]);
pane::init(cx);
}
+#[derive(Clone)]
pub struct OpenParams {
pub paths: Vec<PathBuf>,
pub app_state: Arc<AppState>,
}
-fn open(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
- let app_state = app_state.clone();
+fn open(action: &Open, cx: &mut MutableAppContext) {
+ let app_state = action.0.clone();
cx.prompt_for_paths(
PathPromptOptions {
files: true,
@@ -63,22 +82,22 @@ fn open(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
},
move |paths, cx| {
if let Some(paths) = paths {
- cx.dispatch_global_action("workspace:open_paths", OpenParams { paths, app_state });
+ cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state }));
}
},
);
}
-fn open_paths(params: &OpenParams, cx: &mut MutableAppContext) -> Task<()> {
- log::info!("open paths {:?}", params.paths);
+fn open_paths(action: &OpenPaths, cx: &mut MutableAppContext) -> Task<()> {
+ log::info!("open paths {:?}", action.0.paths);
// Open paths in existing workspace if possible
for window_id in cx.window_ids().collect::<Vec<_>>() {
if let Some(handle) = cx.root_view::<Workspace>(window_id) {
let task = handle.update(cx, |view, cx| {
- if view.contains_paths(¶ms.paths, cx.as_ref()) {
+ if view.contains_paths(&action.0.paths, cx.as_ref()) {
log::info!("open paths on existing workspace");
- Some(view.open_paths(¶ms.paths, cx))
+ Some(view.open_paths(&action.0.paths, cx))
} else {
None
}
@@ -93,26 +112,40 @@ fn open_paths(params: &OpenParams, cx: &mut MutableAppContext) -> Task<()> {
log::info!("open new workspace");
// Add a new workspace if necessary
- let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms.app_state, cx));
- workspace.update(cx, |workspace, cx| workspace.open_paths(¶ms.paths, cx))
+
+ let (_, workspace) = cx.add_window(window_options(), |cx| {
+ Workspace::new(&action.0.app_state, cx)
+ });
+ workspace.update(cx, |workspace, cx| {
+ workspace.open_paths(&action.0.paths, cx)
+ })
}
-fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
- cx.add_window(|cx| {
- let mut view = Workspace::new(app_state.as_ref(), cx);
- view.open_new_file(&app_state, cx);
+fn open_new(action: &OpenNew, cx: &mut MutableAppContext) {
+ cx.add_window(window_options(), |cx| {
+ let mut view = Workspace::new(action.0.as_ref(), cx);
+ view.open_new_file(&action, cx);
view
});
}
-fn join_worktree(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
- cx.add_window(|cx| {
- let mut view = Workspace::new(app_state.as_ref(), cx);
- view.join_worktree(&app_state, cx);
+fn join_worktree(action: &JoinWorktree, cx: &mut MutableAppContext) {
+ cx.add_window(window_options(), |cx| {
+ let mut view = Workspace::new(action.0.as_ref(), cx);
+ view.join_worktree(action, cx);
view
});
}
+fn window_options() -> WindowOptions<'static> {
+ WindowOptions {
+ bounds: RectF::new(vec2f(0., 0.), vec2f(1024., 768.)),
+ title: None,
+ titlebar_appears_transparent: true,
+ traffic_light_position: Some(vec2f(8., 8.)),
+ }
+}
+
pub trait Item: Entity + Sized {
type View: ItemView;
@@ -160,7 +193,7 @@ pub trait ItemHandle: Send + Sync {
fn downgrade(&self) -> Box<dyn WeakItemHandle>;
}
-pub trait WeakItemHandle: Send + Sync {
+pub trait WeakItemHandle {
fn file<'a>(&'a self, cx: &'a AppContext) -> Option<&'a File>;
fn add_view(
&self,
@@ -171,7 +204,7 @@ pub trait WeakItemHandle: Send + Sync {
fn alive(&self, cx: &AppContext) -> bool;
}
-pub trait ItemViewHandle: Send + Sync {
+pub trait ItemViewHandle {
fn title(&self, cx: &AppContext) -> String;
fn entry_id(&self, cx: &AppContext) -> Option<(usize, Arc<Path>)>;
fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
@@ -247,7 +280,7 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext) {
pane.update(cx, |_, cx| {
- cx.subscribe_to_view(self, |pane, item, event, cx| {
+ cx.subscribe(self, |pane, item, event, cx| {
if T::should_activate_item_on_event(event) {
if let Some(ix) = pane.item_index(&item) {
pane.activate_item(ix, cx);
@@ -258,7 +291,8 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
cx.notify()
}
})
- })
+ .detach();
+ });
}
fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>> {
@@ -303,19 +337,15 @@ impl Clone for Box<dyn ItemHandle> {
}
}
-#[derive(Debug)]
-pub struct State {
- pub modal: Option<usize>,
- pub center: PaneGroup,
-}
-
pub struct Workspace {
pub settings: watch::Receiver<Settings>,
languages: Arc<LanguageRegistry>,
- rpc: rpc::Client,
+ rpc: Arc<rpc::Client>,
fs: Arc<dyn Fs>,
modal: Option<AnyViewHandle>,
center: PaneGroup,
+ left_sidebar: Sidebar,
+ right_sidebar: Sidebar,
panes: Vec<ViewHandle<Pane>>,
active_pane: ViewHandle<Pane>,
worktrees: HashSet<ModelHandle<Worktree>>,
@@ -330,11 +360,33 @@ impl Workspace {
pub fn new(app_state: &AppState, cx: &mut ViewContext<Self>) -> Self {
let pane = cx.add_view(|_| Pane::new(app_state.settings.clone()));
let pane_id = pane.id();
- cx.subscribe_to_view(&pane, move |me, _, event, cx| {
+ cx.subscribe(&pane, move |me, _, event, cx| {
me.handle_pane_event(pane_id, event, cx)
- });
+ })
+ .detach();
cx.focus(&pane);
+ let mut left_sidebar = Sidebar::new(Side::Left);
+ left_sidebar.add_item(
+ "icons/folder-tree-16.svg",
+ cx.add_view(|_| ProjectBrowser).into(),
+ );
+
+ let mut right_sidebar = Sidebar::new(Side::Right);
+ right_sidebar.add_item(
+ "icons/comment-16.svg",
+ cx.add_view(|cx| {
+ ChatPanel::new(
+ app_state.rpc.clone(),
+ app_state.channel_list.clone(),
+ app_state.settings.clone(),
+ cx,
+ )
+ })
+ .into(),
+ );
+ right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into());
+
Workspace {
modal: None,
center: PaneGroup::new(pane.id()),
@@ -344,6 +396,8 @@ impl Workspace {
languages: app_state.languages.clone(),
rpc: app_state.rpc.clone(),
fs: app_state.fs.clone(),
+ left_sidebar,
+ right_sidebar,
worktrees: Default::default(),
items: Default::default(),
loading_items: Default::default(),
@@ -476,7 +530,7 @@ impl Workspace {
cx.spawn(|this, mut cx| async move {
let worktree = Worktree::open_local(path, languages, fs, &mut cx).await?;
this.update(&mut cx, |this, cx| {
- cx.observe_model(&worktree, |_, _, cx| cx.notify());
+ cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
this.worktrees.insert(worktree.clone());
cx.notify();
});
@@ -511,13 +565,14 @@ impl Workspace {
}
}
- pub fn open_new_file(&mut self, _: &Arc<AppState>, cx: &mut ViewContext<Self>) {
+ pub fn open_new_file(&mut self, _: &OpenNew, cx: &mut ViewContext<Self>) {
let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
- let buffer_view =
- cx.add_view(|cx| Editor::for_buffer(buffer.clone(), self.settings.clone(), cx));
- self.items.push(ItemHandle::downgrade(&buffer));
- self.active_pane()
- .add_item_view(Box::new(buffer_view), cx.as_mut());
+ let item_handle = ItemHandle::downgrade(&buffer);
+ let view = item_handle
+ .add_view(cx.window_id(), self.settings.clone(), cx)
+ .unwrap();
+ self.items.push(item_handle);
+ self.active_pane().add_item_view(view, cx.as_mut());
}
#[must_use]
@@ -644,7 +699,7 @@ impl Workspace {
self.active_pane().read(cx).active_item()
}
- pub fn save_active_item(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn save_active_item(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
if let Some(item) = self.active_item(cx) {
let handle = cx.handle();
if item.entry_id(cx.as_ref()).is_none() {
@@ -709,7 +764,21 @@ impl Workspace {
}
}
- pub fn debug_elements(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+ pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
+ let sidebar = match action.0.side {
+ Side::Left => &mut self.left_sidebar,
+ Side::Right => &mut self.right_sidebar,
+ };
+ sidebar.toggle_item(action.0.item_index);
+ if let Some(active_item) = sidebar.active_item() {
+ cx.focus(active_item);
+ } else {
+ cx.focus_self();
+ }
+ cx.notify();
+ }
+
+ pub fn debug_elements(&mut self, _: &DebugElements, cx: &mut ViewContext<Self>) {
match to_string_pretty(&cx.debug_elements()) {
Ok(json) => {
let kib = json.len() as f32 / 1024.;
@@ -725,13 +794,12 @@ impl Workspace {
};
}
- fn share_worktree(&mut self, app_state: &Arc<AppState>, cx: &mut ViewContext<Self>) {
+ fn share_worktree(&mut self, _: &ShareWorktree, cx: &mut ViewContext<Self>) {
let rpc = self.rpc.clone();
let platform = cx.platform();
- let router = app_state.rpc_router.clone();
let task = cx.spawn(|this, mut cx| async move {
- rpc.log_in_and_connect(router, cx.clone()).await?;
+ rpc.authenticate_and_connect(&cx).await?;
let share_task = this.update(&mut cx, |this, cx| {
let worktree = this.worktrees.iter().next()?;
@@ -758,13 +826,12 @@ impl Workspace {
.detach();
}
- fn join_worktree(&mut self, app_state: &Arc<AppState>, cx: &mut ViewContext<Self>) {
+ fn join_worktree(&mut self, _: &JoinWorktree, cx: &mut ViewContext<Self>) {
let rpc = self.rpc.clone();
let languages = self.languages.clone();
- let router = app_state.rpc_router.clone();
let task = cx.spawn(|this, mut cx| async move {
- rpc.log_in_and_connect(router, cx.clone()).await?;
+ rpc.authenticate_and_connect(&cx).await?;
let worktree_url = cx
.platform()
@@ -778,7 +845,7 @@ impl Workspace {
Worktree::open_remote(rpc.clone(), worktree_id, access_token, languages, &mut cx)
.await?;
this.update(&mut cx, |workspace, cx| {
- cx.observe_model(&worktree, |_, _, cx| cx.notify());
+ cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
workspace.worktrees.insert(worktree);
cx.notify();
});
@@ -797,9 +864,10 @@ impl Workspace {
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
let pane = cx.add_view(|_| Pane::new(self.settings.clone()));
let pane_id = pane.id();
- cx.subscribe_to_view(&pane, move |me, _, event, cx| {
+ cx.subscribe(&pane, move |me, _, event, cx| {
me.handle_pane_event(pane_id, event, cx)
- });
+ })
+ .detach();
self.panes.push(pane.clone());
self.activate_pane(pane.clone(), cx);
pane
@@ -879,15 +947,46 @@ impl View for Workspace {
"Workspace"
}
- fn render(&self, _: &RenderContext<Self>) -> ElementBox {
+ fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let settings = self.settings.borrow();
Container::new(
- Stack::new()
- .with_child(self.center.render())
- .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()))
+ Flex::column()
+ .with_child(
+ ConstrainedBox::new(Empty::new().boxed())
+ .with_height(32.)
+ .named("titlebar"),
+ )
+ .with_child(
+ Expanded::new(
+ 1.0,
+ Stack::new()
+ .with_child({
+ let mut content = Flex::row();
+ content.add_child(self.left_sidebar.render(&settings, cx));
+ if let Some(element) =
+ self.left_sidebar.render_active_item(&settings, cx)
+ {
+ content.add_child(Flexible::new(0.8, element).boxed());
+ }
+ content.add_child(Expanded::new(1.0, self.center.render()).boxed());
+ if let Some(element) =
+ self.right_sidebar.render_active_item(&settings, cx)
+ {
+ content.add_child(Flexible::new(0.8, element).boxed());
+ }
+ content.add_child(self.right_sidebar.render(&settings, cx));
+ content.boxed()
+ })
+ .with_children(
+ self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()),
+ )
+ .boxed(),
+ )
+ .boxed(),
+ )
.boxed(),
)
- .with_background_color(settings.theme.ui.background)
+ .with_background_color(settings.theme.workspace.background)
.named("workspace")
}
@@ -921,9 +1020,9 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
mod tests {
use super::*;
use crate::{
- editor::Editor,
+ editor::{Editor, Insert},
fs::FakeFs,
- test::{build_app_state, temp_tree},
+ test::{temp_tree, test_app_state},
worktree::WorktreeHandle,
};
use serde_json::json;
@@ -932,7 +1031,7 @@ mod tests {
#[gpui::test]
async fn test_open_paths_action(mut cx: gpui::TestAppContext) {
- let app_state = cx.read(build_app_state);
+ let app_state = cx.update(test_app_state);
let dir = temp_tree(json!({
"a": {
"aa": null,
@@ -950,13 +1049,13 @@ mod tests {
cx.update(|cx| {
open_paths(
- &OpenParams {
+ &OpenPaths(OpenParams {
paths: vec![
dir.path().join("a").to_path_buf(),
dir.path().join("b").to_path_buf(),
],
app_state: app_state.clone(),
- },
+ }),
cx,
)
})
@@ -965,10 +1064,10 @@ mod tests {
cx.update(|cx| {
open_paths(
- &OpenParams {
+ &OpenPaths(OpenParams {
paths: vec![dir.path().join("a").to_path_buf()],
app_state: app_state.clone(),
- },
+ }),
cx,
)
})
@@ -981,13 +1080,13 @@ mod tests {
cx.update(|cx| {
open_paths(
- &OpenParams {
+ &OpenPaths(OpenParams {
paths: vec![
dir.path().join("b").to_path_buf(),
dir.path().join("c").to_path_buf(),
],
app_state: app_state.clone(),
- },
+ }),
cx,
)
})
@@ -1005,7 +1104,7 @@ mod tests {
},
}));
- let app_state = cx.read(build_app_state);
+ let app_state = cx.update(test_app_state);
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
workspace
@@ -1109,7 +1208,7 @@ mod tests {
fs.insert_file("/dir1/a.txt", "".into()).await.unwrap();
fs.insert_file("/dir2/b.txt", "".into()).await.unwrap();
- let mut app_state = cx.read(build_app_state);
+ let mut app_state = cx.update(test_app_state);
Arc::get_mut(&mut app_state).unwrap().fs = Arc::new(fs);
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
@@ -1178,7 +1277,7 @@ mod tests {
"a.txt": "",
}));
- let app_state = cx.read(build_app_state);
+ let app_state = cx.update(test_app_state);
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
workspace
.update(&mut cx, |workspace, cx| {
@@ -1205,14 +1304,14 @@ mod tests {
item.to_any().downcast::<Editor>().unwrap()
});
- cx.update(|cx| editor.update(cx, |editor, cx| editor.insert(&"x".to_string(), cx)));
+ cx.update(|cx| editor.update(cx, |editor, cx| editor.insert(&Insert("x".into()), cx)));
fs::write(dir.path().join("a.txt"), "changed").unwrap();
editor
.condition(&cx, |editor, cx| editor.has_conflict(cx))
.await;
cx.read(|cx| assert!(editor.is_dirty(cx)));
- cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&(), cx)));
+ cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&Save, cx)));
cx.simulate_prompt_answer(window_id, 0);
editor
.condition(&cx, |editor, cx| !editor.is_dirty(cx))
@@ -1223,7 +1322,7 @@ mod tests {
#[gpui::test]
async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) {
let dir = TempDir::new("test-new-file").unwrap();
- let app_state = cx.read(build_app_state);
+ let app_state = cx.update(test_app_state);
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
workspace
.update(&mut cx, |workspace, cx| {
@@ -1244,7 +1343,7 @@ mod tests {
// Create a new untitled buffer
let editor = workspace.update(&mut cx, |workspace, cx| {
- workspace.open_new_file(&app_state, cx);
+ workspace.open_new_file(&OpenNew(app_state.clone()), cx);
workspace
.active_item(cx)
.unwrap()
@@ -1256,12 +1355,14 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert!(!editor.is_dirty(cx.as_ref()));
assert_eq!(editor.title(cx.as_ref()), "untitled");
- editor.insert(&"hi".to_string(), cx);
+ editor.insert(&Insert("hi".into()), cx);
assert!(editor.is_dirty(cx.as_ref()));
});
// Save the buffer. This prompts for a filename.
- workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(&(), cx));
+ workspace.update(&mut cx, |workspace, cx| {
+ workspace.save_active_item(&Save, cx)
+ });
cx.simulate_new_path_selection(|parent_dir| {
assert_eq!(parent_dir, dir.path());
Some(parent_dir.join("the-new-name"))
@@ -1282,10 +1383,12 @@ mod tests {
// Edit the file and save it again. This time, there is no filename prompt.
editor.update(&mut cx, |editor, cx| {
- editor.insert(&" there".to_string(), cx);
+ editor.insert(&Insert(" there".into()), cx);
assert_eq!(editor.is_dirty(cx.as_ref()), true);
});
- workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(&(), cx));
+ workspace.update(&mut cx, |workspace, cx| {
+ workspace.save_active_item(&Save, cx)
+ });
assert!(!cx.did_prompt_for_new_path());
editor
.condition(&cx, |editor, cx| !editor.is_dirty(cx))
@@ -1295,7 +1398,7 @@ mod tests {
// Open the same newly-created file in another pane item. The new editor should reuse
// the same buffer.
workspace.update(&mut cx, |workspace, cx| {
- workspace.open_new_file(&app_state, cx);
+ 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)
@@ -1318,8 +1421,8 @@ mod tests {
async fn test_new_empty_workspace(mut cx: gpui::TestAppContext) {
cx.update(init);
- let app_state = cx.read(build_app_state);
- cx.dispatch_global_action("workspace:new_file", app_state);
+ let app_state = cx.update(test_app_state);
+ cx.dispatch_global_action(OpenNew(app_state));
let window_id = *cx.window_ids().first().unwrap();
let workspace = cx.root_view::<Workspace>(window_id).unwrap();
let editor = workspace.update(&mut cx, |workspace, cx| {
@@ -1335,7 +1438,9 @@ mod tests {
assert!(editor.text(cx).is_empty());
});
- workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(&(), cx));
+ workspace.update(&mut cx, |workspace, cx| {
+ workspace.save_active_item(&Save, cx)
+ });
let dir = TempDir::new("test-new-empty-workspace").unwrap();
cx.simulate_new_path_selection(|_| {
@@ -1362,7 +1467,7 @@ mod tests {
},
}));
- let app_state = cx.read(build_app_state);
+ let app_state = cx.update(test_app_state);
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
workspace
.update(&mut cx, |workspace, cx| {
@@ -1388,7 +1493,11 @@ mod tests {
);
});
- cx.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ());
+ cx.dispatch_action(
+ window_id,
+ vec![pane_1.id()],
+ pane::Split(SplitDirection::Right),
+ );
cx.update(|cx| {
let pane_2 = workspace.read(cx).active_pane().clone();
assert_ne!(pane_1, pane_2);
@@ -1396,7 +1505,7 @@ mod tests {
let pane2_item = pane_2.read(cx).active_item().unwrap();
assert_eq!(pane2_item.entry_id(cx.as_ref()), Some(file1.clone()));
- cx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ());
+ cx.dispatch_action(window_id, vec![pane_2.id()], &CloseActiveItem);
let workspace = workspace.read(cx);
assert_eq!(workspace.panes.len(), 1);
assert_eq!(workspace.active_pane(), &pane_1);
@@ -1,56 +1,51 @@
use super::{ItemViewHandle, SplitDirection};
use crate::{settings::Settings, theme};
use gpui::{
+ action,
color::Color,
elements::*,
geometry::{rect::RectF, vector::vec2f},
keymap::Binding,
- AppContext, Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext,
- ViewHandle,
+ Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle,
};
use postage::watch;
use std::{cmp, path::Path, sync::Arc};
+action!(Split, SplitDirection);
+action!(ActivateItem, usize);
+action!(ActivatePrevItem);
+action!(ActivateNextItem);
+action!(CloseActiveItem);
+action!(CloseItem, usize);
+
pub fn init(cx: &mut MutableAppContext) {
- cx.add_action(
- "pane:activate_item",
- |pane: &mut Pane, index: &usize, cx| {
- pane.activate_item(*index, cx);
- },
- );
- cx.add_action("pane:activate_prev_item", |pane: &mut Pane, _: &(), cx| {
+ cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
+ pane.activate_item(action.0, cx);
+ });
+ cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
pane.activate_prev_item(cx);
});
- cx.add_action("pane:activate_next_item", |pane: &mut Pane, _: &(), cx| {
+ cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
pane.activate_next_item(cx);
});
- cx.add_action("pane:close_active_item", |pane: &mut Pane, _: &(), cx| {
+ cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| {
pane.close_active_item(cx);
});
- cx.add_action("pane:close_item", |pane: &mut Pane, item_id: &usize, cx| {
- pane.close_item(*item_id, cx);
- });
- cx.add_action("pane:split_up", |pane: &mut Pane, _: &(), cx| {
- pane.split(SplitDirection::Up, cx);
- });
- cx.add_action("pane:split_down", |pane: &mut Pane, _: &(), cx| {
- pane.split(SplitDirection::Down, cx);
- });
- cx.add_action("pane:split_left", |pane: &mut Pane, _: &(), cx| {
- pane.split(SplitDirection::Left, cx);
+ cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| {
+ pane.close_item(action.0, cx);
});
- cx.add_action("pane:split_right", |pane: &mut Pane, _: &(), cx| {
- pane.split(SplitDirection::Right, cx);
+ cx.add_action(|pane: &mut Pane, action: &Split, cx| {
+ pane.split(action.0, cx);
});
cx.add_bindings(vec![
- Binding::new("shift-cmd-{", "pane:activate_prev_item", Some("Pane")),
- Binding::new("shift-cmd-}", "pane:activate_next_item", Some("Pane")),
- Binding::new("cmd-w", "pane:close_active_item", Some("Pane")),
- Binding::new("cmd-k up", "pane:split_up", Some("Pane")),
- Binding::new("cmd-k down", "pane:split_down", Some("Pane")),
- Binding::new("cmd-k left", "pane:split_left", Some("Pane")),
- Binding::new("cmd-k right", "pane:split_right", Some("Pane")),
+ Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")),
+ Binding::new("shift-cmd-}", ActivateNextItem, Some("Pane")),
+ Binding::new("cmd-w", CloseActiveItem, Some("Pane")),
+ Binding::new("cmd-k up", Split(SplitDirection::Up), Some("Pane")),
+ Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
+ Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
+ Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
]);
}
@@ -179,12 +174,12 @@ impl Pane {
cx.emit(Event::Split(direction));
}
- fn render_tabs(&self, cx: &AppContext) -> ElementBox {
+ fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let settings = self.settings.borrow();
- let theme = &settings.theme.ui;
+ let theme = &settings.theme;
let line_height = cx.font_cache().line_height(
- cx.font_cache().default_font(settings.ui_font_family),
- settings.ui_font_size,
+ theme.workspace.tab.label.text.font_id,
+ theme.workspace.tab.label.text.font_size,
);
let mut row = Flex::row();
@@ -193,12 +188,12 @@ impl Pane {
let is_active = ix == self.active_item;
enum Tab {}
- let border = &theme.tab.container.border;
+ let border = &theme.workspace.tab.container.border;
row.add_child(
- Expanded::new(
+ Flexible::new(
1.0,
- MouseEventHandler::new::<Tab, _>(item.id(), cx, |mouse_state| {
+ MouseEventHandler::new::<Tab, _, _, _>(item.id(), cx, |mouse_state, cx| {
let title = item.title(cx);
let mut border = border.clone();
@@ -212,14 +207,12 @@ impl Pane {
Align::new(
Label::new(
title,
- settings.ui_font_family,
- settings.ui_font_size,
+ if is_active {
+ theme.workspace.active_tab.label.clone()
+ } else {
+ theme.workspace.tab.label.clone()
+ },
)
- .with_style(if is_active {
- &theme.active_tab.label
- } else {
- &theme.tab.label
- })
.boxed(),
)
.boxed(),
@@ -240,9 +233,9 @@ impl Pane {
.boxed(),
)
.with_style(if is_active {
- &theme.active_tab.container
+ &theme.workspace.active_tab.container
} else {
- &theme.tab.container
+ &theme.workspace.tab.container
})
.with_border(border);
@@ -253,7 +246,7 @@ impl Pane {
ConstrainedBox::new(
EventHandler::new(container.boxed())
.on_mouse_down(move |cx| {
- cx.dispatch_action("pane:activate_item", ix);
+ cx.dispatch_action(ActivateItem(ix));
true
})
.boxed(),
@@ -271,7 +264,7 @@ impl Pane {
// Ensure there's always a minimum amount of space after the last tab,
// so that the tab's border doesn't abut the window's border.
let mut border = Border::bottom(1.0, Color::default());
- border.color = theme.tab.container.border.color;
+ border.color = theme.workspace.tab.container.border.color;
row.add_child(
ConstrainedBox::new(
@@ -304,33 +297,33 @@ impl Pane {
tab_hovered: bool,
is_dirty: bool,
has_conflict: bool,
- theme: &theme::Ui,
- cx: &AppContext,
+ theme: &theme::Theme,
+ cx: &mut RenderContext<Self>,
) -> ElementBox {
enum TabCloseButton {}
- let mut clicked_color = theme.tab.icon_dirty;
+ let mut clicked_color = theme.workspace.tab.icon_dirty;
clicked_color.a = 180;
let current_color = if has_conflict {
- Some(theme.tab.icon_conflict)
+ Some(theme.workspace.tab.icon_conflict)
} else if is_dirty {
- Some(theme.tab.icon_dirty)
+ Some(theme.workspace.tab.icon_dirty)
} else {
None
};
let icon = if tab_hovered {
- let close_color = current_color.unwrap_or(theme.tab.icon_close);
+ let close_color = current_color.unwrap_or(theme.workspace.tab.icon_close);
let icon = Svg::new("icons/x.svg").with_color(close_color);
- MouseEventHandler::new::<TabCloseButton, _>(item_id, cx, |mouse_state| {
+ MouseEventHandler::new::<TabCloseButton, _, _, _>(item_id, cx, |mouse_state, _| {
if mouse_state.hovered {
Container::new(icon.with_color(Color::white()).boxed())
.with_background_color(if mouse_state.clicked {
clicked_color
} else {
- theme.tab.icon_dirty
+ theme.workspace.tab.icon_dirty
})
.with_corner_radius(close_icon_size / 2.)
.boxed()
@@ -338,12 +331,12 @@ impl Pane {
icon.boxed()
}
})
- .on_click(move |cx| cx.dispatch_action("pane:close_item", item_id))
+ .on_click(move |cx| cx.dispatch_action(CloseItem(item_id)))
.named("close-tab-icon")
} else {
let diameter = 8.;
ConstrainedBox::new(
- Canvas::new(move |bounds, cx| {
+ Canvas::new(move |bounds, _, cx| {
if let Some(current_color) = current_color {
let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
cx.scene.push_quad(Quad {
@@ -376,7 +369,7 @@ impl View for Pane {
"Pane"
}
- fn render<'a>(&self, cx: &RenderContext<Self>) -> ElementBox {
+ fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if let Some(active_item) = self.active_item() {
Flex::column()
.with_child(self.render_tabs(cx))
@@ -184,7 +184,7 @@ impl PaneAxis {
}
}
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Debug)]
pub enum SplitDirection {
Up,
Down,
@@ -0,0 +1,174 @@
+use super::Workspace;
+use crate::Settings;
+use gpui::{
+ action, elements::*, platform::CursorStyle, AnyViewHandle, MutableAppContext, RenderContext,
+};
+use std::{cell::RefCell, rc::Rc};
+
+pub struct Sidebar {
+ side: Side,
+ items: Vec<Item>,
+ active_item_ix: Option<usize>,
+ width: Rc<RefCell<f32>>,
+}
+
+#[derive(Clone, Copy)]
+pub enum Side {
+ Left,
+ Right,
+}
+
+struct Item {
+ icon_path: &'static str,
+ view: AnyViewHandle,
+}
+
+action!(ToggleSidebarItem, ToggleArg);
+
+#[derive(Clone)]
+pub struct ToggleArg {
+ pub side: Side,
+ pub item_index: usize,
+}
+
+impl Sidebar {
+ pub fn new(side: Side) -> Self {
+ Self {
+ side,
+ items: Default::default(),
+ active_item_ix: None,
+ width: Rc::new(RefCell::new(220.)),
+ }
+ }
+
+ pub fn add_item(&mut self, icon_path: &'static str, view: AnyViewHandle) {
+ self.items.push(Item { icon_path, view });
+ }
+
+ pub fn toggle_item(&mut self, item_ix: usize) {
+ if self.active_item_ix == Some(item_ix) {
+ self.active_item_ix = None;
+ } else {
+ self.active_item_ix = Some(item_ix);
+ }
+ }
+
+ pub fn active_item(&self) -> Option<&AnyViewHandle> {
+ self.active_item_ix
+ .and_then(|ix| self.items.get(ix))
+ .map(|item| &item.view)
+ }
+
+ pub fn render(&self, settings: &Settings, cx: &mut RenderContext<Workspace>) -> ElementBox {
+ let side = self.side;
+ let theme = &settings.theme;
+ let line_height = cx.font_cache().line_height(
+ theme.workspace.tab.label.text.font_id,
+ theme.workspace.tab.label.text.font_size,
+ );
+
+ Container::new(
+ Flex::column()
+ .with_children(self.items.iter().enumerate().map(|(item_index, item)| {
+ let theme = if Some(item_index) == self.active_item_ix {
+ &settings.theme.workspace.active_sidebar_icon
+ } else {
+ &settings.theme.workspace.sidebar_icon
+ };
+ enum SidebarButton {}
+ MouseEventHandler::new::<SidebarButton, _, _, _>(item.view.id(), cx, |_, _| {
+ ConstrainedBox::new(
+ Align::new(
+ ConstrainedBox::new(
+ Svg::new(item.icon_path).with_color(theme.color).boxed(),
+ )
+ .with_height(line_height)
+ .boxed(),
+ )
+ .boxed(),
+ )
+ .with_height(line_height + 16.0)
+ .boxed()
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_mouse_down(move |cx| {
+ cx.dispatch_action(ToggleSidebarItem(ToggleArg { side, item_index }))
+ })
+ .boxed()
+ }))
+ .boxed(),
+ )
+ .with_style(&settings.theme.workspace.sidebar.icons)
+ .boxed()
+ }
+
+ pub fn render_active_item(
+ &self,
+ settings: &Settings,
+ cx: &mut MutableAppContext,
+ ) -> Option<ElementBox> {
+ if let Some(active_item) = self.active_item() {
+ let mut container = Flex::row();
+ if matches!(self.side, Side::Right) {
+ container.add_child(self.render_resize_handle(settings, cx));
+ }
+
+ container.add_child(
+ Flexible::new(
+ 1.,
+ Hook::new(
+ ConstrainedBox::new(ChildView::new(active_item.id()).boxed())
+ .with_max_width(*self.width.borrow())
+ .boxed(),
+ )
+ .on_after_layout({
+ let width = self.width.clone();
+ move |size, _| *width.borrow_mut() = size.x()
+ })
+ .boxed(),
+ )
+ .boxed(),
+ );
+ if matches!(self.side, Side::Left) {
+ container.add_child(self.render_resize_handle(settings, cx));
+ }
+ Some(container.boxed())
+ } else {
+ None
+ }
+ }
+
+ fn render_resize_handle(
+ &self,
+ settings: &Settings,
+ mut cx: &mut MutableAppContext,
+ ) -> ElementBox {
+ let width = self.width.clone();
+ let side = self.side;
+ MouseEventHandler::new::<Self, _, _, _>(self.side.id(), &mut cx, |_, _| {
+ Container::new(Empty::new().boxed())
+ .with_style(&settings.theme.workspace.sidebar.resize_handle)
+ .boxed()
+ })
+ .with_cursor_style(CursorStyle::ResizeLeftRight)
+ .on_drag(move |delta, cx| {
+ let prev_width = *width.borrow();
+ match side {
+ Side::Left => *width.borrow_mut() = 0f32.max(prev_width + delta.x()),
+ Side::Right => *width.borrow_mut() = 0f32.max(prev_width - delta.x()),
+ }
+
+ cx.notify();
+ })
+ .boxed()
+ }
+}
+
+impl Side {
+ fn id(self) -> usize {
+ match self {
+ Side::Left => 0,
+ Side::Right => 1,
+ }
+ }
+}
@@ -8,17 +8,18 @@ use crate::{
fuzzy::CharBag,
language::LanguageRegistry,
rpc::{self, proto},
- sum_tree::{self, Cursor, Edit, SumTree},
time::{self, ReplicaId},
- util::Bias,
+ util::{Bias, TryFutureExt},
};
use ::ignore::gitignore::Gitignore;
use anyhow::{anyhow, Result};
use futures::{Stream, StreamExt};
pub use fuzzy::{match_paths, PathMatch};
use gpui::{
- executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
- Task, WeakModelHandle,
+ executor,
+ sum_tree::{self, Cursor, Edit, SumTree},
+ AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
+ UpgradeModelHandle, WeakModelHandle,
};
use lazy_static::lazy_static;
use parking_lot::Mutex;
@@ -42,23 +43,12 @@ use std::{
},
time::{Duration, SystemTime},
};
-use zrpc::{ForegroundRouter, PeerId, TypedEnvelope};
+use zrpc::{PeerId, TypedEnvelope};
lazy_static! {
static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
}
-pub fn init(cx: &mut MutableAppContext, rpc: &rpc::Client, router: &mut ForegroundRouter) {
- rpc.on_message(router, remote::add_peer, cx);
- rpc.on_message(router, remote::remove_peer, cx);
- rpc.on_message(router, remote::update_worktree, cx);
- rpc.on_message(router, remote::open_buffer, cx);
- rpc.on_message(router, remote::close_buffer, cx);
- rpc.on_message(router, remote::update_buffer, cx);
- rpc.on_message(router, remote::buffer_saved, cx);
- rpc.on_message(router, remote::save_buffer, cx);
-}
-
#[derive(Clone, Debug)]
enum ScanState {
Idle,
@@ -76,17 +66,15 @@ impl Entity for Worktree {
fn release(&mut self, cx: &mut MutableAppContext) {
let rpc = match self {
- Self::Local(tree) => tree.rpc.clone(),
+ Self::Local(tree) => tree
+ .share
+ .as_ref()
+ .map(|share| (share.rpc.clone(), share.remote_id)),
Self::Remote(tree) => Some((tree.rpc.clone(), tree.remote_id)),
};
if let Some((rpc, worktree_id)) = rpc {
cx.spawn(|_| async move {
- rpc.state
- .write()
- .await
- .shared_worktrees
- .remove(&worktree_id);
if let Err(err) = rpc.send(proto::CloseWorktree { worktree_id }).await {
log::error!("error closing worktree {}: {}", worktree_id, err);
}
@@ -120,7 +108,7 @@ impl Worktree {
}
pub async fn open_remote(
- rpc: rpc::Client,
+ rpc: Arc<rpc::Client>,
id: u64,
access_token: String,
languages: Arc<LanguageRegistry>,
@@ -138,7 +126,7 @@ impl Worktree {
async fn remote(
open_response: proto::OpenWorktreeResponse,
- rpc: rpc::Client,
+ rpc: Arc<rpc::Client>,
languages: Arc<LanguageRegistry>,
cx: &mut AsyncAppContext,
) -> Result<ModelHandle<Self>> {
@@ -226,6 +214,14 @@ impl Worktree {
.detach();
}
+ let _subscriptions = vec![
+ rpc.subscribe_from_model(remote_id, cx, Self::handle_add_peer),
+ rpc.subscribe_from_model(remote_id, cx, Self::handle_remove_peer),
+ rpc.subscribe_from_model(remote_id, cx, Self::handle_update),
+ rpc.subscribe_from_model(remote_id, cx, Self::handle_update_buffer),
+ rpc.subscribe_from_model(remote_id, cx, Self::handle_buffer_saved),
+ ];
+
Worktree::Remote(RemoteWorktree {
remote_id,
replica_id,
@@ -239,14 +235,10 @@ impl Worktree {
.map(|p| (PeerId(p.peer_id), p.replica_id as ReplicaId))
.collect(),
languages,
+ _subscriptions,
})
})
});
- rpc.state
- .write()
- .await
- .shared_worktrees
- .insert(open_response.worktree_id, worktree.downgrade());
Ok(worktree)
}
@@ -289,10 +281,11 @@ impl Worktree {
}
}
- pub fn add_peer(
+ pub fn handle_add_peer(
&mut self,
envelope: TypedEnvelope<proto::AddPeer>,
- cx: &mut ModelContext<Worktree>,
+ _: Arc<rpc::Client>,
+ cx: &mut ModelContext<Self>,
) -> Result<()> {
match self {
Worktree::Local(worktree) => worktree.add_peer(envelope, cx),
@@ -300,10 +293,11 @@ impl Worktree {
}
}
- pub fn remove_peer(
+ pub fn handle_remove_peer(
&mut self,
envelope: TypedEnvelope<proto::RemovePeer>,
- cx: &mut ModelContext<Worktree>,
+ _: Arc<rpc::Client>,
+ cx: &mut ModelContext<Self>,
) -> Result<()> {
match self {
Worktree::Local(worktree) => worktree.remove_peer(envelope, cx),
@@ -311,6 +305,54 @@ impl Worktree {
}
}
+ pub fn handle_update(
+ &mut self,
+ envelope: TypedEnvelope<proto::UpdateWorktree>,
+ _: Arc<rpc::Client>,
+ cx: &mut ModelContext<Self>,
+ ) -> anyhow::Result<()> {
+ self.as_remote_mut()
+ .unwrap()
+ .update_from_remote(envelope, cx)
+ }
+
+ pub fn handle_open_buffer(
+ &mut self,
+ envelope: TypedEnvelope<proto::OpenBuffer>,
+ rpc: Arc<rpc::Client>,
+ cx: &mut ModelContext<Self>,
+ ) -> anyhow::Result<()> {
+ let receipt = envelope.receipt();
+
+ let response = self
+ .as_local_mut()
+ .unwrap()
+ .open_remote_buffer(envelope, cx);
+
+ cx.background()
+ .spawn(
+ async move {
+ rpc.respond(receipt, response.await?).await?;
+ Ok(())
+ }
+ .log_err(),
+ )
+ .detach();
+
+ Ok(())
+ }
+
+ pub fn handle_close_buffer(
+ &mut self,
+ envelope: TypedEnvelope<proto::CloseBuffer>,
+ _: Arc<rpc::Client>,
+ cx: &mut ModelContext<Self>,
+ ) -> anyhow::Result<()> {
+ self.as_local_mut()
+ .unwrap()
+ .close_remote_buffer(envelope, cx)
+ }
+
pub fn peers(&self) -> &HashMap<PeerId, ReplicaId> {
match self {
Worktree::Local(worktree) => &worktree.peers,
@@ -356,13 +398,15 @@ impl Worktree {
.is_some()
}
- pub fn update_buffer(
+ pub fn handle_update_buffer(
&mut self,
- envelope: proto::UpdateBuffer,
+ envelope: TypedEnvelope<proto::UpdateBuffer>,
+ _: Arc<rpc::Client>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
- let buffer_id = envelope.buffer_id as usize;
- let ops = envelope
+ let payload = envelope.payload.clone();
+ let buffer_id = payload.buffer_id as usize;
+ let ops = payload
.operations
.into_iter()
.map(|op| op.try_into())
@@ -373,7 +417,7 @@ impl Worktree {
let buffer = worktree
.open_buffers
.get(&buffer_id)
- .and_then(|buf| buf.upgrade(&cx))
+ .and_then(|buf| buf.upgrade(cx))
.ok_or_else(|| {
anyhow!("invalid buffer {} in update buffer message", buffer_id)
})?;
@@ -382,7 +426,7 @@ impl Worktree {
Worktree::Remote(worktree) => match worktree.open_buffers.get_mut(&buffer_id) {
Some(RemoteBuffer::Operations(pending_ops)) => pending_ops.extend(ops),
Some(RemoteBuffer::Loaded(buffer)) => {
- if let Some(buffer) = buffer.upgrade(&cx) {
+ if let Some(buffer) = buffer.upgrade(cx) {
buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))?;
} else {
worktree
@@ -401,34 +445,77 @@ impl Worktree {
Ok(())
}
- pub fn buffer_saved(
+ pub fn handle_save_buffer(
&mut self,
- message: proto::BufferSaved,
+ envelope: TypedEnvelope<proto::SaveBuffer>,
+ rpc: Arc<rpc::Client>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
- if let Worktree::Remote(worktree) = self {
- if let Some(buffer) = worktree
- .open_buffers
- .get(&(message.buffer_id as usize))
- .and_then(|buf| buf.upgrade(&cx))
- {
- buffer.update(cx, |buffer, cx| {
- let version = message.version.try_into()?;
- let mtime = message
- .mtime
- .ok_or_else(|| anyhow!("missing mtime"))?
- .into();
- buffer.did_save(version, mtime, cx);
- Result::<_, anyhow::Error>::Ok(())
- })?;
- }
- Ok(())
- } else {
- Err(anyhow!(
- "invalid buffer {} in buffer saved message",
- message.buffer_id
- ))
+ let sender_id = envelope.original_sender_id()?;
+ let buffer = self
+ .as_local()
+ .unwrap()
+ .shared_buffers
+ .get(&sender_id)
+ .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned())
+ .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?;
+
+ let receipt = envelope.receipt();
+ let worktree_id = envelope.payload.worktree_id;
+ let buffer_id = envelope.payload.buffer_id;
+ let save = cx.spawn(|_, mut cx| async move {
+ buffer.update(&mut cx, |buffer, cx| buffer.save(cx))?.await
+ });
+
+ cx.background()
+ .spawn(
+ async move {
+ let (version, mtime) = save.await?;
+
+ rpc.respond(
+ receipt,
+ proto::BufferSaved {
+ worktree_id,
+ buffer_id,
+ version: (&version).into(),
+ mtime: Some(mtime.into()),
+ },
+ )
+ .await?;
+
+ Ok(())
+ }
+ .log_err(),
+ )
+ .detach();
+
+ Ok(())
+ }
+
+ pub fn handle_buffer_saved(
+ &mut self,
+ envelope: TypedEnvelope<proto::BufferSaved>,
+ _: Arc<rpc::Client>,
+ cx: &mut ModelContext<Self>,
+ ) -> Result<()> {
+ let payload = envelope.payload.clone();
+ let worktree = self.as_remote_mut().unwrap();
+ if let Some(buffer) = worktree
+ .open_buffers
+ .get(&(payload.buffer_id as usize))
+ .and_then(|buf| buf.upgrade(cx))
+ {
+ buffer.update(cx, |buffer, cx| {
+ let version = payload.version.try_into()?;
+ let mtime = payload
+ .mtime
+ .ok_or_else(|| anyhow!("missing mtime"))?
+ .into();
+ buffer.did_save(version, mtime, cx);
+ Result::<_, anyhow::Error>::Ok(())
+ })?;
}
+ Ok(())
}
fn poll_snapshot(&mut self, cx: &mut ModelContext<Self>) {
@@ -480,7 +567,7 @@ impl Worktree {
let mut buffers_to_delete = Vec::new();
for (buffer_id, buffer) in open_buffers {
- if let Some(buffer) = buffer.upgrade(&cx) {
+ if let Some(buffer) = buffer.upgrade(cx) {
buffer.update(cx, |buffer, cx| {
let buffer_is_clean = !buffer.is_dirty();
@@ -561,11 +648,10 @@ impl Deref for Worktree {
pub struct LocalWorktree {
snapshot: Snapshot,
background_snapshot: Arc<Mutex<Snapshot>>,
- snapshots_to_send_tx: Option<Sender<Snapshot>>,
last_scan_state_rx: watch::Receiver<ScanState>,
_background_scanner_task: Option<Task<()>>,
poll_task: Option<Task<()>>,
- rpc: Option<(rpc::Client, u64)>,
+ share: Option<ShareState>,
open_buffers: HashMap<usize, WeakModelHandle<Buffer>>,
shared_buffers: HashMap<PeerId, HashMap<u64, ModelHandle<Buffer>>>,
peers: HashMap<PeerId, ReplicaId>,
@@ -619,30 +705,27 @@ impl LocalWorktree {
let tree = Self {
snapshot: snapshot.clone(),
background_snapshot: Arc::new(Mutex::new(snapshot)),
- snapshots_to_send_tx: None,
last_scan_state_rx,
_background_scanner_task: None,
+ share: None,
poll_task: None,
open_buffers: Default::default(),
shared_buffers: Default::default(),
peers: Default::default(),
- rpc: None,
languages,
fs,
};
cx.spawn_weak(|this, mut cx| async move {
while let Ok(scan_state) = scan_states_rx.recv().await {
- if let Some(handle) = cx.read(|cx| this.upgrade(&cx)) {
+ if let Some(handle) = cx.read(|cx| this.upgrade(cx)) {
let to_send = handle.update(&mut cx, |this, cx| {
last_scan_state_tx.blocking_send(scan_state).ok();
this.poll_snapshot(cx);
let tree = this.as_local_mut().unwrap();
if !tree.is_scanning() {
- if let Some(snapshots_to_send_tx) =
- tree.snapshots_to_send_tx.clone()
- {
- Some((tree.snapshot(), snapshots_to_send_tx))
+ if let Some(share) = tree.share.as_ref() {
+ Some((tree.snapshot(), share.snapshots_tx.clone()))
} else {
None
}
@@ -745,10 +828,11 @@ impl LocalWorktree {
pub fn close_remote_buffer(
&mut self,
envelope: TypedEnvelope<proto::CloseBuffer>,
- _: &mut ModelContext<Worktree>,
+ cx: &mut ModelContext<Worktree>,
) -> Result<()> {
if let Some(shared_buffers) = self.shared_buffers.get_mut(&envelope.original_sender_id()?) {
shared_buffers.remove(&envelope.payload.buffer_id);
+ cx.notify();
}
Ok(())
@@ -759,10 +843,15 @@ impl LocalWorktree {
envelope: TypedEnvelope<proto::AddPeer>,
cx: &mut ModelContext<Worktree>,
) -> Result<()> {
- let peer = envelope.payload.peer.ok_or_else(|| anyhow!("empty peer"))?;
+ let peer = envelope
+ .payload
+ .peer
+ .as_ref()
+ .ok_or_else(|| anyhow!("empty peer"))?;
self.peers
.insert(PeerId(peer.peer_id), peer.replica_id as ReplicaId);
cx.notify();
+
Ok(())
}
@@ -778,11 +867,12 @@ impl LocalWorktree {
.ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))?;
self.shared_buffers.remove(&peer_id);
for (_, buffer) in &self.open_buffers {
- if let Some(buffer) = buffer.upgrade(&cx) {
+ if let Some(buffer) = buffer.upgrade(cx) {
buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx));
}
}
cx.notify();
+
Ok(())
}
@@ -883,21 +973,15 @@ impl LocalWorktree {
pub fn share(
&mut self,
- rpc: rpc::Client,
+ rpc: Arc<rpc::Client>,
cx: &mut ModelContext<Worktree>,
) -> Task<anyhow::Result<(u64, String)>> {
let snapshot = self.snapshot();
let share_request = self.share_request(cx);
- let handle = cx.handle();
cx.spawn(|this, mut cx| async move {
let share_request = share_request.await;
let share_response = rpc.request(share_request).await?;
-
- rpc.state
- .write()
- .await
- .shared_worktrees
- .insert(share_response.worktree_id, handle.downgrade());
+ let remote_id = share_response.worktree_id;
log::info!("sharing worktree {:?}", share_response);
let (snapshots_to_send_tx, snapshots_to_send_rx) =
@@ -906,11 +990,10 @@ impl LocalWorktree {
cx.background()
.spawn({
let rpc = rpc.clone();
- let worktree_id = share_response.worktree_id;
async move {
let mut prev_snapshot = snapshot;
while let Ok(snapshot) = snapshots_to_send_rx.recv().await {
- let message = snapshot.build_update(&prev_snapshot, worktree_id);
+ let message = snapshot.build_update(&prev_snapshot, remote_id);
match rpc.send(message).await {
Ok(()) => prev_snapshot = snapshot,
Err(err) => log::error!("error sending snapshot diff {}", err),
@@ -920,13 +1003,26 @@ impl LocalWorktree {
})
.detach();
- this.update(&mut cx, |worktree, _| {
+ this.update(&mut cx, |worktree, cx| {
+ let _subscriptions = vec![
+ rpc.subscribe_from_model(remote_id, cx, Worktree::handle_add_peer),
+ rpc.subscribe_from_model(remote_id, cx, Worktree::handle_remove_peer),
+ rpc.subscribe_from_model(remote_id, cx, Worktree::handle_open_buffer),
+ rpc.subscribe_from_model(remote_id, cx, Worktree::handle_close_buffer),
+ rpc.subscribe_from_model(remote_id, cx, Worktree::handle_update_buffer),
+ rpc.subscribe_from_model(remote_id, cx, Worktree::handle_save_buffer),
+ ];
+
let worktree = worktree.as_local_mut().unwrap();
- worktree.rpc = Some((rpc, share_response.worktree_id));
- worktree.snapshots_to_send_tx = Some(snapshots_to_send_tx);
+ worktree.share = Some(ShareState {
+ rpc,
+ remote_id: share_response.worktree_id,
+ snapshots_tx: snapshots_to_send_tx,
+ _subscriptions,
+ });
});
- Ok((share_response.worktree_id, share_response.access_token))
+ Ok((remote_id, share_response.access_token))
})
}
@@ -978,16 +1074,24 @@ impl fmt::Debug for LocalWorktree {
}
}
+struct ShareState {
+ rpc: Arc<rpc::Client>,
+ remote_id: u64,
+ snapshots_tx: Sender<Snapshot>,
+ _subscriptions: Vec<rpc::Subscription>,
+}
+
pub struct RemoteWorktree {
remote_id: u64,
snapshot: Snapshot,
snapshot_rx: watch::Receiver<Snapshot>,
- rpc: rpc::Client,
+ rpc: Arc<rpc::Client>,
updates_tx: postage::mpsc::Sender<proto::UpdateWorktree>,
replica_id: ReplicaId,
open_buffers: HashMap<usize, RemoteBuffer>,
peers: HashMap<PeerId, ReplicaId>,
languages: Arc<LanguageRegistry>,
+ _subscriptions: Vec<rpc::Subscription>,
}
impl RemoteWorktree {
@@ -1055,12 +1159,32 @@ impl RemoteWorktree {
self.snapshot.clone()
}
+ fn update_from_remote(
+ &mut self,
+ envelope: TypedEnvelope<proto::UpdateWorktree>,
+ cx: &mut ModelContext<Worktree>,
+ ) -> Result<()> {
+ let mut tx = self.updates_tx.clone();
+ let payload = envelope.payload.clone();
+ cx.background()
+ .spawn(async move {
+ tx.send(payload).await.expect("receiver runs to completion");
+ })
+ .detach();
+
+ Ok(())
+ }
+
pub fn add_peer(
&mut self,
envelope: TypedEnvelope<proto::AddPeer>,
cx: &mut ModelContext<Worktree>,
) -> Result<()> {
- let peer = envelope.payload.peer.ok_or_else(|| anyhow!("empty peer"))?;
+ let peer = envelope
+ .payload
+ .peer
+ .as_ref()
+ .ok_or_else(|| anyhow!("empty peer"))?;
self.peers
.insert(PeerId(peer.peer_id), peer.replica_id as ReplicaId);
cx.notify();
@@ -1078,7 +1202,7 @@ impl RemoteWorktree {
.remove(&peer_id)
.ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))?;
for (_, buffer) in &self.open_buffers {
- if let Some(buffer) = buffer.upgrade(&cx) {
+ if let Some(buffer) = buffer.upgrade(cx) {
buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx));
}
}
@@ -1093,7 +1217,7 @@ enum RemoteBuffer {
}
impl RemoteBuffer {
- fn upgrade(&self, cx: impl AsRef<AppContext>) -> Option<ModelHandle<Buffer>> {
+ fn upgrade(&self, cx: &impl UpgradeModelHandle) -> Option<ModelHandle<Buffer>> {
match self {
Self::Operations(_) => None,
Self::Loaded(buffer) => buffer.upgrade(cx),
@@ -1420,7 +1544,10 @@ impl File {
pub fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) {
self.worktree.update(cx, |worktree, cx| {
if let Some((rpc, remote_id)) = match worktree {
- Worktree::Local(worktree) => worktree.rpc.clone(),
+ Worktree::Local(worktree) => worktree
+ .share
+ .as_ref()
+ .map(|share| (share.rpc.clone(), share.remote_id)),
Worktree::Remote(worktree) => Some((worktree.rpc.clone(), worktree.remote_id)),
} {
cx.spawn(|_, _| async move {
@@ -1497,9 +1624,12 @@ impl File {
) -> Task<Result<(time::Global, SystemTime)>> {
self.worktree.update(cx, |worktree, cx| match worktree {
Worktree::Local(worktree) => {
- let rpc = worktree.rpc.clone();
+ let rpc = worktree
+ .share
+ .as_ref()
+ .map(|share| (share.rpc.clone(), share.remote_id));
let save = worktree.save(self.path.clone(), text, cx);
- cx.spawn(|_, _| async move {
+ cx.background().spawn(async move {
let entry = save.await?;
if let Some((rpc, worktree_id)) = rpc {
rpc.send(proto::BufferSaved {
@@ -1516,7 +1646,7 @@ impl File {
Worktree::Remote(worktree) => {
let rpc = worktree.rpc.clone();
let worktree_id = worktree.remote_id;
- cx.spawn(|_, _| async move {
+ cx.foreground().spawn(async move {
let response = rpc
.request(proto::SaveBuffer {
worktree_id,
@@ -2383,167 +2513,6 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
}
}
-mod remote {
- use super::*;
-
- pub async fn add_peer(
- envelope: TypedEnvelope<proto::AddPeer>,
- rpc: &rpc::Client,
- cx: &mut AsyncAppContext,
- ) -> anyhow::Result<()> {
- rpc.state
- .read()
- .await
- .shared_worktree(envelope.payload.worktree_id, cx)?
- .update(cx, |worktree, cx| worktree.add_peer(envelope, cx))
- }
-
- pub async fn remove_peer(
- envelope: TypedEnvelope<proto::RemovePeer>,
- rpc: &rpc::Client,
- cx: &mut AsyncAppContext,
- ) -> anyhow::Result<()> {
- rpc.state
- .read()
- .await
- .shared_worktree(envelope.payload.worktree_id, cx)?
- .update(cx, |worktree, cx| worktree.remove_peer(envelope, cx))
- }
-
- pub async fn update_worktree(
- envelope: TypedEnvelope<proto::UpdateWorktree>,
- rpc: &rpc::Client,
- cx: &mut AsyncAppContext,
- ) -> anyhow::Result<()> {
- rpc.state
- .read()
- .await
- .shared_worktree(envelope.payload.worktree_id, cx)?
- .update(cx, |worktree, _| {
- if let Some(worktree) = worktree.as_remote_mut() {
- let mut tx = worktree.updates_tx.clone();
- Ok(async move {
- tx.send(envelope.payload)
- .await
- .expect("receiver runs to completion");
- })
- } else {
- Err(anyhow!(
- "invalid update message for local worktree {}",
- envelope.payload.worktree_id
- ))
- }
- })?
- .await;
-
- Ok(())
- }
-
- pub async fn open_buffer(
- envelope: TypedEnvelope<proto::OpenBuffer>,
- rpc: &rpc::Client,
- cx: &mut AsyncAppContext,
- ) -> anyhow::Result<()> {
- let receipt = envelope.receipt();
- let worktree = rpc
- .state
- .read()
- .await
- .shared_worktree(envelope.payload.worktree_id, cx)?;
-
- let response = worktree
- .update(cx, |worktree, cx| {
- worktree
- .as_local_mut()
- .unwrap()
- .open_remote_buffer(envelope, cx)
- })
- .await?;
-
- rpc.respond(receipt, response).await?;
-
- Ok(())
- }
-
- pub async fn close_buffer(
- envelope: TypedEnvelope<proto::CloseBuffer>,
- rpc: &rpc::Client,
- cx: &mut AsyncAppContext,
- ) -> anyhow::Result<()> {
- let worktree = rpc
- .state
- .read()
- .await
- .shared_worktree(envelope.payload.worktree_id, cx)?;
-
- worktree.update(cx, |worktree, cx| {
- worktree
- .as_local_mut()
- .unwrap()
- .close_remote_buffer(envelope, cx)
- })
- }
-
- pub async fn update_buffer(
- envelope: TypedEnvelope<proto::UpdateBuffer>,
- rpc: &rpc::Client,
- cx: &mut AsyncAppContext,
- ) -> anyhow::Result<()> {
- let message = envelope.payload;
- rpc.state
- .read()
- .await
- .shared_worktree(message.worktree_id, cx)?
- .update(cx, |tree, cx| tree.update_buffer(message, cx))?;
- Ok(())
- }
-
- pub async fn save_buffer(
- envelope: TypedEnvelope<proto::SaveBuffer>,
- rpc: &rpc::Client,
- cx: &mut AsyncAppContext,
- ) -> anyhow::Result<()> {
- let state = rpc.state.read().await;
- let worktree = state.shared_worktree(envelope.payload.worktree_id, cx)?;
- let sender_id = envelope.original_sender_id()?;
- let buffer = worktree.read_with(cx, |tree, _| {
- tree.as_local()
- .unwrap()
- .shared_buffers
- .get(&sender_id)
- .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned())
- .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
- })?;
- let (version, mtime) = buffer.update(cx, |buffer, cx| buffer.save(cx))?.await?;
- rpc.respond(
- envelope.receipt(),
- proto::BufferSaved {
- worktree_id: envelope.payload.worktree_id,
- buffer_id: envelope.payload.buffer_id,
- version: (&version).into(),
- mtime: Some(mtime.into()),
- },
- )
- .await?;
- Ok(())
- }
-
- pub async fn buffer_saved(
- envelope: TypedEnvelope<proto::BufferSaved>,
- rpc: &rpc::Client,
- cx: &mut AsyncAppContext,
- ) -> anyhow::Result<()> {
- rpc.state
- .read()
- .await
- .shared_worktree(envelope.payload.worktree_id, cx)?
- .update(cx, |worktree, cx| {
- worktree.buffer_saved(envelope.payload, cx)
- })?;
- Ok(())
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -2677,13 +2646,12 @@ mod tests {
#[gpui::test]
async fn test_save_file(mut cx: gpui::TestAppContext) {
- let app_state = cx.read(build_app_state);
let dir = temp_tree(json!({
"file1": "the old contents",
}));
let tree = Worktree::open_local(
dir.path(),
- app_state.languages.clone(),
+ Arc::new(LanguageRegistry::new()),
Arc::new(RealFs),
&mut cx.to_async(),
)
@@ -2705,7 +2673,6 @@ mod tests {
#[gpui::test]
async fn test_save_in_single_file_worktree(mut cx: gpui::TestAppContext) {
- let app_state = cx.read(build_app_state);
let dir = temp_tree(json!({
"file1": "the old contents",
}));
@@ -2713,7 +2680,7 @@ mod tests {
let tree = Worktree::open_local(
file_path.clone(),
- app_state.languages.clone(),
+ Arc::new(LanguageRegistry::new()),
Arc::new(RealFs),
&mut cx.to_async(),
)
@@ -2802,7 +2769,7 @@ mod tests {
replica_id: 1,
peers: Vec::new(),
},
- rpc::Client::new(Default::default()),
+ rpc::Client::new(),
Default::default(),
&mut cx.to_async(),
)
@@ -6,32 +6,50 @@ message Envelope {
optional uint32 responding_to = 2;
optional uint32 original_sender_id = 3;
oneof payload {
- Auth auth = 4;
- AuthResponse auth_response = 5;
- ShareWorktree share_worktree = 6;
- ShareWorktreeResponse share_worktree_response = 7;
- OpenWorktree open_worktree = 8;
- OpenWorktreeResponse open_worktree_response = 9;
- UpdateWorktree update_worktree = 10;
- CloseWorktree close_worktree = 11;
- OpenBuffer open_buffer = 12;
- OpenBufferResponse open_buffer_response = 13;
- CloseBuffer close_buffer = 14;
- UpdateBuffer update_buffer = 15;
- SaveBuffer save_buffer = 16;
- BufferSaved buffer_saved = 17;
- AddPeer add_peer = 18;
- RemovePeer remove_peer = 19;
+ Error error = 4;
+ Ping ping = 5;
+ Pong pong = 6;
+ ShareWorktree share_worktree = 7;
+ ShareWorktreeResponse share_worktree_response = 8;
+ OpenWorktree open_worktree = 9;
+ OpenWorktreeResponse open_worktree_response = 10;
+ UpdateWorktree update_worktree = 11;
+ CloseWorktree close_worktree = 12;
+ OpenBuffer open_buffer = 13;
+ OpenBufferResponse open_buffer_response = 14;
+ CloseBuffer close_buffer = 15;
+ UpdateBuffer update_buffer = 16;
+ SaveBuffer save_buffer = 17;
+ BufferSaved buffer_saved = 18;
+ AddPeer add_peer = 19;
+ RemovePeer remove_peer = 20;
+ GetChannels get_channels = 21;
+ GetChannelsResponse get_channels_response = 22;
+ GetUsers get_users = 23;
+ GetUsersResponse get_users_response = 24;
+ JoinChannel join_channel = 25;
+ JoinChannelResponse join_channel_response = 26;
+ LeaveChannel leave_channel = 27;
+ SendChannelMessage send_channel_message = 28;
+ SendChannelMessageResponse send_channel_message_response = 29;
+ ChannelMessageSent channel_message_sent = 30;
+ GetChannelMessages get_channel_messages = 31;
+ GetChannelMessagesResponse get_channel_messages_response = 32;
}
}
-message Auth {
- int32 user_id = 1;
- string access_token = 2;
+// Messages
+
+message Ping {
+ int32 id = 1;
+}
+
+message Pong {
+ int32 id = 2;
}
-message AuthResponse {
- bool credentials_valid = 1;
+message Error {
+ string message = 1;
}
message ShareWorktree {
@@ -75,11 +93,6 @@ message RemovePeer {
uint32 peer_id = 2;
}
-message Peer {
- uint32 peer_id = 1;
- uint32 replica_id = 2;
-}
-
message OpenBuffer {
uint64 worktree_id = 1;
string path = 2;
@@ -112,6 +125,64 @@ message BufferSaved {
Timestamp mtime = 4;
}
+message GetChannels {}
+
+message GetChannelsResponse {
+ repeated Channel channels = 1;
+}
+
+message JoinChannel {
+ uint64 channel_id = 1;
+}
+
+message JoinChannelResponse {
+ repeated ChannelMessage messages = 1;
+ bool done = 2;
+}
+
+message LeaveChannel {
+ uint64 channel_id = 1;
+}
+
+message GetUsers {
+ repeated uint64 user_ids = 1;
+}
+
+message GetUsersResponse {
+ repeated User users = 1;
+}
+
+message SendChannelMessage {
+ uint64 channel_id = 1;
+ string body = 2;
+}
+
+message SendChannelMessageResponse {
+ ChannelMessage message = 1;
+}
+
+message ChannelMessageSent {
+ uint64 channel_id = 1;
+ ChannelMessage message = 2;
+}
+
+message GetChannelMessages {
+ uint64 channel_id = 1;
+ uint64 before_message_id = 2;
+}
+
+message GetChannelMessagesResponse {
+ repeated ChannelMessage messages = 1;
+ bool done = 2;
+}
+
+// Entities
+
+message Peer {
+ uint32 peer_id = 1;
+ uint32 replica_id = 2;
+}
+
message User {
uint64 id = 1;
string github_login = 2;
@@ -228,3 +299,15 @@ message Range {
uint64 start = 1;
uint64 end = 2;
}
+
+message Channel {
+ uint64 id = 1;
+ string name = 2;
+}
+
+message ChannelMessage {
+ uint64 id = 1;
+ string body = 2;
+ uint64 timestamp = 3;
+ uint64 sender_id = 4;
+}
@@ -1,18 +1,14 @@
-use crate::proto::{self, EnvelopedMessage, MessageStream, RequestMessage};
+use crate::proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, RequestMessage};
use anyhow::{anyhow, Context, Result};
use async_lock::{Mutex, RwLock};
use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage};
-use futures::{
- future::{BoxFuture, LocalBoxFuture},
- FutureExt, StreamExt,
-};
+use futures::{FutureExt, StreamExt};
use postage::{
mpsc,
- prelude::{Sink, Stream},
+ prelude::{Sink as _, Stream as _},
};
use std::{
- any::TypeId,
- collections::{HashMap, HashSet},
+ collections::HashMap,
fmt,
future::Future,
marker::PhantomData,
@@ -25,27 +21,42 @@ use std::{
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct ConnectionId(pub u32);
+impl fmt::Display for ConnectionId {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct PeerId(pub u32);
-type MessageHandler = Box<
- dyn Send
- + Sync
- + Fn(&mut Option<proto::Envelope>, ConnectionId) -> Option<BoxFuture<'static, ()>>,
->;
-
-type ForegroundMessageHandler =
- Box<dyn Fn(&mut Option<proto::Envelope>, ConnectionId) -> Option<LocalBoxFuture<'static, ()>>>;
+impl fmt::Display for PeerId {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
pub struct Receipt<T> {
- sender_id: ConnectionId,
- message_id: u32,
+ pub sender_id: ConnectionId,
+ pub message_id: u32,
payload_type: PhantomData<T>,
}
+impl<T> Clone for Receipt<T> {
+ fn clone(&self) -> Self {
+ Self {
+ sender_id: self.sender_id,
+ message_id: self.message_id,
+ payload_type: PhantomData,
+ }
+ }
+}
+
+impl<T> Copy for Receipt<T> {}
+
pub struct TypedEnvelope<T> {
pub sender_id: ConnectionId,
- original_sender_id: Option<PeerId>,
+ pub original_sender_id: Option<PeerId>,
pub message_id: u32,
pub payload: T,
}
@@ -67,13 +78,6 @@ impl<T: RequestMessage> TypedEnvelope<T> {
}
}
-pub type Router = RouterInternal<MessageHandler>;
-pub type ForegroundRouter = RouterInternal<ForegroundMessageHandler>;
-pub struct RouterInternal<H> {
- message_handlers: Vec<H>,
- handler_types: HashSet<TypeId>,
-}
-
pub struct Peer {
connections: RwLock<HashMap<ConnectionId, Connection>>,
next_connection_id: AtomicU32,
@@ -94,18 +98,15 @@ impl Peer {
})
}
- pub async fn add_connection<Conn, H, Fut>(
+ pub async fn add_connection<Conn>(
self: &Arc<Self>,
conn: Conn,
- router: Arc<RouterInternal<H>>,
) -> (
ConnectionId,
impl Future<Output = anyhow::Result<()>> + Send,
- impl Future<Output = ()>,
+ mpsc::Receiver<Box<dyn AnyTypedEnvelope>>,
)
where
- H: Fn(&mut Option<proto::Envelope>, ConnectionId) -> Option<Fut>,
- Fut: Future<Output = ()>,
Conn: futures::Sink<WebSocketMessage, Error = WebSocketError>
+ futures::Stream<Item = Result<WebSocketMessage, WebSocketError>>
+ Send
@@ -116,7 +117,7 @@ impl Peer {
self.next_connection_id
.fetch_add(1, atomic::Ordering::SeqCst),
);
- let (mut incoming_tx, mut incoming_rx) = mpsc::channel(64);
+ let (mut incoming_tx, incoming_rx) = mpsc::channel(64);
let (outgoing_tx, mut outgoing_rx) = mpsc::channel(64);
let connection = Connection {
outgoing_tx,
@@ -126,6 +127,7 @@ impl Peer {
let mut writer = MessageStream::new(tx);
let mut reader = MessageStream::new(rx);
+ let response_channels = connection.response_channels.clone();
let handle_io = async move {
loop {
let read_message = reader.read_message().fuse();
@@ -134,51 +136,54 @@ impl Peer {
futures::select_biased! {
incoming = read_message => match incoming {
Ok(incoming) => {
- if incoming_tx.send(incoming).await.is_err() {
- return Ok(());
+ if let Some(responding_to) = incoming.responding_to {
+ let channel = response_channels.lock().await.remove(&responding_to);
+ if let Some(mut tx) = channel {
+ tx.send(incoming).await.ok();
+ } else {
+ log::warn!("received RPC response to unknown request {}", responding_to);
+ }
+ } else {
+ if let Some(envelope) = proto::build_typed_envelope(connection_id, incoming) {
+ if incoming_tx.send(envelope).await.is_err() {
+ response_channels.lock().await.clear();
+ return Ok(())
+ }
+ } else {
+ log::error!("unable to construct a typed envelope");
+ }
}
+
break;
}
Err(error) => {
+ response_channels.lock().await.clear();
Err(error).context("received invalid RPC message")?;
}
},
outgoing = outgoing_rx.recv().fuse() => match outgoing {
Some(outgoing) => {
if let Err(result) = writer.write_message(&outgoing).await {
+ response_channels.lock().await.clear();
Err(result).context("failed to write RPC message")?;
}
}
- None => return Ok(()),
+ None => {
+ response_channels.lock().await.clear();
+ return Ok(())
+ }
}
}
}
}
};
- let response_channels = connection.response_channels.clone();
- let handle_messages = async move {
- while let Some(message) = incoming_rx.recv().await {
- if let Some(responding_to) = message.responding_to {
- let channel = response_channels.lock().await.remove(&responding_to);
- if let Some(mut tx) = channel {
- tx.send(message).await.ok();
- } else {
- log::warn!("received RPC response to unknown request {}", responding_to);
- }
- } else {
- router.handle(connection_id, message).await;
- }
- }
- response_channels.lock().await.clear();
- };
-
self.connections
.write()
.await
.insert(connection_id, connection);
- (connection_id, handle_io, handle_messages)
+ (connection_id, handle_io, incoming_rx)
}
pub async fn disconnect(&self, connection_id: ConnectionId) {
@@ -233,8 +238,12 @@ impl Peer {
.recv()
.await
.ok_or_else(|| anyhow!("connection was closed"))?;
- T::Response::from_envelope(response)
- .ok_or_else(|| anyhow!("received response of the wrong type"))
+ if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
+ Err(anyhow!("request failed").context(error.message.clone()))
+ } else {
+ T::Response::from_envelope(response)
+ .ok_or_else(|| anyhow!("received response of the wrong type"))
+ }
}
}
@@ -296,6 +305,25 @@ impl Peer {
}
}
+ pub fn respond_with_error<T: RequestMessage>(
+ self: &Arc<Self>,
+ receipt: Receipt<T>,
+ response: proto::Error,
+ ) -> impl Future<Output = Result<()>> {
+ let this = self.clone();
+ async move {
+ let mut connection = this.connection(receipt.sender_id).await?;
+ let message_id = connection
+ .next_message_id
+ .fetch_add(1, atomic::Ordering::SeqCst);
+ connection
+ .outgoing_tx
+ .send(response.into_envelope(message_id, Some(receipt.message_id), None))
+ .await?;
+ Ok(())
+ }
+ }
+
fn connection(
self: &Arc<Self>,
connection_id: ConnectionId,
@@ -311,142 +339,10 @@ impl Peer {
}
}
-impl<H, Fut> RouterInternal<H>
-where
- H: Fn(&mut Option<proto::Envelope>, ConnectionId) -> Option<Fut>,
- Fut: Future<Output = ()>,
-{
- pub fn new() -> Self {
- Self {
- message_handlers: Default::default(),
- handler_types: Default::default(),
- }
- }
-
- async fn handle(&self, connection_id: ConnectionId, message: proto::Envelope) {
- let mut envelope = Some(message);
- for handler in self.message_handlers.iter() {
- if let Some(future) = handler(&mut envelope, connection_id) {
- future.await;
- return;
- }
- }
- log::warn!("unhandled message: {:?}", envelope.unwrap().payload);
- }
-}
-
-impl Router {
- pub fn add_message_handler<T, Fut, F>(&mut self, handler: F)
- where
- T: EnvelopedMessage,
- Fut: 'static + Send + Future<Output = Result<()>>,
- F: 'static + Send + Sync + Fn(TypedEnvelope<T>) -> Fut,
- {
- if !self.handler_types.insert(TypeId::of::<T>()) {
- panic!("duplicate handler type");
- }
-
- self.message_handlers
- .push(Box::new(move |envelope, connection_id| {
- if envelope.as_ref().map_or(false, T::matches_envelope) {
- let envelope = Option::take(envelope).unwrap();
- let message_id = envelope.id;
- let future = handler(TypedEnvelope {
- sender_id: connection_id,
- original_sender_id: envelope.original_sender_id.map(PeerId),
- message_id,
- payload: T::from_envelope(envelope).unwrap(),
- });
- Some(
- async move {
- if let Err(error) = future.await {
- log::error!(
- "error handling message {} {}: {:?}",
- T::NAME,
- message_id,
- error
- );
- }
- }
- .boxed(),
- )
- } else {
- None
- }
- }));
- }
-}
-
-impl ForegroundRouter {
- pub fn add_message_handler<T, Fut, F>(&mut self, handler: F)
- where
- T: EnvelopedMessage,
- Fut: 'static + Future<Output = Result<()>>,
- F: 'static + Fn(TypedEnvelope<T>) -> Fut,
- {
- if !self.handler_types.insert(TypeId::of::<T>()) {
- panic!("duplicate handler type");
- }
-
- self.message_handlers
- .push(Box::new(move |envelope, connection_id| {
- if envelope.as_ref().map_or(false, T::matches_envelope) {
- let envelope = Option::take(envelope).unwrap();
- let message_id = envelope.id;
- let future = handler(TypedEnvelope {
- sender_id: connection_id,
- original_sender_id: envelope.original_sender_id.map(PeerId),
- message_id,
- payload: T::from_envelope(envelope).unwrap(),
- });
- Some(
- async move {
- if let Err(error) = future.await {
- log::error!(
- "error handling message {} {}: {:?}",
- T::NAME,
- message_id,
- error
- );
- }
- }
- .boxed_local(),
- )
- } else {
- None
- }
- }));
- }
-}
-
-impl<T> Clone for Receipt<T> {
- fn clone(&self) -> Self {
- Self {
- sender_id: self.sender_id,
- message_id: self.message_id,
- payload_type: PhantomData,
- }
- }
-}
-
-impl<T> Copy for Receipt<T> {}
-
-impl fmt::Display for ConnectionId {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.0.fmt(f)
- }
-}
-
-impl fmt::Display for PeerId {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.0.fmt(f)
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
- use crate::test;
+ use crate::{test, TypedEnvelope};
#[test]
fn test_request_response() {
@@ -456,139 +352,37 @@ mod tests {
let client1 = Peer::new();
let client2 = Peer::new();
- let mut router = Router::new();
- router.add_message_handler({
- let server = server.clone();
- move |envelope: TypedEnvelope<proto::Auth>| {
- let server = server.clone();
- async move {
- let receipt = envelope.receipt();
- let message = envelope.payload;
- server
- .respond(
- receipt,
- match message.user_id {
- 1 => {
- assert_eq!(message.access_token, "access-token-1");
- proto::AuthResponse {
- credentials_valid: true,
- }
- }
- 2 => {
- assert_eq!(message.access_token, "access-token-2");
- proto::AuthResponse {
- credentials_valid: false,
- }
- }
- _ => {
- panic!("unexpected user id {}", message.user_id);
- }
- },
- )
- .await
- }
- }
- });
-
- router.add_message_handler({
- let server = server.clone();
- move |envelope: TypedEnvelope<proto::OpenBuffer>| {
- let server = server.clone();
- async move {
- let receipt = envelope.receipt();
- let message = envelope.payload;
- server
- .respond(
- receipt,
- match message.path.as_str() {
- "path/one" => {
- assert_eq!(message.worktree_id, 1);
- proto::OpenBufferResponse {
- buffer: Some(proto::Buffer {
- id: 101,
- content: "path/one content".to_string(),
- history: vec![],
- selections: vec![],
- }),
- }
- }
- "path/two" => {
- assert_eq!(message.worktree_id, 2);
- proto::OpenBufferResponse {
- buffer: Some(proto::Buffer {
- id: 102,
- content: "path/two content".to_string(),
- history: vec![],
- selections: vec![],
- }),
- }
- }
- _ => {
- panic!("unexpected path {}", message.path);
- }
- },
- )
- .await
- }
- }
- });
- let router = Arc::new(router);
-
let (client1_to_server_conn, server_to_client_1_conn) = test::Channel::bidirectional();
- let (client1_conn_id, io_task1, msg_task1) = client1
- .add_connection(client1_to_server_conn, router.clone())
- .await;
- let (_, io_task2, msg_task2) = server
- .add_connection(server_to_client_1_conn, router.clone())
- .await;
+ 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) = test::Channel::bidirectional();
- let (client2_conn_id, io_task3, msg_task3) = client2
- .add_connection(client2_to_server_conn, router.clone())
- .await;
- let (_, io_task4, msg_task4) = server
- .add_connection(server_to_client_2_conn, router.clone())
- .await;
+ 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;
smol::spawn(io_task1).detach();
smol::spawn(io_task2).detach();
smol::spawn(io_task3).detach();
smol::spawn(io_task4).detach();
- smol::spawn(msg_task1).detach();
- smol::spawn(msg_task2).detach();
- smol::spawn(msg_task3).detach();
- smol::spawn(msg_task4).detach();
+ smol::spawn(handle_messages(incoming1, server.clone())).detach();
+ smol::spawn(handle_messages(incoming2, server.clone())).detach();
assert_eq!(
client1
- .request(
- client1_conn_id,
- proto::Auth {
- user_id: 1,
- access_token: "access-token-1".to_string(),
- },
- )
+ .request(client1_conn_id, proto::Ping { id: 1 },)
.await
.unwrap(),
- proto::AuthResponse {
- credentials_valid: true,
- }
+ proto::Pong { id: 1 }
);
assert_eq!(
client2
- .request(
- client2_conn_id,
- proto::Auth {
- user_id: 2,
- access_token: "access-token-2".to_string(),
- },
- )
+ .request(client2_conn_id, proto::Ping { id: 2 },)
.await
.unwrap(),
- proto::AuthResponse {
- credentials_valid: false,
- }
+ proto::Pong { id: 2 }
);
assert_eq!(
@@ -635,6 +429,63 @@ mod tests {
client1.disconnect(client1_conn_id).await;
client2.disconnect(client1_conn_id).await;
+
+ async fn handle_messages(
+ mut messages: mpsc::Receiver<Box<dyn AnyTypedEnvelope>>,
+ peer: Arc<Peer>,
+ ) -> Result<()> {
+ while let Some(envelope) = messages.next().await {
+ let envelope = envelope.into_any();
+ if let Some(envelope) = envelope.downcast_ref::<TypedEnvelope<proto::Ping>>() {
+ let receipt = envelope.receipt();
+ peer.respond(
+ receipt,
+ proto::Pong {
+ id: envelope.payload.id,
+ },
+ )
+ .await?
+ } else if let Some(envelope) =
+ envelope.downcast_ref::<TypedEnvelope<proto::OpenBuffer>>()
+ {
+ let message = &envelope.payload;
+ let receipt = envelope.receipt();
+ let response = match message.path.as_str() {
+ "path/one" => {
+ assert_eq!(message.worktree_id, 1);
+ proto::OpenBufferResponse {
+ buffer: Some(proto::Buffer {
+ id: 101,
+ content: "path/one content".to_string(),
+ history: vec![],
+ selections: vec![],
+ }),
+ }
+ }
+ "path/two" => {
+ assert_eq!(message.worktree_id, 2);
+ proto::OpenBufferResponse {
+ buffer: Some(proto::Buffer {
+ id: 102,
+ content: "path/two content".to_string(),
+ history: vec![],
+ selections: vec![],
+ }),
+ }
+ }
+ _ => {
+ panic!("unexpected path {}", message.path);
+ }
+ };
+
+ peer.respond(receipt, response).await?
+ } else {
+ panic!("unknown message type");
+ }
+ }
+
+ Ok(())
+ }
});
}
@@ -644,9 +495,8 @@ mod tests {
let (client_conn, mut server_conn) = test::Channel::bidirectional();
let client = Peer::new();
- let router = Arc::new(Router::new());
- let (connection_id, io_handler, message_handler) =
- client.add_connection(client_conn, router).await;
+ let (connection_id, io_handler, mut incoming) =
+ client.add_connection(client_conn).await;
let (mut io_ended_tx, mut io_ended_rx) = postage::barrier::channel();
smol::spawn(async move {
@@ -657,7 +507,7 @@ mod tests {
let (mut messages_ended_tx, mut messages_ended_rx) = postage::barrier::channel();
smol::spawn(async move {
- message_handler.await;
+ incoming.next().await;
messages_ended_tx.send(()).await.unwrap();
})
.detach();
@@ -681,20 +531,13 @@ mod tests {
drop(server_conn);
let client = Peer::new();
- let router = Arc::new(Router::new());
- let (connection_id, io_handler, message_handler) =
- client.add_connection(client_conn, router).await;
+ let (connection_id, io_handler, mut incoming) =
+ client.add_connection(client_conn).await;
smol::spawn(io_handler).detach();
- smol::spawn(message_handler).detach();
+ smol::spawn(async move { incoming.next().await }).detach();
let err = client
- .request(
- connection_id,
- proto::Auth {
- user_id: 42,
- access_token: "token".to_string(),
- },
- )
+ .request(connection_id, proto::Ping { id: 42 })
.await
.unwrap_err();
assert_eq!(err.to_string(), "connection was closed");
@@ -1,6 +1,9 @@
+use super::{ConnectionId, PeerId, TypedEnvelope};
+use anyhow::Result;
use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage};
use futures::{SinkExt as _, StreamExt as _};
use prost::Message;
+use std::any::{Any, TypeId};
use std::{
io,
time::{Duration, SystemTime, UNIX_EPOCH},
@@ -8,7 +11,7 @@ use std::{
include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
-pub trait EnvelopedMessage: Clone + Sized + Send + 'static {
+pub trait EnvelopedMessage: Clone + Sized + Send + Sync + 'static {
const NAME: &'static str;
fn into_envelope(
self,
@@ -16,69 +19,166 @@ pub trait EnvelopedMessage: Clone + Sized + Send + 'static {
responding_to: Option<u32>,
original_sender_id: Option<u32>,
) -> Envelope;
- fn matches_envelope(envelope: &Envelope) -> bool;
fn from_envelope(envelope: Envelope) -> Option<Self>;
}
+pub trait EntityMessage: EnvelopedMessage {
+ fn remote_entity_id(&self) -> u64;
+}
+
pub trait RequestMessage: EnvelopedMessage {
type Response: EnvelopedMessage;
}
-macro_rules! message {
- ($name:ident) => {
- impl EnvelopedMessage for $name {
- const NAME: &'static str = std::stringify!($name);
-
- fn into_envelope(
- self,
- id: u32,
- responding_to: Option<u32>,
- original_sender_id: Option<u32>,
- ) -> Envelope {
- Envelope {
- id,
- responding_to,
- original_sender_id,
- payload: Some(envelope::Payload::$name(self)),
- }
- }
+pub trait AnyTypedEnvelope: 'static + Send + Sync {
+ fn payload_type_id(&self) -> TypeId;
+ fn payload_type_name(&self) -> &'static str;
+ fn as_any(&self) -> &dyn Any;
+ fn into_any(self: Box<Self>) -> Box<dyn Any + Send + Sync>;
+}
+
+impl<T: EnvelopedMessage> AnyTypedEnvelope for TypedEnvelope<T> {
+ fn payload_type_id(&self) -> TypeId {
+ TypeId::of::<T>()
+ }
- fn matches_envelope(envelope: &Envelope) -> bool {
- matches!(&envelope.payload, Some(envelope::Payload::$name(_)))
+ fn payload_type_name(&self) -> &'static str {
+ T::NAME
+ }
+
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn into_any(self: Box<Self>) -> Box<dyn Any + Send + Sync> {
+ self
+ }
+}
+
+macro_rules! messages {
+ ($($name:ident),* $(,)?) => {
+ pub fn build_typed_envelope(sender_id: ConnectionId, envelope: Envelope) -> Option<Box<dyn AnyTypedEnvelope>> {
+ match envelope.payload {
+ $(Some(envelope::Payload::$name(payload)) => {
+ Some(Box::new(TypedEnvelope {
+ sender_id,
+ original_sender_id: envelope.original_sender_id.map(PeerId),
+ message_id: envelope.id,
+ payload,
+ }))
+ }, )*
+ _ => None
}
+ }
- fn from_envelope(envelope: Envelope) -> Option<Self> {
- if let Some(envelope::Payload::$name(msg)) = envelope.payload {
- Some(msg)
- } else {
- None
+ $(
+ impl EnvelopedMessage for $name {
+ const NAME: &'static str = std::stringify!($name);
+
+ fn into_envelope(
+ self,
+ id: u32,
+ responding_to: Option<u32>,
+ original_sender_id: Option<u32>,
+ ) -> Envelope {
+ Envelope {
+ id,
+ responding_to,
+ original_sender_id,
+ payload: Some(envelope::Payload::$name(self)),
+ }
+ }
+
+ fn from_envelope(envelope: Envelope) -> Option<Self> {
+ if let Some(envelope::Payload::$name(msg)) = envelope.payload {
+ Some(msg)
+ } else {
+ None
+ }
}
}
- }
+ )*
};
}
-macro_rules! request_message {
- ($req:ident, $resp:ident) => {
- message!($req);
- message!($resp);
- impl RequestMessage for $req {
- type Response = $resp;
- }
+macro_rules! request_messages {
+ ($(($request_name:ident, $response_name:ident)),* $(,)?) => {
+ $(impl RequestMessage for $request_name {
+ type Response = $response_name;
+ })*
+ };
+}
+
+macro_rules! entity_messages {
+ ($id_field:ident, $($name:ident),* $(,)?) => {
+ $(impl EntityMessage for $name {
+ fn remote_entity_id(&self) -> u64 {
+ self.$id_field
+ }
+ })*
};
}
-request_message!(Auth, AuthResponse);
-request_message!(ShareWorktree, ShareWorktreeResponse);
-request_message!(OpenWorktree, OpenWorktreeResponse);
-message!(UpdateWorktree);
-message!(CloseWorktree);
-request_message!(OpenBuffer, OpenBufferResponse);
-message!(CloseBuffer);
-message!(UpdateBuffer);
-request_message!(SaveBuffer, BufferSaved);
-message!(AddPeer);
-message!(RemovePeer);
+messages!(
+ AddPeer,
+ BufferSaved,
+ ChannelMessageSent,
+ CloseBuffer,
+ CloseWorktree,
+ Error,
+ GetChannelMessages,
+ GetChannelMessagesResponse,
+ GetChannels,
+ GetChannelsResponse,
+ GetUsers,
+ GetUsersResponse,
+ JoinChannel,
+ JoinChannelResponse,
+ LeaveChannel,
+ OpenBuffer,
+ OpenBufferResponse,
+ OpenWorktree,
+ OpenWorktreeResponse,
+ Ping,
+ Pong,
+ RemovePeer,
+ SaveBuffer,
+ SendChannelMessage,
+ SendChannelMessageResponse,
+ ShareWorktree,
+ ShareWorktreeResponse,
+ UpdateBuffer,
+ UpdateWorktree,
+);
+
+request_messages!(
+ (GetChannels, GetChannelsResponse),
+ (GetUsers, GetUsersResponse),
+ (JoinChannel, JoinChannelResponse),
+ (OpenBuffer, OpenBufferResponse),
+ (OpenWorktree, OpenWorktreeResponse),
+ (Ping, Pong),
+ (SaveBuffer, BufferSaved),
+ (ShareWorktree, ShareWorktreeResponse),
+ (SendChannelMessage, SendChannelMessageResponse),
+ (GetChannelMessages, GetChannelMessagesResponse),
+);
+
+entity_messages!(
+ worktree_id,
+ AddPeer,
+ BufferSaved,
+ CloseBuffer,
+ CloseWorktree,
+ OpenBuffer,
+ OpenWorktree,
+ RemovePeer,
+ SaveBuffer,
+ UpdateBuffer,
+ UpdateWorktree,
+);
+
+entity_messages!(channel_id, ChannelMessageSent);
/// A stream of protobuf messages.
pub struct MessageStream<S> {
@@ -157,12 +257,7 @@ mod tests {
fn test_round_trip_message() {
smol::block_on(async {
let stream = test::Channel::new();
- let message1 = Auth {
- user_id: 5,
- access_token: "the-access-token".into(),
- }
- .into_envelope(3, None, None);
-
+ let message1 = Ping { id: 5 }.into_envelope(3, None, None);
let message2 = OpenBuffer {
worktree_id: 0,
path: "some/path".to_string(),