diff --git a/Cargo.lock b/Cargo.lock index f4294b6165..69b17a9aee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7212,6 +7212,35 @@ dependencies = [ "util", ] +[[package]] +name = "settings2" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "feature_flags", + "fs", + "futures 0.3.28", + "gpui2", + "indoc", + "lazy_static", + "postage", + "pretty_assertions", + "rust-embed", + "schemars", + "serde", + "serde_derive", + "serde_json", + "serde_json_lenient", + "smallvec", + "sqlez", + "toml 0.5.11", + "tree-sitter", + "tree-sitter-json 0.19.0", + "unindent", + "util", +] + [[package]] name = "sha-1" version = "0.9.8" @@ -10338,6 +10367,95 @@ dependencies = [ "gpui", ] +[[package]] +name = "zed2" +version = "0.109.0" +dependencies = [ + "anyhow", + "async-compression", + "async-recursion 0.3.2", + "async-tar", + "async-trait", + "backtrace", + "chrono", + "cli", + "collections", + "ctor", + "env_logger 0.9.3", + "feature_flags", + "fs", + "fsevent", + "futures 0.3.28", + "fuzzy", + "gpui2", + "ignore", + "image", + "indexmap 1.9.3", + "install_cli", + "isahc", + "language_tools", + "lazy_static", + "libc", + "log", + "lsp", + "node_runtime", + "num_cpus", + "parking_lot 0.11.2", + "postage", + "rand 0.8.5", + "regex", + "rpc", + "rsa 0.4.0", + "rust-embed", + "schemars", + "serde", + "serde_derive", + "serde_json", + "settings2", + "shellexpand", + "simplelog", + "smallvec", + "smol", + "sum_tree", + "tempdir", + "text", + "thiserror", + "tiny_http", + "toml 0.5.11", + "tree-sitter", + "tree-sitter-bash", + "tree-sitter-c", + "tree-sitter-cpp", + "tree-sitter-css", + "tree-sitter-elixir", + "tree-sitter-elm", + "tree-sitter-embedded-template", + "tree-sitter-glsl", + "tree-sitter-go", + "tree-sitter-heex", + "tree-sitter-html", + "tree-sitter-json 0.20.0", + "tree-sitter-lua", + "tree-sitter-markdown", + "tree-sitter-nix", + "tree-sitter-nu", + "tree-sitter-php", + "tree-sitter-python", + "tree-sitter-racket", + "tree-sitter-ruby", + "tree-sitter-rust", + "tree-sitter-scheme", + "tree-sitter-svelte", + "tree-sitter-toml", + "tree-sitter-typescript", + "tree-sitter-yaml", + "unindent", + "url", + "urlencoding", + "util", + "uuid 1.4.1", +] + [[package]] name = "zeroize" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index cdc767dce7..99e1b4da54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,7 @@ members = [ "crates/welcome", "crates/xtask", "crates/zed", + "crates/zed2", "crates/zed-actions" ] default-members = ["crates/zed"] diff --git a/crates/gpui2/Cargo.toml b/crates/gpui2/Cargo.toml index 7743ebad51..fa072dadc3 100644 --- a/crates/gpui2/Cargo.toml +++ b/crates/gpui2/Cargo.toml @@ -7,7 +7,7 @@ description = "The next version of Zed's GPU-accelerated UI framework" publish = false [features] -test = ["backtrace", "dhat", "env_logger", "collections/test-support", "util/test-support"] +test-support = ["backtrace", "dhat", "env_logger", "collections/test-support", "util/test-support"] [lib] path = "src/gpui2.rs" diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 0532790e60..9e3db6f83e 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -1,18 +1,42 @@ use crate::SharedString; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use collections::{HashMap, HashSet}; -use std::any::Any; +use serde::Deserialize; +use std::any::{type_name, Any}; pub trait Action: Any + Send + Sync { + fn qualified_name() -> SharedString + where + Self: Sized; + fn build(value: Option) -> Result> + where + Self: Sized; + fn partial_eq(&self, action: &dyn Action) -> bool; fn boxed_clone(&self) -> Box; fn as_any(&self) -> &dyn Any; } -impl Action for T +impl Action for A where - T: Any + PartialEq + Clone + Send + Sync, + A: for<'a> Deserialize<'a> + Any + PartialEq + Clone + Default + Send + Sync, { + fn qualified_name() -> SharedString { + type_name::().into() + } + + fn build(params: Option) -> Result> + where + Self: Sized, + { + let action = if let Some(params) = params { + serde_json::from_value(params).context("failed to deserialize action")? + } else { + Self::default() + }; + Ok(Box::new(action)) + } + fn partial_eq(&self, action: &dyn Action) -> bool { action .as_any() @@ -130,15 +154,15 @@ impl DispatchContextPredicate { return false; }; match self { - Self::Identifier(name) => context.set.contains(&name), + Self::Identifier(name) => context.set.contains(name), Self::Equal(left, right) => context .map - .get(&left) + .get(left) .map(|value| value == right) .unwrap_or(false), Self::NotEqual(left, right) => context .map - .get(&left) + .get(left) .map(|value| value != right) .unwrap_or(true), Self::Not(pred) => !pred.eval(contexts), diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index da69599e37..19d95d2d14 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -9,10 +9,10 @@ use refineable::Refineable; use smallvec::SmallVec; use crate::{ - current_platform, image_cache::ImageCache, AssetSource, Context, DisplayId, Executor, + current_platform, image_cache::ImageCache, Action, AssetSource, Context, DisplayId, Executor, FocusEvent, FocusHandle, FocusId, KeyBinding, Keymap, LayoutId, MainThread, MainThreadOnly, - Platform, SubscriberSet, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, - Window, WindowContext, WindowHandle, WindowId, + Platform, SharedString, SubscriberSet, SvgRenderer, Task, TextStyle, TextStyleRefinement, + TextSystem, View, Window, WindowContext, WindowHandle, WindowId, }; use anyhow::{anyhow, Result}; use collections::{HashMap, HashSet, VecDeque}; @@ -55,10 +55,10 @@ impl App { Mutex::new(AppContext { this: this.clone(), text_system: Arc::new(TextSystem::new(platform.text_system())), - pending_updates: 0, - flushing_effects: false, - next_frame_callbacks: Default::default(), platform: MainThreadOnly::new(platform, executor.clone()), + flushing_effects: false, + pending_updates: 0, + next_frame_callbacks: Default::default(), executor, svg_renderer: SvgRenderer::new(asset_source), image_cache: ImageCache::new(http_client), @@ -68,6 +68,7 @@ impl App { entities, windows: SlotMap::with_key(), keymap: Arc::new(RwLock::new(Keymap::default())), + action_builders: HashMap::default(), pending_notifications: Default::default(), pending_effects: Default::default(), observers: SubscriberSet::new(), @@ -90,12 +91,17 @@ impl App { on_finish_launching(cx); })); } + + pub fn executor(&self) -> Executor { + self.0.lock().executor.clone() + } } type Handler = Box bool + Send + Sync + 'static>; type EventHandler = Box bool + Send + Sync + 'static>; type ReleaseHandler = Box; type FrameCallback = Box; +type ActionBuilder = fn(json: Option) -> anyhow::Result>; pub struct AppContext { this: Weak>, @@ -113,6 +119,7 @@ pub struct AppContext { pub(crate) entities: EntityMap, pub(crate) windows: SlotMap>, pub(crate) keymap: Arc>, + action_builders: HashMap, pub(crate) pending_notifications: HashSet, pending_effects: VecDeque, pub(crate) observers: SubscriberSet, @@ -134,6 +141,20 @@ impl AppContext { result } + pub(crate) fn read_window( + &mut self, + id: WindowId, + read: impl FnOnce(&WindowContext) -> R, + ) -> Result { + let window = self + .windows + .get(id) + .ok_or_else(|| anyhow!("window not found"))? + .as_ref() + .unwrap(); + Ok(read(&WindowContext::immutable(self, &window))) + } + pub(crate) fn update_window( &mut self, id: WindowId, @@ -385,6 +406,24 @@ impl AppContext { .unwrap() } + pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R + where + G: 'static + Send + Sync, + { + let mut global = self + .global_stacks_by_type + .get_mut(&TypeId::of::()) + .and_then(|stack| stack.pop()) + .ok_or_else(|| anyhow!("no state of type {} exists", type_name::())) + .unwrap(); + let result = f(global.downcast_mut().unwrap(), self); + self.global_stacks_by_type + .get_mut(&TypeId::of::()) + .unwrap() + .push(global); + result + } + pub fn default_global(&mut self) -> &mut G { let stack = self .global_stacks_by_type @@ -396,6 +435,19 @@ impl AppContext { stack.last_mut().unwrap().downcast_mut::().unwrap() } + pub fn set_global(&mut self, global: T) { + let global = Box::new(global); + let stack = self + .global_stacks_by_type + .entry(TypeId::of::()) + .or_default(); + if let Some(last) = stack.last_mut() { + *last = global; + } else { + stack.push(global) + } + } + pub(crate) fn push_global(&mut self, state: T) { self.global_stacks_by_type .entry(TypeId::of::()) @@ -422,9 +474,26 @@ impl AppContext { self.keymap.write().add_bindings(bindings); self.push_effect(Effect::Refresh); } + + pub fn register_action_type(&mut self) { + self.action_builders.insert(A::qualified_name(), A::build); + } + + pub fn build_action( + &mut self, + name: &str, + params: Option, + ) -> Result> { + let build = self + .action_builders + .get(name) + .ok_or_else(|| anyhow!("no action type registered for {}", name))?; + (build)(params) + } } impl Context for AppContext { + type BorrowedContext<'a, 'w> = Self; type EntityContext<'a, 'w, T: Send + Sync + 'static> = ModelContext<'a, T>; type Result = T; @@ -451,6 +520,10 @@ impl Context for AppContext { result }) } + + fn read_global(&self, read: impl FnOnce(&G, &Self) -> R) -> R { + read(self.global(), self) + } } impl MainThread { diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index 026a1b0a07..f4d1166b04 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -9,18 +9,19 @@ use std::sync::Weak; pub struct AsyncAppContext(pub(crate) Weak>); impl Context for AsyncAppContext { + type BorrowedContext<'a, 'w> = AppContext; type EntityContext<'a, 'w, T: 'static + Send + Sync> = ModelContext<'a, T>; type Result = Result; fn entity( &mut self, build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T, - ) -> Result> { + ) -> Self::Result> { let app = self .0 .upgrade() .ok_or_else(|| anyhow!("app was released"))?; - let mut lock = app.lock(); // Does not compile without this variable. + let mut lock = app.lock(); Ok(lock.entity(build_entity)) } @@ -28,17 +29,42 @@ impl Context for AsyncAppContext { &mut self, handle: &Handle, update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R, + ) -> Self::Result { + let app = self + .0 + .upgrade() + .ok_or_else(|| anyhow!("app was released"))?; + let mut lock = app.lock(); + Ok(lock.update_entity(handle, update)) + } + + fn read_global( + &self, + read: impl FnOnce(&G, &Self::BorrowedContext<'_, '_>) -> R, + ) -> Self::Result { + let app = self + .0 + .upgrade() + .ok_or_else(|| anyhow!("app was released"))?; + let mut lock = app.lock(); + Ok(lock.read_global(read)) + } +} + +impl AsyncAppContext { + pub fn read_window( + &self, + handle: AnyWindowHandle, + update: impl FnOnce(&WindowContext) -> R, ) -> Result { let app = self .0 .upgrade() .ok_or_else(|| anyhow!("app was released"))?; - let mut lock = app.lock(); // Does not compile without this variable. - Ok(lock.update_entity(handle, update)) + let mut app_context = app.lock(); + app_context.read_window(handle.id, update) } -} -impl AsyncAppContext { pub fn update_window( &self, handle: AnyWindowHandle, @@ -76,6 +102,7 @@ impl AsyncWindowContext { } impl Context for AsyncWindowContext { + type BorrowedContext<'a, 'w> = WindowContext<'a, 'w>; type EntityContext<'a, 'w, T: 'static + Send + Sync> = ViewContext<'a, 'w, T>; type Result = Result; @@ -95,4 +122,11 @@ impl Context for AsyncWindowContext { self.app .update_window(self.window, |cx| cx.update_entity(handle, update)) } + + fn read_global( + &self, + read: impl FnOnce(&G, &Self::BorrowedContext<'_, '_>) -> R, + ) -> Result { + self.app.read_window(self.window, |cx| cx.read_global(read)) + } } diff --git a/crates/gpui2/src/app/model_context.rs b/crates/gpui2/src/app/model_context.rs index 4b77099cb1..b4996880a6 100644 --- a/crates/gpui2/src/app/model_context.rs +++ b/crates/gpui2/src/app/model_context.rs @@ -132,6 +132,7 @@ impl<'a, T: EventEmitter + Send + Sync + 'static> ModelContext<'a, T> { } impl<'a, T: 'static> Context for ModelContext<'a, T> { + type BorrowedContext<'b, 'c> = ModelContext<'b, T>; type EntityContext<'b, 'c, U: Send + Sync + 'static> = ModelContext<'b, U>; type Result = U; @@ -149,4 +150,11 @@ impl<'a, T: 'static> Context for ModelContext<'a, T> { ) -> R { self.app.update_entity(handle, update) } + + fn read_global( + &self, + read: impl FnOnce(&G, &Self::BorrowedContext<'_, '_>) -> R, + ) -> R { + read(self.app.global(), self) + } } diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index 9b2e3ad4c3..6db06f03a5 100644 --- a/crates/gpui2/src/executor.rs +++ b/crates/gpui2/src/executor.rs @@ -1,10 +1,12 @@ -use crate::PlatformDispatcher; +use crate::{AppContext, PlatformDispatcher}; use smol::prelude::*; use std::{ + fmt::Debug, pin::Pin, sync::Arc, task::{Context, Poll}, }; +use util::TryFutureExt; #[derive(Clone)] pub struct Executor { @@ -30,6 +32,16 @@ impl Task { } } +impl Task> +where + T: 'static + Send, + E: 'static + Send + Debug, +{ + pub fn detach_and_log_err(self, cx: &mut AppContext) { + cx.executor().spawn(self.log_err()).detach(); + } +} + impl Future for Task { type Output = T; diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index da1790199e..0fb83007bc 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -56,6 +56,7 @@ pub use window::*; use derive_more::{Deref, DerefMut}; use std::{ any::{Any, TypeId}, + borrow::Borrow, mem, ops::{Deref, DerefMut}, sync::Arc, @@ -65,6 +66,7 @@ use taffy::TaffyLayoutEngine; type AnyBox = Box; pub trait Context { + type BorrowedContext<'a, 'w>: Context; type EntityContext<'a, 'w, T: 'static + Send + Sync>; type Result; @@ -78,6 +80,11 @@ pub trait Context { handle: &Handle, update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R, ) -> Self::Result; + + fn read_global( + &self, + read: impl FnOnce(&G, &Self::BorrowedContext<'_, '_>) -> R, + ) -> Self::Result; } pub enum GlobalKey { @@ -104,6 +111,7 @@ impl DerefMut for MainThread { } impl Context for MainThread { + type BorrowedContext<'a, 'w> = MainThread>; type EntityContext<'a, 'w, T: 'static + Send + Sync> = MainThread>; type Result = C::Result; @@ -137,6 +145,21 @@ impl Context for MainThread { update(entity, cx) }) } + + fn read_global( + &self, + read: impl FnOnce(&G, &Self::BorrowedContext<'_, '_>) -> R, + ) -> Self::Result { + self.0.read_global(|global, cx| { + let cx = unsafe { + mem::transmute::< + &C::BorrowedContext<'_, '_>, + &MainThread>, + >(cx) + }; + read(global, cx) + }) + } } pub trait BorrowAppContext { @@ -152,15 +175,19 @@ pub trait BorrowAppContext { result } - fn with_global(&mut self, state: T, f: F) -> R + fn with_global(&mut self, global: T, f: F) -> R where F: FnOnce(&mut Self) -> R, { - self.app_mut().push_global(state); + self.app_mut().push_global(global); let result = f(self); self.app_mut().pop_global::(); result } + + fn set_global(&mut self, global: T) { + self.app_mut().set_global(global) + } } pub trait EventEmitter { @@ -198,6 +225,12 @@ impl AsRef for SharedString { } } +impl Borrow for SharedString { + fn borrow(&self) -> &str { + self.as_ref() + } +} + impl std::fmt::Debug for SharedString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 16c22ec797..476898c60e 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -288,6 +288,13 @@ pub struct WindowContext<'a, 'w> { } impl<'a, 'w> WindowContext<'a, 'w> { + pub(crate) fn immutable(app: &'a AppContext, window: &'w Window) -> Self { + Self { + app: Reference::Immutable(app), + window: Reference::Immutable(window), + } + } + pub(crate) fn mutable(app: &'a mut AppContext, window: &'w mut Window) -> Self { Self { app: Reference::Mutable(app), @@ -1049,6 +1056,7 @@ impl<'a, 'w> MainThread> { } impl Context for WindowContext<'_, '_> { + type BorrowedContext<'a, 'w> = WindowContext<'a, 'w>; type EntityContext<'a, 'w, T: 'static + Send + Sync> = ViewContext<'a, 'w, T>; type Result = T; @@ -1078,6 +1086,10 @@ impl Context for WindowContext<'_, '_> { self.entities.end_lease(entity); result } + + fn read_global(&self, read: impl FnOnce(&G, &Self) -> R) -> R { + read(self.app.global(), self) + } } impl<'a, 'w> std::ops::Deref for WindowContext<'a, 'w> { @@ -1520,7 +1532,11 @@ impl<'a, 'w, S: EventEmitter + Send + Sync + 'static> ViewContext<'a, 'w, S> { } } -impl<'a, 'w, S> Context for ViewContext<'a, 'w, S> { +impl<'a, 'w, V> Context for ViewContext<'a, 'w, V> +where + V: 'static + Send + Sync, +{ + type BorrowedContext<'b, 'c> = ViewContext<'b, 'c, V>; type EntityContext<'b, 'c, U: 'static + Send + Sync> = ViewContext<'b, 'c, U>; type Result = U; @@ -1531,13 +1547,20 @@ impl<'a, 'w, S> Context for ViewContext<'a, 'w, S> { self.window_cx.entity(build_entity) } - fn update_entity( + fn update_entity( &mut self, handle: &Handle, update: impl FnOnce(&mut U, &mut Self::EntityContext<'_, '_, U>) -> R, ) -> R { self.window_cx.update_entity(handle, update) } + + fn read_global( + &self, + read: impl FnOnce(&G, &Self::BorrowedContext<'_, '_>) -> R, + ) -> R { + read(self.global(), self) + } } impl<'a, 'w, S: 'static> std::ops::Deref for ViewContext<'a, 'w, S> { diff --git a/crates/settings2/Cargo.toml b/crates/settings2/Cargo.toml new file mode 100644 index 0000000000..b3df9b9e93 --- /dev/null +++ b/crates/settings2/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "settings2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/settings2.rs" +doctest = false + +[features] +test-support = ["gpui2/test-support", "fs/test-support"] + +[dependencies] +collections = { path = "../collections" } +gpui2 = { path = "../gpui2" } +sqlez = { path = "../sqlez" } +fs = { path = "../fs" } +feature_flags = { path = "../feature_flags" } +util = { path = "../util" } + +anyhow.workspace = true +futures.workspace = true +serde_json_lenient = {version = "0.1", features = ["preserve_order", "raw_value"]} +lazy_static.workspace = true +postage.workspace = true +rust-embed.workspace = true +schemars.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +smallvec.workspace = true +toml.workspace = true +tree-sitter.workspace = true +tree-sitter-json = "*" + +[dev-dependencies] +gpui2 = { path = "../gpui2", features = ["test-support"] } +fs = { path = "../fs", features = ["test-support"] } +indoc.workspace = true +pretty_assertions.workspace = true +unindent.workspace = true diff --git a/crates/settings2/src/keymap_file.rs b/crates/settings2/src/keymap_file.rs new file mode 100644 index 0000000000..be2a11c401 --- /dev/null +++ b/crates/settings2/src/keymap_file.rs @@ -0,0 +1,163 @@ +use crate::{settings_store::parse_json_with_comments, SettingsAssets}; +use anyhow::{anyhow, Context, Result}; +use collections::BTreeMap; +use gpui2::{AppContext, KeyBinding}; +use schemars::{ + gen::{SchemaGenerator, SchemaSettings}, + schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation}, + JsonSchema, +}; +use serde::Deserialize; +use serde_json::Value; +use util::{asset_str, ResultExt}; + +#[derive(Debug, Deserialize, Default, Clone, JsonSchema)] +#[serde(transparent)] +pub struct KeymapFile(Vec); + +#[derive(Debug, Deserialize, Default, Clone, JsonSchema)] +pub struct KeymapBlock { + #[serde(default)] + context: Option, + bindings: BTreeMap, +} + +#[derive(Debug, Deserialize, Default, Clone)] +#[serde(transparent)] +pub struct KeymapAction(Value); + +impl JsonSchema for KeymapAction { + fn schema_name() -> String { + "KeymapAction".into() + } + + fn json_schema(_: &mut SchemaGenerator) -> Schema { + Schema::Bool(true) + } +} + +#[derive(Deserialize)] +struct ActionWithData(Box, Value); + +impl KeymapFile { + pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> { + let content = asset_str::(asset_path); + + Self::parse(content.as_ref())?.add_to_cx(cx) + } + + pub fn parse(content: &str) -> Result { + parse_json_with_comments::(content) + } + + pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> { + for KeymapBlock { context, bindings } in self.0 { + let bindings = bindings + .into_iter() + .filter_map(|(keystroke, action)| { + let action = action.0; + + // This is a workaround for a limitation in serde: serde-rs/json#497 + // We want to deserialize the action data as a `RawValue` so that we can + // deserialize the action itself dynamically directly from the JSON + // string. But `RawValue` currently does not work inside of an untagged enum. + match action { + Value::Array(items) => { + let Ok([name, data]): Result<[serde_json::Value; 2], _> = + items.try_into() + else { + return Some(Err(anyhow!("Expected array of length 2"))); + }; + let serde_json::Value::String(name) = name else { + return Some(Err(anyhow!( + "Expected first item in array to be a string." + ))); + }; + cx.build_action(&name, Some(data)) + } + Value::String(name) => cx.build_action(&name, None), + Value::Null => Ok(no_action()), + _ => { + return Some(Err(anyhow!("Expected two-element array, got {action:?}"))) + } + } + .with_context(|| { + format!( + "invalid binding value for keystroke {keystroke}, context {context:?}" + ) + }) + .log_err() + .map(|action| KeyBinding::load(&keystroke, action, context.as_deref())) + }) + .collect::>>()?; + + cx.bind_keys(bindings); + } + Ok(()) + } + + pub fn generate_json_schema(action_names: &[&'static str]) -> serde_json::Value { + let mut root_schema = SchemaSettings::draft07() + .with(|settings| settings.option_add_null_type = false) + .into_generator() + .into_root_schema_for::(); + + let action_schema = Schema::Object(SchemaObject { + subschemas: Some(Box::new(SubschemaValidation { + one_of: Some(vec![ + Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), + enum_values: Some( + action_names + .iter() + .map(|name| Value::String(name.to_string())) + .collect(), + ), + ..Default::default() + }), + Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))), + ..Default::default() + }), + Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))), + ..Default::default() + }), + ]), + ..Default::default() + })), + ..Default::default() + }); + + root_schema + .definitions + .insert("KeymapAction".to_owned(), action_schema); + + serde_json::to_value(root_schema).unwrap() + } +} + +fn no_action() -> Box { + todo!() +} + +#[cfg(test)] +mod tests { + use crate::KeymapFile; + + #[test] + fn can_deserialize_keymap_with_trailing_comma() { + let json = indoc::indoc! {"[ + // Standard macOS bindings + { + \"bindings\": { + \"up\": \"menu::SelectPrev\", + }, + }, + ] + " + + }; + KeymapFile::parse(json).unwrap(); + } +} diff --git a/crates/settings2/src/settings2.rs b/crates/settings2/src/settings2.rs new file mode 100644 index 0000000000..8c3587d942 --- /dev/null +++ b/crates/settings2/src/settings2.rs @@ -0,0 +1,38 @@ +mod keymap_file; +mod settings_file; +mod settings_store; + +use rust_embed::RustEmbed; +use std::{borrow::Cow, str}; +use util::asset_str; + +pub use keymap_file::KeymapFile; +pub use settings_file::*; +pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore}; + +#[derive(RustEmbed)] +#[folder = "../../assets"] +#[include = "settings/*"] +#[include = "keymaps/*"] +#[exclude = "*.DS_Store"] +pub struct SettingsAssets; + +pub fn default_settings() -> Cow<'static, str> { + asset_str::("settings/default.json") +} + +pub fn default_keymap() -> Cow<'static, str> { + asset_str::("keymaps/default.json") +} + +pub fn vim_keymap() -> Cow<'static, str> { + asset_str::("keymaps/vim.json") +} + +pub fn initial_user_settings_content() -> Cow<'static, str> { + asset_str::("settings/initial_user_settings.json") +} + +pub fn initial_local_settings_content() -> Cow<'static, str> { + asset_str::("settings/initial_local_settings.json") +} diff --git a/crates/settings2/src/settings_file.rs b/crates/settings2/src/settings_file.rs new file mode 100644 index 0000000000..c4b6b9bd1d --- /dev/null +++ b/crates/settings2/src/settings_file.rs @@ -0,0 +1,135 @@ +use crate::{settings_store::SettingsStore, Setting}; +use anyhow::Result; +use fs::Fs; +use futures::{channel::mpsc, StreamExt}; +use gpui2::{AppContext, Context}; +use std::{ + io::ErrorKind, + path::{Path, PathBuf}, + str, + sync::Arc, + time::Duration, +}; +use util::{paths, ResultExt}; + +pub fn register(cx: &mut AppContext) { + cx.update_global(|store: &mut SettingsStore, cx| { + store.register_setting::(cx); + }); +} + +pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T { + cx.global::().get(None) +} + +pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a T { + cx.global::().get(location) +} + +pub const EMPTY_THEME_NAME: &'static str = "empty-theme"; + +#[cfg(any(test, feature = "test-support"))] +pub fn test_settings() -> String { + let mut value = crate::settings_store::parse_json_with_comments::( + crate::default_settings().as_ref(), + ) + .unwrap(); + util::merge_non_null_json_value_into( + serde_json::json!({ + "buffer_font_family": "Courier", + "buffer_font_features": {}, + "buffer_font_size": 14, + "theme": EMPTY_THEME_NAME, + }), + &mut value, + ); + value.as_object_mut().unwrap().remove("languages"); + serde_json::to_string(&value).unwrap() +} + +pub fn watch_config_file( + executor: Arc, + fs: Arc, + path: PathBuf, +) -> mpsc::UnboundedReceiver { + let (tx, rx) = mpsc::unbounded(); + executor + .spawn(async move { + let events = fs.watch(&path, Duration::from_millis(100)).await; + futures::pin_mut!(events); + + let contents = fs.load(&path).await.unwrap_or_default(); + if tx.unbounded_send(contents).is_err() { + return; + } + + loop { + if events.next().await.is_none() { + break; + } + + if let Ok(contents) = fs.load(&path).await { + if !tx.unbounded_send(contents).is_ok() { + break; + } + } + } + }) + .detach(); + rx +} + +pub fn handle_settings_file_changes( + mut user_settings_file_rx: mpsc::UnboundedReceiver, + cx: &mut AppContext, +) { + let user_settings_content = cx.background().block(user_settings_file_rx.next()).unwrap(); + cx.update_global(|store: &mut SettingsStore, cx| { + store + .set_user_settings(&user_settings_content, cx) + .log_err(); + }); + cx.spawn(move |mut cx| async move { + while let Some(user_settings_content) = user_settings_file_rx.next().await { + cx.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store + .set_user_settings(&user_settings_content, cx) + .log_err(); + }); + cx.refresh_windows(); + }); + } + }) + .detach(); +} + +async fn load_settings(fs: &Arc) -> Result { + match fs.load(&paths::SETTINGS).await { + result @ Ok(_) => result, + Err(err) => { + if let Some(e) = err.downcast_ref::() { + if e.kind() == ErrorKind::NotFound { + return Ok(crate::initial_user_settings_content().to_string()); + } + } + return Err(err); + } + } +} + +pub fn update_settings_file( + fs: Arc, + cx: &mut AppContext, + update: impl 'static + Send + FnOnce(&mut T::FileContent), +) { + cx.spawn(|cx| async move { + let old_text = load_settings(&fs).await; + let new_text = cx.read_global(|store: &SettingsStore, cx| { + store.new_text_for_update::(old_text, update) + }); + fs.atomic_write(paths::SETTINGS.clone(), new_text).await?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); +} diff --git a/crates/settings2/src/settings_store.rs b/crates/settings2/src/settings_store.rs new file mode 100644 index 0000000000..354985a686 --- /dev/null +++ b/crates/settings2/src/settings_store.rs @@ -0,0 +1,1268 @@ +use anyhow::{anyhow, Context, Result}; +use collections::{btree_map, hash_map, BTreeMap, HashMap}; +use gpui2::AppContext; +use lazy_static::lazy_static; +use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema}; +use serde::{de::DeserializeOwned, Deserialize as _, Serialize}; +use smallvec::SmallVec; +use std::{ + any::{type_name, Any, TypeId}, + fmt::Debug, + ops::Range, + path::Path, + str, + sync::Arc, +}; +use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _}; + +/// A value that can be defined as a user setting. +/// +/// Settings can be loaded from a combination of multiple JSON files. +pub trait Setting: 'static { + /// The name of a key within the JSON file from which this setting should + /// be deserialized. If this is `None`, then the setting will be deserialized + /// from the root object. + const KEY: Option<&'static str>; + + /// The type that is stored in an individual JSON file. + type FileContent: Clone + Default + Serialize + DeserializeOwned + JsonSchema; + + /// The logic for combining together values from one or more JSON files into the + /// final value for this setting. + /// + /// The user values are ordered from least specific (the global settings file) + /// to most specific (the innermost local settings file). + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + cx: &AppContext, + ) -> Result + where + Self: Sized; + + fn json_schema( + generator: &mut SchemaGenerator, + _: &SettingsJsonSchemaParams, + _: &AppContext, + ) -> RootSchema { + generator.root_schema_for::() + } + + fn json_merge( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + ) -> Result { + let mut merged = serde_json::Value::Null; + for value in [default_value].iter().chain(user_values) { + merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged); + } + Ok(serde_json::from_value(merged)?) + } + + fn load_via_json_merge( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + ) -> Result + where + Self: DeserializeOwned, + { + let mut merged = serde_json::Value::Null; + for value in [default_value].iter().chain(user_values) { + merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged); + } + Ok(serde_json::from_value(merged)?) + } + + fn missing_default() -> anyhow::Error { + anyhow::anyhow!("missing default") + } +} + +pub struct SettingsJsonSchemaParams<'a> { + pub staff_mode: bool, + pub language_names: &'a [String], +} + +/// A set of strongly-typed setting values defined via multiple JSON files. +pub struct SettingsStore { + setting_values: HashMap>, + raw_default_settings: serde_json::Value, + raw_user_settings: serde_json::Value, + raw_local_settings: BTreeMap<(usize, Arc), serde_json::Value>, + tab_size_callback: Option<(TypeId, Box Option>)>, +} + +impl Default for SettingsStore { + fn default() -> Self { + SettingsStore { + setting_values: Default::default(), + raw_default_settings: serde_json::json!({}), + raw_user_settings: serde_json::json!({}), + raw_local_settings: Default::default(), + tab_size_callback: Default::default(), + } + } +} + +#[derive(Debug)] +struct SettingValue { + global_value: Option, + local_values: Vec<(usize, Arc, T)>, +} + +trait AnySettingValue { + fn key(&self) -> Option<&'static str>; + fn setting_type_name(&self) -> &'static str; + fn deserialize_setting(&self, json: &serde_json::Value) -> Result; + fn load_setting( + &self, + default_value: &DeserializedSetting, + custom: &[DeserializedSetting], + cx: &AppContext, + ) -> Result>; + fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any; + fn set_global_value(&mut self, value: Box); + fn set_local_value(&mut self, root_id: usize, path: Arc, value: Box); + fn json_schema( + &self, + generator: &mut SchemaGenerator, + _: &SettingsJsonSchemaParams, + cx: &AppContext, + ) -> RootSchema; +} + +struct DeserializedSetting(Box); + +impl SettingsStore { + /// Add a new type of setting to the store. + pub fn register_setting(&mut self, cx: &AppContext) { + let setting_type_id = TypeId::of::(); + let entry = self.setting_values.entry(setting_type_id); + if matches!(entry, hash_map::Entry::Occupied(_)) { + return; + } + + let setting_value = entry.or_insert(Box::new(SettingValue:: { + global_value: None, + local_values: Vec::new(), + })); + + if let Some(default_settings) = setting_value + .deserialize_setting(&self.raw_default_settings) + .log_err() + { + let mut user_values_stack = Vec::new(); + + if let Some(user_settings) = setting_value + .deserialize_setting(&self.raw_user_settings) + .log_err() + { + user_values_stack = vec![user_settings]; + } + + if let Some(setting) = setting_value + .load_setting(&default_settings, &user_values_stack, cx) + .context("A default setting must be added to the `default.json` file") + .log_err() + { + setting_value.set_global_value(setting); + } + } + } + + /// Get the value of a setting. + /// + /// Panics if the given setting type has not been registered, or if there is no + /// value for this setting. + pub fn get(&self, path: Option<(usize, &Path)>) -> &T { + self.setting_values + .get(&TypeId::of::()) + .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) + .value_for_path(path) + .downcast_ref::() + .expect("no default value for setting type") + } + + /// Override the global value for a setting. + /// + /// The given value will be overwritten if the user settings file changes. + pub fn override_global(&mut self, value: T) { + self.setting_values + .get_mut(&TypeId::of::()) + .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) + .set_global_value(Box::new(value)) + } + + /// Get the user's settings as a raw JSON value. + /// + /// This is only for debugging and reporting. For user-facing functionality, + /// use the typed setting interface. + pub fn raw_user_settings(&self) -> &serde_json::Value { + &self.raw_user_settings + } + + #[cfg(any(test, feature = "test-support"))] + pub fn test(cx: &AppContext) -> Self { + let mut this = Self::default(); + this.set_default_settings(&crate::test_settings(), cx) + .unwrap(); + this.set_user_settings("{}", cx).unwrap(); + this + } + + /// Update the value of a setting in the user's global configuration. + /// + /// This is only for tests. Normally, settings are only loaded from + /// JSON files. + #[cfg(any(test, feature = "test-support"))] + pub fn update_user_settings( + &mut self, + cx: &AppContext, + update: impl FnOnce(&mut T::FileContent), + ) { + let old_text = serde_json::to_string(&self.raw_user_settings).unwrap(); + let new_text = self.new_text_for_update::(old_text, update); + self.set_user_settings(&new_text, cx).unwrap(); + } + + /// Update the value of a setting in a JSON file, returning the new text + /// for that JSON file. + pub fn new_text_for_update( + &self, + old_text: String, + update: impl FnOnce(&mut T::FileContent), + ) -> String { + let edits = self.edits_for_update::(&old_text, update); + let mut new_text = old_text; + for (range, replacement) in edits.into_iter() { + new_text.replace_range(range, &replacement); + } + new_text + } + + /// Update the value of a setting in a JSON file, returning a list + /// of edits to apply to the JSON file. + pub fn edits_for_update( + &self, + text: &str, + update: impl FnOnce(&mut T::FileContent), + ) -> Vec<(Range, String)> { + let setting_type_id = TypeId::of::(); + + let setting = self + .setting_values + .get(&setting_type_id) + .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())); + let raw_settings = parse_json_with_comments::(text).unwrap_or_default(); + let old_content = match setting.deserialize_setting(&raw_settings) { + Ok(content) => content.0.downcast::().unwrap(), + Err(_) => Box::new(T::FileContent::default()), + }; + let mut new_content = old_content.clone(); + update(&mut new_content); + + let old_value = serde_json::to_value(&old_content).unwrap(); + let new_value = serde_json::to_value(new_content).unwrap(); + + let mut key_path = Vec::new(); + if let Some(key) = T::KEY { + key_path.push(key); + } + + let mut edits = Vec::new(); + let tab_size = self.json_tab_size(); + let mut text = text.to_string(); + update_value_in_json_text( + &mut text, + &mut key_path, + tab_size, + &old_value, + &new_value, + &mut edits, + ); + return edits; + } + + /// Configure the tab sized when updating JSON files. + pub fn set_json_tab_size_callback( + &mut self, + get_tab_size: fn(&T) -> Option, + ) { + self.tab_size_callback = Some(( + TypeId::of::(), + Box::new(move |value| get_tab_size(value.downcast_ref::().unwrap())), + )); + } + + fn json_tab_size(&self) -> usize { + const DEFAULT_JSON_TAB_SIZE: usize = 2; + + if let Some((setting_type_id, callback)) = &self.tab_size_callback { + let setting_value = self.setting_values.get(setting_type_id).unwrap(); + let value = setting_value.value_for_path(None); + if let Some(value) = callback(value) { + return value; + } + } + + DEFAULT_JSON_TAB_SIZE + } + + /// Set the default settings via a JSON string. + /// + /// The string should contain a JSON object with a default value for every setting. + pub fn set_default_settings( + &mut self, + default_settings_content: &str, + cx: &AppContext, + ) -> Result<()> { + let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?; + if settings.is_object() { + self.raw_default_settings = settings; + self.recompute_values(None, cx)?; + Ok(()) + } else { + Err(anyhow!("settings must be an object")) + } + } + + /// Set the user settings via a JSON string. + pub fn set_user_settings( + &mut self, + user_settings_content: &str, + cx: &AppContext, + ) -> Result<()> { + let settings: serde_json::Value = parse_json_with_comments(user_settings_content)?; + if settings.is_object() { + self.raw_user_settings = settings; + self.recompute_values(None, cx)?; + Ok(()) + } else { + Err(anyhow!("settings must be an object")) + } + } + + /// Add or remove a set of local settings via a JSON string. + pub fn set_local_settings( + &mut self, + root_id: usize, + path: Arc, + settings_content: Option<&str>, + cx: &AppContext, + ) -> Result<()> { + if let Some(content) = settings_content { + self.raw_local_settings + .insert((root_id, path.clone()), parse_json_with_comments(content)?); + } else { + self.raw_local_settings.remove(&(root_id, path.clone())); + } + self.recompute_values(Some((root_id, &path)), cx)?; + Ok(()) + } + + /// Add or remove a set of local settings via a JSON string. + pub fn clear_local_settings(&mut self, root_id: usize, cx: &AppContext) -> Result<()> { + self.raw_local_settings.retain(|k, _| k.0 != root_id); + self.recompute_values(Some((root_id, "".as_ref())), cx)?; + Ok(()) + } + + pub fn local_settings(&self, root_id: usize) -> impl '_ + Iterator, String)> { + self.raw_local_settings + .range((root_id, Path::new("").into())..(root_id + 1, Path::new("").into())) + .map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap())) + } + + pub fn json_schema( + &self, + schema_params: &SettingsJsonSchemaParams, + cx: &AppContext, + ) -> serde_json::Value { + use schemars::{ + gen::SchemaSettings, + schema::{Schema, SchemaObject}, + }; + + let settings = SchemaSettings::draft07().with(|settings| { + settings.option_add_null_type = false; + }); + let mut generator = SchemaGenerator::new(settings); + let mut combined_schema = RootSchema::default(); + + for setting_value in self.setting_values.values() { + let setting_schema = setting_value.json_schema(&mut generator, schema_params, cx); + combined_schema + .definitions + .extend(setting_schema.definitions); + + let target_schema = if let Some(key) = setting_value.key() { + let key_schema = combined_schema + .schema + .object() + .properties + .entry(key.to_string()) + .or_insert_with(|| Schema::Object(SchemaObject::default())); + if let Schema::Object(key_schema) = key_schema { + key_schema + } else { + continue; + } + } else { + &mut combined_schema.schema + }; + + merge_schema(target_schema, setting_schema.schema); + } + + fn merge_schema(target: &mut SchemaObject, source: SchemaObject) { + if let Some(source) = source.object { + let target_properties = &mut target.object().properties; + for (key, value) in source.properties { + match target_properties.entry(key) { + btree_map::Entry::Vacant(e) => { + e.insert(value); + } + btree_map::Entry::Occupied(e) => { + if let (Schema::Object(target), Schema::Object(src)) = + (e.into_mut(), value) + { + merge_schema(target, src); + } + } + } + } + } + + overwrite(&mut target.instance_type, source.instance_type); + overwrite(&mut target.string, source.string); + overwrite(&mut target.number, source.number); + overwrite(&mut target.reference, source.reference); + overwrite(&mut target.array, source.array); + overwrite(&mut target.enum_values, source.enum_values); + + fn overwrite(target: &mut Option, source: Option) { + if let Some(source) = source { + *target = Some(source); + } + } + } + + serde_json::to_value(&combined_schema).unwrap() + } + + fn recompute_values( + &mut self, + changed_local_path: Option<(usize, &Path)>, + cx: &AppContext, + ) -> Result<()> { + // Reload the global and local values for every setting. + let mut user_settings_stack = Vec::::new(); + let mut paths_stack = Vec::>::new(); + for setting_value in self.setting_values.values_mut() { + let default_settings = setting_value.deserialize_setting(&self.raw_default_settings)?; + + user_settings_stack.clear(); + paths_stack.clear(); + + if let Some(user_settings) = setting_value + .deserialize_setting(&self.raw_user_settings) + .log_err() + { + user_settings_stack.push(user_settings); + paths_stack.push(None); + } + + // If the global settings file changed, reload the global value for the field. + if changed_local_path.is_none() { + if let Some(value) = setting_value + .load_setting(&default_settings, &user_settings_stack, cx) + .log_err() + { + setting_value.set_global_value(value); + } + } + + // Reload the local values for the setting. + for ((root_id, path), local_settings) in &self.raw_local_settings { + // Build a stack of all of the local values for that setting. + while let Some(prev_entry) = paths_stack.last() { + if let Some((prev_root_id, prev_path)) = prev_entry { + if root_id != prev_root_id || !path.starts_with(prev_path) { + paths_stack.pop(); + user_settings_stack.pop(); + continue; + } + } + break; + } + + if let Some(local_settings) = + setting_value.deserialize_setting(&local_settings).log_err() + { + paths_stack.push(Some((*root_id, path.as_ref()))); + user_settings_stack.push(local_settings); + + // If a local settings file changed, then avoid recomputing local + // settings for any path outside of that directory. + if changed_local_path.map_or(false, |(changed_root_id, changed_local_path)| { + *root_id != changed_root_id || !path.starts_with(changed_local_path) + }) { + continue; + } + + if let Some(value) = setting_value + .load_setting(&default_settings, &user_settings_stack, cx) + .log_err() + { + setting_value.set_local_value(*root_id, path.clone(), value); + } + } + } + } + Ok(()) + } +} + +impl Debug for SettingsStore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SettingsStore") + .field( + "types", + &self + .setting_values + .values() + .map(|value| value.setting_type_name()) + .collect::>(), + ) + .field("default_settings", &self.raw_default_settings) + .field("user_settings", &self.raw_user_settings) + .field("local_settings", &self.raw_local_settings) + .finish_non_exhaustive() + } +} + +impl AnySettingValue for SettingValue { + fn key(&self) -> Option<&'static str> { + T::KEY + } + + fn setting_type_name(&self) -> &'static str { + type_name::() + } + + fn load_setting( + &self, + default_value: &DeserializedSetting, + user_values: &[DeserializedSetting], + cx: &AppContext, + ) -> Result> { + let default_value = default_value.0.downcast_ref::().unwrap(); + let values: SmallVec<[&T::FileContent; 6]> = user_values + .iter() + .map(|value| value.0.downcast_ref().unwrap()) + .collect(); + Ok(Box::new(T::load(default_value, &values, cx)?)) + } + + fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result { + if let Some(key) = T::KEY { + if let Some(value) = json.get(key) { + json = value; + } else { + let value = T::FileContent::default(); + return Ok(DeserializedSetting(Box::new(value))); + } + } + let value = T::FileContent::deserialize(json)?; + Ok(DeserializedSetting(Box::new(value))) + } + + fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any { + if let Some((root_id, path)) = path { + for (settings_root_id, settings_path, value) in self.local_values.iter().rev() { + if root_id == *settings_root_id && path.starts_with(&settings_path) { + return value; + } + } + } + self.global_value + .as_ref() + .unwrap_or_else(|| panic!("no default value for setting {}", self.setting_type_name())) + } + + fn set_global_value(&mut self, value: Box) { + self.global_value = Some(*value.downcast().unwrap()); + } + + fn set_local_value(&mut self, root_id: usize, path: Arc, value: Box) { + let value = *value.downcast().unwrap(); + match self + .local_values + .binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1)) + { + Ok(ix) => self.local_values[ix].2 = value, + Err(ix) => self.local_values.insert(ix, (root_id, path, value)), + } + } + + fn json_schema( + &self, + generator: &mut SchemaGenerator, + params: &SettingsJsonSchemaParams, + cx: &AppContext, + ) -> RootSchema { + T::json_schema(generator, params, cx) + } +} + +fn update_value_in_json_text<'a>( + text: &mut String, + key_path: &mut Vec<&'a str>, + tab_size: usize, + old_value: &'a serde_json::Value, + new_value: &'a serde_json::Value, + edits: &mut Vec<(Range, String)>, +) { + // If the old and new values are both objects, then compare them key by key, + // preserving the comments and formatting of the unchanged parts. Otherwise, + // replace the old value with the new value. + if let (serde_json::Value::Object(old_object), serde_json::Value::Object(new_object)) = + (old_value, new_value) + { + for (key, old_sub_value) in old_object.iter() { + key_path.push(key); + let new_sub_value = new_object.get(key).unwrap_or(&serde_json::Value::Null); + update_value_in_json_text( + text, + key_path, + tab_size, + old_sub_value, + new_sub_value, + edits, + ); + key_path.pop(); + } + for (key, new_sub_value) in new_object.iter() { + key_path.push(key); + if !old_object.contains_key(key) { + update_value_in_json_text( + text, + key_path, + tab_size, + &serde_json::Value::Null, + new_sub_value, + edits, + ); + } + key_path.pop(); + } + } else if old_value != new_value { + let mut new_value = new_value.clone(); + if let Some(new_object) = new_value.as_object_mut() { + new_object.retain(|_, v| !v.is_null()); + } + let (range, replacement) = + replace_value_in_json_text(text, &key_path, tab_size, &new_value); + text.replace_range(range.clone(), &replacement); + edits.push((range, replacement)); + } +} + +fn replace_value_in_json_text( + text: &str, + key_path: &[&str], + tab_size: usize, + new_value: &serde_json::Value, +) -> (Range, String) { + const LANGUAGE_OVERRIDES: &'static str = "language_overrides"; + const LANGUAGES: &'static str = "languages"; + + lazy_static! { + static ref PAIR_QUERY: tree_sitter::Query = tree_sitter::Query::new( + tree_sitter_json::language(), + "(pair key: (string) @key value: (_) @value)", + ) + .unwrap(); + } + + let mut parser = tree_sitter::Parser::new(); + parser.set_language(tree_sitter_json::language()).unwrap(); + let syntax_tree = parser.parse(text, None).unwrap(); + + let mut cursor = tree_sitter::QueryCursor::new(); + + let has_language_overrides = text.contains(LANGUAGE_OVERRIDES); + + let mut depth = 0; + let mut last_value_range = 0..0; + let mut first_key_start = None; + let mut existing_value_range = 0..text.len(); + let matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes()); + for mat in matches { + if mat.captures.len() != 2 { + continue; + } + + let key_range = mat.captures[0].node.byte_range(); + let value_range = mat.captures[1].node.byte_range(); + + // Don't enter sub objects until we find an exact + // match for the current keypath + if last_value_range.contains_inclusive(&value_range) { + continue; + } + + last_value_range = value_range.clone(); + + if key_range.start > existing_value_range.end { + break; + } + + first_key_start.get_or_insert_with(|| key_range.start); + + let found_key = text + .get(key_range.clone()) + .map(|key_text| { + if key_path[depth] == LANGUAGES && has_language_overrides { + return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES); + } else { + return key_text == format!("\"{}\"", key_path[depth]); + } + }) + .unwrap_or(false); + + if found_key { + existing_value_range = value_range; + // Reset last value range when increasing in depth + last_value_range = existing_value_range.start..existing_value_range.start; + depth += 1; + + if depth == key_path.len() { + break; + } else { + first_key_start = None; + } + } + } + + // We found the exact key we want, insert the new value + if depth == key_path.len() { + let new_val = to_pretty_json(&new_value, tab_size, tab_size * depth); + (existing_value_range, new_val) + } else { + // We have key paths, construct the sub objects + let new_key = if has_language_overrides && key_path[depth] == LANGUAGES { + LANGUAGE_OVERRIDES + } else { + key_path[depth] + }; + + // We don't have the key, construct the nested objects + let mut new_value = serde_json::to_value(new_value).unwrap(); + for key in key_path[(depth + 1)..].iter().rev() { + if has_language_overrides && key == &LANGUAGES { + new_value = serde_json::json!({ LANGUAGE_OVERRIDES.to_string(): new_value }); + } else { + new_value = serde_json::json!({ key.to_string(): new_value }); + } + } + + if let Some(first_key_start) = first_key_start { + let mut row = 0; + let mut column = 0; + for (ix, char) in text.char_indices() { + if ix == first_key_start { + break; + } + if char == '\n' { + row += 1; + column = 0; + } else { + column += char.len_utf8(); + } + } + + if row > 0 { + // depth is 0 based, but division needs to be 1 based. + let new_val = to_pretty_json(&new_value, column / (depth + 1), column); + let space = ' '; + let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column); + (first_key_start..first_key_start, content) + } else { + let new_val = serde_json::to_string(&new_value).unwrap(); + let mut content = format!(r#""{new_key}": {new_val},"#); + content.push(' '); + (first_key_start..first_key_start, content) + } + } else { + new_value = serde_json::json!({ new_key.to_string(): new_value }); + let indent_prefix_len = 4 * depth; + let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len); + if depth == 0 { + new_val.push('\n'); + } + + (existing_value_range, new_val) + } + } +} + +fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: usize) -> String { + const SPACES: [u8; 32] = [b' '; 32]; + + debug_assert!(indent_size <= SPACES.len()); + debug_assert!(indent_prefix_len <= SPACES.len()); + + let mut output = Vec::new(); + let mut ser = serde_json::Serializer::with_formatter( + &mut output, + serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]), + ); + + value.serialize(&mut ser).unwrap(); + let text = String::from_utf8(output).unwrap(); + + let mut adjusted_text = String::new(); + for (i, line) in text.split('\n').enumerate() { + if i > 0 { + adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap()); + } + adjusted_text.push_str(line); + adjusted_text.push('\n'); + } + adjusted_text.pop(); + adjusted_text +} + +pub fn parse_json_with_comments(content: &str) -> Result { + Ok(serde_json_lenient::from_str(content)?) +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// use serde_derive::Deserialize; +// use unindent::Unindent; + +// #[gpui::test] +// fn test_settings_store_basic(cx: &mut AppContext) { +// let mut store = SettingsStore::default(); +// store.register_setting::(cx); +// store.register_setting::(cx); +// store.register_setting::(cx); +// store +// .set_default_settings( +// r#"{ +// "turbo": false, +// "user": { +// "name": "John Doe", +// "age": 30, +// "staff": false +// } +// }"#, +// cx, +// ) +// .unwrap(); + +// assert_eq!(store.get::(None), &TurboSetting(false)); +// assert_eq!( +// store.get::(None), +// &UserSettings { +// name: "John Doe".to_string(), +// age: 30, +// staff: false, +// } +// ); +// assert_eq!( +// store.get::(None), +// &MultiKeySettings { +// key1: String::new(), +// key2: String::new(), +// } +// ); + +// store +// .set_user_settings( +// r#"{ +// "turbo": true, +// "user": { "age": 31 }, +// "key1": "a" +// }"#, +// cx, +// ) +// .unwrap(); + +// assert_eq!(store.get::(None), &TurboSetting(true)); +// assert_eq!( +// store.get::(None), +// &UserSettings { +// name: "John Doe".to_string(), +// age: 31, +// staff: false +// } +// ); + +// store +// .set_local_settings( +// 1, +// Path::new("/root1").into(), +// Some(r#"{ "user": { "staff": true } }"#), +// cx, +// ) +// .unwrap(); +// store +// .set_local_settings( +// 1, +// Path::new("/root1/subdir").into(), +// Some(r#"{ "user": { "name": "Jane Doe" } }"#), +// cx, +// ) +// .unwrap(); + +// store +// .set_local_settings( +// 1, +// Path::new("/root2").into(), +// Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#), +// cx, +// ) +// .unwrap(); + +// assert_eq!( +// store.get::(Some((1, Path::new("/root1/something")))), +// &UserSettings { +// name: "John Doe".to_string(), +// age: 31, +// staff: true +// } +// ); +// assert_eq!( +// store.get::(Some((1, Path::new("/root1/subdir/something")))), +// &UserSettings { +// name: "Jane Doe".to_string(), +// age: 31, +// staff: true +// } +// ); +// assert_eq!( +// store.get::(Some((1, Path::new("/root2/something")))), +// &UserSettings { +// name: "John Doe".to_string(), +// age: 42, +// staff: false +// } +// ); +// assert_eq!( +// store.get::(Some((1, Path::new("/root2/something")))), +// &MultiKeySettings { +// key1: "a".to_string(), +// key2: "b".to_string(), +// } +// ); +// } + +// #[gpui::test] +// fn test_setting_store_assign_json_before_register(cx: &mut AppContext) { +// let mut store = SettingsStore::default(); +// store +// .set_default_settings( +// r#"{ +// "turbo": true, +// "user": { +// "name": "John Doe", +// "age": 30, +// "staff": false +// }, +// "key1": "x" +// }"#, +// cx, +// ) +// .unwrap(); +// store +// .set_user_settings(r#"{ "turbo": false }"#, cx) +// .unwrap(); +// store.register_setting::(cx); +// store.register_setting::(cx); + +// assert_eq!(store.get::(None), &TurboSetting(false)); +// assert_eq!( +// store.get::(None), +// &UserSettings { +// name: "John Doe".to_string(), +// age: 30, +// staff: false, +// } +// ); + +// store.register_setting::(cx); +// assert_eq!( +// store.get::(None), +// &MultiKeySettings { +// key1: "x".into(), +// key2: String::new(), +// } +// ); +// } + +// #[gpui::test] +// fn test_setting_store_update(cx: &mut AppContext) { +// let mut store = SettingsStore::default(); +// store.register_setting::(cx); +// store.register_setting::(cx); +// store.register_setting::(cx); + +// // entries added and updated +// check_settings_update::( +// &mut store, +// r#"{ +// "languages": { +// "JSON": { +// "language_setting_1": true +// } +// } +// }"# +// .unindent(), +// |settings| { +// settings +// .languages +// .get_mut("JSON") +// .unwrap() +// .language_setting_1 = Some(false); +// settings.languages.insert( +// "Rust".into(), +// LanguageSettingEntry { +// language_setting_2: Some(true), +// ..Default::default() +// }, +// ); +// }, +// r#"{ +// "languages": { +// "Rust": { +// "language_setting_2": true +// }, +// "JSON": { +// "language_setting_1": false +// } +// } +// }"# +// .unindent(), +// cx, +// ); + +// // weird formatting +// check_settings_update::( +// &mut store, +// r#"{ +// "user": { "age": 36, "name": "Max", "staff": true } +// }"# +// .unindent(), +// |settings| settings.age = Some(37), +// r#"{ +// "user": { "age": 37, "name": "Max", "staff": true } +// }"# +// .unindent(), +// cx, +// ); + +// // single-line formatting, other keys +// check_settings_update::( +// &mut store, +// r#"{ "one": 1, "two": 2 }"#.unindent(), +// |settings| settings.key1 = Some("x".into()), +// r#"{ "key1": "x", "one": 1, "two": 2 }"#.unindent(), +// cx, +// ); + +// // empty object +// check_settings_update::( +// &mut store, +// r#"{ +// "user": {} +// }"# +// .unindent(), +// |settings| settings.age = Some(37), +// r#"{ +// "user": { +// "age": 37 +// } +// }"# +// .unindent(), +// cx, +// ); + +// // no content +// check_settings_update::( +// &mut store, +// r#""#.unindent(), +// |settings| settings.age = Some(37), +// r#"{ +// "user": { +// "age": 37 +// } +// } +// "# +// .unindent(), +// cx, +// ); + +// check_settings_update::( +// &mut store, +// r#"{ +// } +// "# +// .unindent(), +// |settings| settings.age = Some(37), +// r#"{ +// "user": { +// "age": 37 +// } +// } +// "# +// .unindent(), +// cx, +// ); +// } + +// fn check_settings_update( +// store: &mut SettingsStore, +// old_json: String, +// update: fn(&mut T::FileContent), +// expected_new_json: String, +// cx: &mut AppContext, +// ) { +// store.set_user_settings(&old_json, cx).ok(); +// let edits = store.edits_for_update::(&old_json, update); +// let mut new_json = old_json; +// for (range, replacement) in edits.into_iter() { +// new_json.replace_range(range, &replacement); +// } +// pretty_assertions::assert_eq!(new_json, expected_new_json); +// } + +// #[derive(Debug, PartialEq, Deserialize)] +// struct UserSettings { +// name: String, +// age: u32, +// staff: bool, +// } + +// #[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] +// struct UserSettingsJson { +// name: Option, +// age: Option, +// staff: Option, +// } + +// impl Setting for UserSettings { +// const KEY: Option<&'static str> = Some("user"); +// type FileContent = UserSettingsJson; + +// fn load( +// default_value: &UserSettingsJson, +// user_values: &[&UserSettingsJson], +// _: &AppContext, +// ) -> Result { +// Self::load_via_json_merge(default_value, user_values) +// } +// } + +// #[derive(Debug, Deserialize, PartialEq)] +// struct TurboSetting(bool); + +// impl Setting for TurboSetting { +// const KEY: Option<&'static str> = Some("turbo"); +// type FileContent = Option; + +// fn load( +// default_value: &Option, +// user_values: &[&Option], +// _: &AppContext, +// ) -> Result { +// Self::load_via_json_merge(default_value, user_values) +// } +// } + +// #[derive(Clone, Debug, PartialEq, Deserialize)] +// struct MultiKeySettings { +// #[serde(default)] +// key1: String, +// #[serde(default)] +// key2: String, +// } + +// #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +// struct MultiKeySettingsJson { +// key1: Option, +// key2: Option, +// } + +// impl Setting for MultiKeySettings { +// const KEY: Option<&'static str> = None; + +// type FileContent = MultiKeySettingsJson; + +// fn load( +// default_value: &MultiKeySettingsJson, +// user_values: &[&MultiKeySettingsJson], +// _: &AppContext, +// ) -> Result { +// Self::load_via_json_merge(default_value, user_values) +// } +// } + +// #[derive(Debug, Deserialize)] +// struct JournalSettings { +// pub path: String, +// pub hour_format: HourFormat, +// } + +// #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +// #[serde(rename_all = "snake_case")] +// enum HourFormat { +// Hour12, +// Hour24, +// } + +// #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] +// struct JournalSettingsJson { +// pub path: Option, +// pub hour_format: Option, +// } + +// impl Setting for JournalSettings { +// const KEY: Option<&'static str> = Some("journal"); + +// type FileContent = JournalSettingsJson; + +// fn load( +// default_value: &JournalSettingsJson, +// user_values: &[&JournalSettingsJson], +// _: &AppContext, +// ) -> Result { +// Self::load_via_json_merge(default_value, user_values) +// } +// } + +// #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +// struct LanguageSettings { +// #[serde(default)] +// languages: HashMap, +// } + +// #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +// struct LanguageSettingEntry { +// language_setting_1: Option, +// language_setting_2: Option, +// } + +// impl Setting for LanguageSettings { +// const KEY: Option<&'static str> = None; + +// type FileContent = Self; + +// fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Result { +// Self::load_via_json_merge(default_value, user_values) +// } +// } +// } diff --git a/crates/storybook2/Cargo.toml b/crates/storybook2/Cargo.toml index e9e85415ca..e5bd0aea02 100644 --- a/crates/storybook2/Cargo.toml +++ b/crates/storybook2/Cargo.toml @@ -28,4 +28,4 @@ ui = { package = "ui2", path = "../ui2", features = ["stories"] } util = { path = "../util" } [dev-dependencies] -gpui2 = { path = "../gpui2", features = ["test"] } +gpui2 = { path = "../gpui2", features = ["test-support"] } diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index cf9e669a2b..29b061adba 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -3,14 +3,15 @@ use gpui2::{ div, view, Context, Focusable, KeyBinding, ParentElement, StatelessInteractive, Styled, View, WindowContext, }; +use serde::Deserialize; -#[derive(Clone, PartialEq)] +#[derive(Clone, Default, PartialEq, Deserialize)] struct ActionA; -#[derive(Clone, PartialEq)] +#[derive(Clone, Default, PartialEq, Deserialize)] struct ActionB; -#[derive(Clone, PartialEq)] +#[derive(Clone, Default, PartialEq, Deserialize)] struct ActionC; pub struct FocusStory { @@ -24,6 +25,8 @@ impl FocusStory { KeyBinding::new("cmd-a", ActionB, Some("child-1")), KeyBinding::new("cmd-c", ActionC, None), ]); + cx.register_action_type::(); + cx.register_action_type::(); let theme = rose_pine(); let color_1 = theme.lowest.negative.default.foreground; diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml new file mode 100644 index 0000000000..fc40c5f950 --- /dev/null +++ b/crates/zed2/Cargo.toml @@ -0,0 +1,180 @@ +[package] +description = "The fast, collaborative code editor." +edition = "2021" +name = "zed2" +version = "0.109.0" +publish = false + +[lib] +name = "zed2" +path = "src/zed2.rs" +doctest = false + +[[bin]] +name = "Zed" +path = "src/main.rs" + +[dependencies] +# audio = { path = "../audio" } +# activity_indicator = { path = "../activity_indicator" } +# auto_update = { path = "../auto_update" } +# breadcrumbs = { path = "../breadcrumbs" } +# call = { path = "../call" } +# channel = { path = "../channel" } +cli = { path = "../cli" } +# collab_ui = { path = "../collab_ui" } +collections = { path = "../collections" } +# command_palette = { path = "../command_palette" } +# component_test = { path = "../component_test" } +# context_menu = { path = "../context_menu" } +# client = { path = "../client" } +# clock = { path = "../clock" } +# copilot = { path = "../copilot" } +# copilot_button = { path = "../copilot_button" } +# diagnostics = { path = "../diagnostics" } +# db = { path = "../db" } +# editor = { path = "../editor" } +# feedback = { path = "../feedback" } +# file_finder = { path = "../file_finder" } +# search = { path = "../search" } +fs = { path = "../fs" } +fsevent = { path = "../fsevent" } +fuzzy = { path = "../fuzzy" } +# go_to_line = { path = "../go_to_line" } +gpui2 = { path = "../gpui2" } +install_cli = { path = "../install_cli" } +# journal = { path = "../journal" } +# language = { path = "../language" } +# language_selector = { path = "../language_selector" } +lsp = { path = "../lsp" } +language_tools = { path = "../language_tools" } +node_runtime = { path = "../node_runtime" } +# assistant = { path = "../assistant" } +# outline = { path = "../outline" } +# plugin_runtime = { path = "../plugin_runtime",optional = true } +# project = { path = "../project" } +# project_panel = { path = "../project_panel" } +# project_symbols = { path = "../project_symbols" } +# quick_action_bar = { path = "../quick_action_bar" } +# recent_projects = { path = "../recent_projects" } +rpc = { path = "../rpc" } +settings2 = { path = "../settings2" } +feature_flags = { path = "../feature_flags" } +sum_tree = { path = "../sum_tree" } +shellexpand = "2.1.0" +text = { path = "../text" } +# terminal_view = { path = "../terminal_view" } +# theme = { path = "../theme" } +# theme_selector = { path = "../theme_selector" } +util = { path = "../util" } +# semantic_index = { path = "../semantic_index" } +# vim = { path = "../vim" } +# workspace = { path = "../workspace" } +# welcome = { path = "../welcome" } +# zed-actions = {path = "../zed-actions"} +anyhow.workspace = true +async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-tar = "0.4.2" +async-recursion = "0.3" +async-trait.workspace = true +backtrace = "0.3" +chrono = "0.4" +ctor = "0.1.20" +env_logger.workspace = true +futures.workspace = true +ignore = "0.4" +image = "0.23" +indexmap = "1.6.2" +isahc.workspace = true +lazy_static.workspace = true +libc = "0.2" +log.workspace = true +num_cpus = "1.13.0" +parking_lot.workspace = true +postage.workspace = true +rand.workspace = true +regex.workspace = true +rsa = "0.4" +rust-embed.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +schemars.workspace = true +simplelog = "0.9" +smallvec.workspace = true +smol.workspace = true +tempdir.workspace = true +thiserror.workspace = true +tiny_http = "0.8" +toml.workspace = true +tree-sitter.workspace = true +tree-sitter-bash.workspace = true +tree-sitter-c.workspace = true +tree-sitter-cpp.workspace = true +tree-sitter-css.workspace = true +tree-sitter-elixir.workspace = true +tree-sitter-elm.workspace = true +tree-sitter-embedded-template.workspace = true +tree-sitter-glsl.workspace = true +tree-sitter-go.workspace = true +tree-sitter-heex.workspace = true +tree-sitter-json.workspace = true +tree-sitter-rust.workspace = true +tree-sitter-markdown.workspace = true +tree-sitter-python.workspace = true +tree-sitter-toml.workspace = true +tree-sitter-typescript.workspace = true +tree-sitter-ruby.workspace = true +tree-sitter-html.workspace = true +tree-sitter-php.workspace = true +tree-sitter-scheme.workspace = true +tree-sitter-svelte.workspace = true +tree-sitter-racket.workspace = true +tree-sitter-yaml.workspace = true +tree-sitter-lua.workspace = true +tree-sitter-nix.workspace = true +tree-sitter-nu.workspace = true + +url = "2.2" +urlencoding = "2.1.2" +uuid.workspace = true + +[dev-dependencies] +# call = { path = "../call", features = ["test-support"] } +# client = { path = "../client", features = ["test-support"] } +# editor = { path = "../editor", features = ["test-support"] } +# gpui = { path = "../gpui", features = ["test-support"] } +gpui2 = { path = "../gpui2", features = ["test-support"] } +# language = { path = "../language", features = ["test-support"] } +# lsp = { path = "../lsp", features = ["test-support"] } +# project = { path = "../project", features = ["test-support"] } +# rpc = { path = "../rpc", features = ["test-support"] } +# settings = { path = "../settings", features = ["test-support"] } +# text = { path = "../text", features = ["test-support"] } +# util = { path = "../util", features = ["test-support"] } +# workspace = { path = "../workspace", features = ["test-support"] } +unindent.workspace = true + +[package.metadata.bundle-dev] +icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"] +identifier = "dev.zed.Zed-Dev" +name = "Zed Dev" +osx_minimum_system_version = "10.15.7" +osx_info_plist_exts = ["resources/info/*"] +osx_url_schemes = ["zed-dev"] + +[package.metadata.bundle-preview] +icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"] +identifier = "dev.zed.Zed-Preview" +name = "Zed Preview" +osx_minimum_system_version = "10.15.7" +osx_info_plist_exts = ["resources/info/*"] +osx_url_schemes = ["zed-preview"] + +[package.metadata.bundle-stable] +icon = ["resources/app-icon@2x.png", "resources/app-icon.png"] +identifier = "dev.zed.Zed" +name = "Zed" +osx_minimum_system_version = "10.15.7" +osx_info_plist_exts = ["resources/info/*"] +osx_url_schemes = ["zed"] diff --git a/crates/zed2/resources/app-icon-preview.png b/crates/zed2/resources/app-icon-preview.png new file mode 100644 index 0000000000..b76e578858 Binary files /dev/null and b/crates/zed2/resources/app-icon-preview.png differ diff --git a/crates/zed2/resources/app-icon-preview@2x.png b/crates/zed2/resources/app-icon-preview@2x.png new file mode 100644 index 0000000000..6e08503927 Binary files /dev/null and b/crates/zed2/resources/app-icon-preview@2x.png differ diff --git a/crates/zed2/resources/app-icon.png b/crates/zed2/resources/app-icon.png new file mode 100644 index 0000000000..08b6d8afa0 Binary files /dev/null and b/crates/zed2/resources/app-icon.png differ diff --git a/crates/zed2/resources/app-icon@2x.png b/crates/zed2/resources/app-icon@2x.png new file mode 100644 index 0000000000..5bb5754bc1 Binary files /dev/null and b/crates/zed2/resources/app-icon@2x.png differ diff --git a/crates/zed2/resources/info/DocumentTypes.plist b/crates/zed2/resources/info/DocumentTypes.plist new file mode 100644 index 0000000000..d043fa8ab9 --- /dev/null +++ b/crates/zed2/resources/info/DocumentTypes.plist @@ -0,0 +1,62 @@ +CFBundleDocumentTypes + + + CFBundleTypeIconFile + Document + CFBundleTypeRole + Editor + LSHandlerRank + Alternate + LSItemContentTypes + + public.text + public.plain-text + public.utf8-plain-text + + + + CFBundleTypeIconFile + Document + CFBundleTypeName + Zed Text Document + CFBundleTypeRole + Editor + CFBundleTypeOSTypes + + **** + + LSHandlerRank + Default + CFBundleTypeExtensions + + Gemfile + c + c++ + cc + cpp + css + erb + ex + exs + go + h + h++ + hh + hpp + html + js + json + jsx + md + py + rb + rkt + rs + scm + toml + ts + tsx + txt + + + diff --git a/crates/zed2/resources/info/Permissions.plist b/crates/zed2/resources/info/Permissions.plist new file mode 100644 index 0000000000..bded5a82e2 --- /dev/null +++ b/crates/zed2/resources/info/Permissions.plist @@ -0,0 +1,24 @@ +NSSystemAdministrationUsageDescription +The operation being performed by a program in Zed requires elevated permission. +NSAppleEventsUsageDescription +An application in Zed wants to use AppleScript. +NSBluetoothAlwaysUsageDescription +An application in Zed wants to use Bluetooth. +NSCalendarsUsageDescription +An application in Zed wants to use Calendar data. +NSCameraUsageDescription +An application in Zed wants to use the camera. +NSContactsUsageDescription +An application in Zed wants to use your contacts. +NSLocationAlwaysUsageDescription +An application in Zed wants to use your location information, even in the background. +NSLocationUsageDescription +An application in Zed wants to use your location information. +NSLocationWhenInUseUsageDescription +An application in Zed wants to use your location information while active. +NSMicrophoneUsageDescription +An application in Zed wants to use your microphone. +NSSpeechRecognitionUsageDescription +An application in Zed wants to use speech recognition. +NSRemindersUsageDescription +An application in Zed wants to use your reminders. diff --git a/crates/zed2/resources/zed.entitlements b/crates/zed2/resources/zed.entitlements new file mode 100644 index 0000000000..f40a8a253a --- /dev/null +++ b/crates/zed2/resources/zed.entitlements @@ -0,0 +1,24 @@ + + + + + com.apple.security.automation.apple-events + + com.apple.security.cs.allow-jit + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + com.apple.security.personal-information.addressbook + + com.apple.security.personal-information.calendars + + com.apple.security.personal-information.location + + com.apple.security.personal-information.photos-library + + + + diff --git a/crates/zed2/src/assets.rs b/crates/zed2/src/assets.rs new file mode 100644 index 0000000000..9751067cef --- /dev/null +++ b/crates/zed2/src/assets.rs @@ -0,0 +1,33 @@ +use anyhow::anyhow; +use gpui2::{AssetSource, Result, SharedString}; +use rust_embed::RustEmbed; + +#[derive(RustEmbed)] +#[folder = "../../assets"] +#[include = "fonts/**/*"] +#[include = "icons/**/*"] +#[include = "themes/**/*"] +#[include = "sounds/**/*"] +#[include = "*.md"] +#[exclude = "*.DS_Store"] +pub struct Assets; + +impl AssetSource for Assets { + fn load(&self, path: &SharedString) -> Result> { + Self::get(path) + .map(|f| f.data) + .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) + } + + fn list(&self, path: &SharedString) -> Result> { + Ok(Self::iter() + .filter_map(|p| { + if p.starts_with(path.as_ref()) { + Some(p.into()) + } else { + None + } + }) + .collect()) + } +} diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs new file mode 100644 index 0000000000..4330f5af59 --- /dev/null +++ b/crates/zed2/src/main.rs @@ -0,0 +1,912 @@ +// Allow binary to be called Zed for a nice application menu when running executable directly +#![allow(non_snake_case)] + +use crate::open_listener::{OpenListener, OpenRequest}; +use anyhow::{anyhow, Context, Result}; +use cli::{ + ipc::{self, IpcSender}, + CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME, +}; +use fs::RealFs; +use futures::{channel::mpsc, SinkExt, StreamExt}; +use gpui2::{App, AsyncAppContext, Task}; +use log::LevelFilter; + +use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; +use settings::{default_settings, handle_settings_file_changes, watch_config_file, SettingsStore}; +use simplelog::ConfigBuilder; +use smol::process::Command; +use std::{ + collections::HashMap, + env, + fs::OpenOptions, + io::IsTerminal, + path::Path, + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, Weak, + }, + thread, +}; +use util::{channel::RELEASE_CHANNEL, http, paths, ResultExt}; +use zed2::{ensure_only_instance, AppState, Assets, IsOnlyInstance}; +// use zed2::{ +// assets::Assets, +// build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, +// only_instance::{ensure_only_instance, IsOnlyInstance}, +// }; + +mod open_listener; + +fn main() { + let http = http::client(); + init_paths(); + init_logger(); + + if ensure_only_instance() != IsOnlyInstance::Yes { + return; + } + + log::info!("========== starting zed =========="); + let mut app = App::production(Arc::new(Assets)); + + // let installation_id = app.background().block(installation_id()).ok(); + // let session_id = Uuid::new_v4().to_string(); + // init_panic_hook(&app, installation_id.clone(), session_id.clone()); + + load_embedded_fonts(&app); + + let fs = Arc::new(RealFs); + let user_settings_file_rx = + watch_config_file(app.executor(), fs.clone(), paths::SETTINGS.clone()); + let user_keymap_file_rx = watch_config_file(app.executor(), fs.clone(), paths::KEYMAP.clone()); + + let login_shell_env_loaded = if stdout_is_a_pty() { + Task::ready(()) + } else { + app.executor().spawn(async { + load_login_shell_environment().await.log_err(); + }) + }; + + let (listener, mut open_rx) = OpenListener::new(); + let listener = Arc::new(listener); + let callback_listener = listener.clone(); + app.on_open_urls(move |urls, _| callback_listener.open_urls(urls)) + .on_reopen(move |cx| { + if cx.has_global::>() { + if let Some(app_state) = cx.global::>().upgrade() { + // todo!("workspace") + // workspace::open_new(&app_state, cx, |workspace, cx| { + // Editor::new_file(workspace, &Default::default(), cx) + // }) + // .detach(); + } + } + }); + + app.run(move |cx| { + cx.set_global(*RELEASE_CHANNEL); + + let mut store = SettingsStore::default(); + store + .set_default_settings(default_settings().as_ref(), cx) + .unwrap(); + cx.set_global(store); + handle_settings_file_changes(user_settings_file_rx, cx); + // handle_keymap_file_changes(user_keymap_file_rx, cx); + + // let client = client::Client::new(http.clone(), cx); + // let mut languages = LanguageRegistry::new(login_shell_env_loaded); + // let copilot_language_server_id = languages.next_language_server_id(); + // languages.set_executor(cx.background().clone()); + // languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone()); + // let languages = Arc::new(languages); + // let node_runtime = RealNodeRuntime::new(http.clone()); + + // languages::init(languages.clone(), node_runtime.clone(), cx); + // let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); + // let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); + + // cx.set_global(client.clone()); + + // theme::init(Assets, cx); + // context_menu::init(cx); + // project::Project::init(&client, cx); + // client::init(&client, cx); + // command_palette::init(cx); + // language::init(cx); + // editor::init(cx); + // go_to_line::init(cx); + // file_finder::init(cx); + // outline::init(cx); + // project_symbols::init(cx); + // project_panel::init(Assets, cx); + // channel::init(&client, user_store.clone(), cx); + // diagnostics::init(cx); + // search::init(cx); + // semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); + // vim::init(cx); + // terminal_view::init(cx); + // copilot::init( + // copilot_language_server_id, + // http.clone(), + // node_runtime.clone(), + // cx, + // ); + // assistant::init(cx); + // component_test::init(cx); + + // cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); + // cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) + // .detach(); + // watch_file_types(fs.clone(), cx); + + // languages.set_theme(theme::current(cx).clone()); + // cx.observe_global::({ + // let languages = languages.clone(); + // move |cx| languages.set_theme(theme::current(cx).clone()) + // }) + // .detach(); + + // client.telemetry().start(installation_id, session_id, cx); + + // todo!("app_state") + let app_state = Arc::new(AppState); + // let app_state = Arc::new(AppState { + // languages, + // client: client.clone(), + // user_store, + // fs, + // build_window_options, + // initialize_workspace, + // background_actions, + // workspace_store, + // node_runtime, + // }); + // cx.set_global(Arc::downgrade(&app_state)); + + // audio::init(Assets, cx); + // auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx); + + // todo!("workspace") + // workspace::init(app_state.clone(), cx); + // recent_projects::init(cx); + + // journal::init(app_state.clone(), cx); + // language_selector::init(cx); + // theme_selector::init(cx); + // activity_indicator::init(cx); + // language_tools::init(cx); + // call::init(app_state.client.clone(), app_state.user_store.clone(), cx); + // collab_ui::init(&app_state, cx); + // feedback::init(cx); + // welcome::init(cx); + // zed::init(&app_state, cx); + + // cx.set_menus(menus::menus()); + + if stdout_is_a_pty() { + cx.activate(true); + let urls = collect_url_args(); + if !urls.is_empty() { + listener.open_urls(urls) + } + } else { + upload_previous_panics(http.clone(), cx); + + // TODO Development mode that forces the CLI mode usually runs Zed binary as is instead + // of an *app, hence gets no specific callbacks run. Emulate them here, if needed. + if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some() + && !listener.triggered.load(Ordering::Acquire) + { + listener.open_urls(collect_url_args()) + } + } + + let mut triggered_authentication = false; + + match open_rx.try_next() { + Ok(Some(OpenRequest::Paths { paths })) => { + // todo!("workspace") + // cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) + // .detach(); + } + Ok(Some(OpenRequest::CliConnection { connection })) => { + cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) + .detach(); + } + Ok(Some(OpenRequest::JoinChannel { channel_id })) => { + // triggered_authentication = true; + // let app_state = app_state.clone(); + // let client = client.clone(); + // cx.spawn(|mut cx| async move { + // // ignore errors here, we'll show a generic "not signed in" + // let _ = authenticate(client, &cx).await; + // cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx)) + // .await + // }) + // .detach_and_log_err(cx) + } + Ok(None) | Err(_) => cx + .spawn({ + let app_state = app_state.clone(); + |cx| async move { restore_or_create_workspace(&app_state, cx).await } + }) + .detach(), + } + + cx.spawn(|mut cx| { + let app_state = app_state.clone(); + async move { + while let Some(request) = open_rx.next().await { + match request { + OpenRequest::Paths { paths } => { + // todo!("workspace") + // cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) + // .detach(); + } + OpenRequest::CliConnection { connection } => { + cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) + .detach(); + } + OpenRequest::JoinChannel { channel_id } => { + // cx + // .update(|cx| { + // workspace::join_channel(channel_id, app_state.clone(), None, cx) + // }) + // .detach() + } + } + } + } + }) + .detach(); + + // if !triggered_authentication { + // cx.spawn(|cx| async move { authenticate(client, &cx).await }) + // .detach_and_log_err(cx); + // } + }); +} + +// async fn authenticate(client: Arc, cx: &AsyncAppContext) -> Result<()> { +// if stdout_is_a_pty() { +// if client::IMPERSONATE_LOGIN.is_some() { +// client.authenticate_and_connect(false, &cx).await?; +// } +// } else if client.has_keychain_credentials(&cx) { +// client.authenticate_and_connect(true, &cx).await?; +// } +// Ok::<_, anyhow::Error>(()) +// } + +// async fn installation_id() -> Result { +// let legacy_key_name = "device_id"; + +// if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(legacy_key_name) { +// Ok(installation_id) +// } else { +// let installation_id = Uuid::new_v4().to_string(); + +// KEY_VALUE_STORE +// .write_kvp(legacy_key_name.to_string(), installation_id.clone()) +// .await?; + +// Ok(installation_id) +// } +// } + +async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncAppContext) { + todo!("workspace") + // if let Some(location) = workspace::last_opened_workspace_paths().await { + // cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx)) + // .await + // .log_err(); + // } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { + // cx.update(|cx| show_welcome_experience(app_state, cx)); + // } else { + // cx.update(|cx| { + // workspace::open_new(app_state, cx, |workspace, cx| { + // Editor::new_file(workspace, &Default::default(), cx) + // }) + // .detach(); + // }); + // } +} + +fn init_paths() { + std::fs::create_dir_all(&*util::paths::CONFIG_DIR).expect("could not create config path"); + std::fs::create_dir_all(&*util::paths::LANGUAGES_DIR).expect("could not create languages path"); + std::fs::create_dir_all(&*util::paths::DB_DIR).expect("could not create database path"); + std::fs::create_dir_all(&*util::paths::LOGS_DIR).expect("could not create logs path"); +} + +fn init_logger() { + if stdout_is_a_pty() { + env_logger::init(); + } else { + let level = LevelFilter::Info; + + // Prevent log file from becoming too large. + const KIB: u64 = 1024; + const MIB: u64 = 1024 * KIB; + const MAX_LOG_BYTES: u64 = MIB; + if std::fs::metadata(&*paths::LOG).map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES) + { + let _ = std::fs::rename(&*paths::LOG, &*paths::OLD_LOG); + } + + let log_file = OpenOptions::new() + .create(true) + .append(true) + .open(&*paths::LOG) + .expect("could not open logfile"); + + let config = ConfigBuilder::new() + .set_time_format_str("%Y-%m-%dT%T") //All timestamps are UTC + .build(); + + simplelog::WriteLogger::init(level, config, log_file).expect("could not initialize logger"); + } +} + +#[derive(Serialize, Deserialize)] +struct LocationData { + file: String, + line: u32, +} + +#[derive(Serialize, Deserialize)] +struct Panic { + thread: String, + payload: String, + #[serde(skip_serializing_if = "Option::is_none")] + location_data: Option, + backtrace: Vec, + app_version: String, + release_channel: String, + os_name: String, + os_version: Option, + architecture: String, + panicked_on: u128, + #[serde(skip_serializing_if = "Option::is_none")] + installation_id: Option, + session_id: String, +} + +#[derive(Serialize)] +struct PanicRequest { + panic: Panic, + token: String, +} + +static PANIC_COUNT: AtomicU32 = AtomicU32::new(0); + +// fn init_panic_hook(app: &App, installation_id: Option, session_id: String) { +// let is_pty = stdout_is_a_pty(); +// let platform = app.platform(); + +// panic::set_hook(Box::new(move |info| { +// let prior_panic_count = PANIC_COUNT.fetch_add(1, Ordering::SeqCst); +// if prior_panic_count > 0 { +// // Give the panic-ing thread time to write the panic file +// loop { +// std::thread::yield_now(); +// } +// } + +// let thread = thread::current(); +// let thread_name = thread.name().unwrap_or(""); + +// let payload = info +// .payload() +// .downcast_ref::<&str>() +// .map(|s| s.to_string()) +// .or_else(|| info.payload().downcast_ref::().map(|s| s.clone())) +// .unwrap_or_else(|| "Box".to_string()); + +// if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Dev { +// let location = info.location().unwrap(); +// let backtrace = Backtrace::new(); +// eprintln!( +// "Thread {:?} panicked with {:?} at {}:{}:{}\n{:?}", +// thread_name, +// payload, +// location.file(), +// location.line(), +// location.column(), +// backtrace, +// ); +// std::process::exit(-1); +// } + +// let app_version = ZED_APP_VERSION +// .or_else(|| platform.app_version().ok()) +// .map_or("dev".to_string(), |v| v.to_string()); + +// let backtrace = Backtrace::new(); +// let mut backtrace = backtrace +// .frames() +// .iter() +// .filter_map(|frame| Some(format!("{:#}", frame.symbols().first()?.name()?))) +// .collect::>(); + +// // Strip out leading stack frames for rust panic-handling. +// if let Some(ix) = backtrace +// .iter() +// .position(|name| name == "rust_begin_unwind") +// { +// backtrace.drain(0..=ix); +// } + +// let panic_data = Panic { +// thread: thread_name.into(), +// payload: payload.into(), +// location_data: info.location().map(|location| LocationData { +// file: location.file().into(), +// line: location.line(), +// }), +// app_version: app_version.clone(), +// release_channel: RELEASE_CHANNEL.display_name().into(), +// os_name: platform.os_name().into(), +// os_version: platform +// .os_version() +// .ok() +// .map(|os_version| os_version.to_string()), +// architecture: env::consts::ARCH.into(), +// panicked_on: SystemTime::now() +// .duration_since(UNIX_EPOCH) +// .unwrap() +// .as_millis(), +// backtrace, +// installation_id: installation_id.clone(), +// session_id: session_id.clone(), +// }; + +// if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() { +// log::error!("{}", panic_data_json); +// } + +// if !is_pty { +// if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { +// let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); +// let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp)); +// let panic_file = std::fs::OpenOptions::new() +// .append(true) +// .create(true) +// .open(&panic_file_path) +// .log_err(); +// if let Some(mut panic_file) = panic_file { +// writeln!(&mut panic_file, "{}", panic_data_json).log_err(); +// panic_file.flush().log_err(); +// } +// } +// } + +// std::process::abort(); +// })); +// } + +// fn upload_previous_panics(http: Arc, cx: &mut AppContext) { +// let telemetry_settings = *settings::get::(cx); + +// cx.background() +// .spawn({ +// async move { +// let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL); +// let mut children = smol::fs::read_dir(&*paths::LOGS_DIR).await?; +// while let Some(child) = children.next().await { +// let child = child?; +// let child_path = child.path(); + +// if child_path.extension() != Some(OsStr::new("panic")) { +// continue; +// } +// let filename = if let Some(filename) = child_path.file_name() { +// filename.to_string_lossy() +// } else { +// continue; +// }; + +// if !filename.starts_with("zed") { +// continue; +// } + +// if telemetry_settings.diagnostics { +// let panic_file_content = smol::fs::read_to_string(&child_path) +// .await +// .context("error reading panic file")?; + +// let panic = serde_json::from_str(&panic_file_content) +// .ok() +// .or_else(|| { +// panic_file_content +// .lines() +// .next() +// .and_then(|line| serde_json::from_str(line).ok()) +// }) +// .unwrap_or_else(|| { +// log::error!( +// "failed to deserialize panic file {:?}", +// panic_file_content +// ); +// None +// }); + +// if let Some(panic) = panic { +// let body = serde_json::to_string(&PanicRequest { +// panic, +// token: ZED_SECRET_CLIENT_TOKEN.into(), +// }) +// .unwrap(); + +// let request = Request::post(&panic_report_url) +// .redirect_policy(isahc::config::RedirectPolicy::Follow) +// .header("Content-Type", "application/json") +// .body(body.into())?; +// let response = +// http.send(request).await.context("error sending panic")?; +// if !response.status().is_success() { +// log::error!( +// "Error uploading panic to server: {}", +// response.status() +// ); +// } +// } +// } + +// // We've done what we can, delete the file +// std::fs::remove_file(child_path) +// .context("error removing panic") +// .log_err(); +// } +// Ok::<_, anyhow::Error>(()) +// } +// .log_err() +// }) +// .detach(); +// } + +async fn load_login_shell_environment() -> Result<()> { + let marker = "ZED_LOGIN_SHELL_START"; + let shell = env::var("SHELL").context( + "SHELL environment variable is not assigned so we can't source login environment variables", + )?; + let output = Command::new(&shell) + .args(["-lic", &format!("echo {marker} && /usr/bin/env -0")]) + .output() + .await + .context("failed to spawn login shell to source login environment variables")?; + if !output.status.success() { + Err(anyhow!("login shell exited with error"))?; + } + + let stdout = String::from_utf8_lossy(&output.stdout); + + if let Some(env_output_start) = stdout.find(marker) { + let env_output = &stdout[env_output_start + marker.len()..]; + for line in env_output.split_terminator('\0') { + if let Some(separator_index) = line.find('=') { + let key = &line[..separator_index]; + let value = &line[separator_index + 1..]; + env::set_var(key, value); + } + } + log::info!( + "set environment variables from shell:{}, path:{}", + shell, + env::var("PATH").unwrap_or_default(), + ); + } + + Ok(()) +} + +fn stdout_is_a_pty() -> bool { + std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && std::io::stdout().is_terminal() +} + +fn collect_url_args() -> Vec { + env::args() + .skip(1) + .filter_map(|arg| match std::fs::canonicalize(Path::new(&arg)) { + Ok(path) => Some(format!("file://{}", path.to_string_lossy())), + Err(error) => { + if let Some(_) = parse_zed_link(&arg) { + Some(arg) + } else { + log::error!("error parsing path argument: {}", error); + None + } + } + }) + .collect() +} + +fn load_embedded_fonts(app: &App) { + let font_paths = Assets.list("fonts"); + let embedded_fonts = Mutex::new(Vec::new()); + smol::block_on(app.background().scoped(|scope| { + for font_path in &font_paths { + if !font_path.ends_with(".ttf") { + continue; + } + + scope.spawn(async { + let font_path = &*font_path; + let font_bytes = Assets.load(font_path).unwrap().to_vec(); + embedded_fonts.lock().push(Arc::from(font_bytes)); + }); + } + })); + app.platform() + .fonts() + .add_fonts(&embedded_fonts.into_inner()) + .unwrap(); +} + +// #[cfg(debug_assertions)] +// async fn watch_themes(fs: Arc, mut cx: AsyncAppContext) -> Option<()> { +// let mut events = fs +// .watch("styles/src".as_ref(), Duration::from_millis(100)) +// .await; +// while (events.next().await).is_some() { +// let output = Command::new("npm") +// .current_dir("styles") +// .args(["run", "build"]) +// .output() +// .await +// .log_err()?; +// if output.status.success() { +// cx.update(|cx| theme_selector::reload(cx)) +// } else { +// eprintln!( +// "build script failed {}", +// String::from_utf8_lossy(&output.stderr) +// ); +// } +// } +// Some(()) +// } + +// #[cfg(debug_assertions)] +// async fn watch_languages(fs: Arc, languages: Arc) -> Option<()> { +// let mut events = fs +// .watch( +// "crates/zed/src/languages".as_ref(), +// Duration::from_millis(100), +// ) +// .await; +// while (events.next().await).is_some() { +// languages.reload(); +// } +// Some(()) +// } + +// #[cfg(debug_assertions)] +// fn watch_file_types(fs: Arc, cx: &mut AppContext) { +// cx.spawn(|mut cx| async move { +// let mut events = fs +// .watch( +// "assets/icons/file_icons/file_types.json".as_ref(), +// Duration::from_millis(100), +// ) +// .await; +// while (events.next().await).is_some() { +// cx.update(|cx| { +// cx.update_global(|file_types, _| { +// *file_types = project_panel::file_associations::FileAssociations::new(Assets); +// }); +// }) +// } +// }) +// .detach() +// } + +// #[cfg(not(debug_assertions))] +// async fn watch_themes(_fs: Arc, _cx: AsyncAppContext) -> Option<()> { +// None +// } + +// #[cfg(not(debug_assertions))] +// async fn watch_languages(_: Arc, _: Arc) -> Option<()> { +// None +// } + +// #[cfg(not(debug_assertions))] +// fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} + +fn connect_to_cli( + server_name: &str, +) -> Result<(mpsc::Receiver, IpcSender)> { + let handshake_tx = cli::ipc::IpcSender::::connect(server_name.to_string()) + .context("error connecting to cli")?; + let (request_tx, request_rx) = ipc::channel::()?; + let (response_tx, response_rx) = ipc::channel::()?; + + handshake_tx + .send(IpcHandshake { + requests: request_tx, + responses: response_rx, + }) + .context("error sending ipc handshake")?; + + let (mut async_request_tx, async_request_rx) = + futures::channel::mpsc::channel::(16); + thread::spawn(move || { + while let Ok(cli_request) = request_rx.recv() { + if smol::block_on(async_request_tx.send(cli_request)).is_err() { + break; + } + } + Ok::<_, anyhow::Error>(()) + }); + + Ok((async_request_rx, response_tx)) +} + +async fn handle_cli_connection( + (mut requests, responses): (mpsc::Receiver, IpcSender), + app_state: Arc, + mut cx: AsyncAppContext, +) { + if let Some(request) = requests.next().await { + match request { + CliRequest::Open { paths, wait } => { + let mut caret_positions = HashMap::new(); + + // todo!("workspace") + // let paths = if paths.is_empty() { + // workspace::last_opened_workspace_paths() + // .await + // .map(|location| location.paths().to_vec()) + // .unwrap_or_default() + // } else { + // paths + // .into_iter() + // .filter_map(|path_with_position_string| { + // let path_with_position = PathLikeWithPosition::parse_str( + // &path_with_position_string, + // |path_str| { + // Ok::<_, std::convert::Infallible>( + // Path::new(path_str).to_path_buf(), + // ) + // }, + // ) + // .expect("Infallible"); + // let path = path_with_position.path_like; + // if let Some(row) = path_with_position.row { + // if path.is_file() { + // let row = row.saturating_sub(1); + // let col = + // path_with_position.column.unwrap_or(0).saturating_sub(1); + // caret_positions.insert(path.clone(), Point::new(row, col)); + // } + // } + // Some(path) + // }) + // .collect() + // }; + + // let mut errored = false; + // match cx + // .update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) + // .await + // { + // Ok((workspace, items)) => { + // let mut item_release_futures = Vec::new(); + + // for (item, path) in items.into_iter().zip(&paths) { + // match item { + // Some(Ok(item)) => { + // if let Some(point) = caret_positions.remove(path) { + // if let Some(active_editor) = item.downcast::() { + // active_editor + // .downgrade() + // .update(&mut cx, |editor, cx| { + // let snapshot = + // editor.snapshot(cx).display_snapshot; + // let point = snapshot + // .buffer_snapshot + // .clip_point(point, Bias::Left); + // editor.change_selections( + // Some(Autoscroll::center()), + // cx, + // |s| s.select_ranges([point..point]), + // ); + // }) + // .log_err(); + // } + // } + + // let released = oneshot::channel(); + // cx.update(|cx| { + // item.on_release( + // cx, + // Box::new(move |_| { + // let _ = released.0.send(()); + // }), + // ) + // .detach(); + // }); + // item_release_futures.push(released.1); + // } + // Some(Err(err)) => { + // responses + // .send(CliResponse::Stderr { + // message: format!("error opening {:?}: {}", path, err), + // }) + // .log_err(); + // errored = true; + // } + // None => {} + // } + // } + + // if wait { + // let background = cx.background(); + // let wait = async move { + // if paths.is_empty() { + // let (done_tx, done_rx) = oneshot::channel(); + // if let Some(workspace) = workspace.upgrade(&cx) { + // let _subscription = cx.update(|cx| { + // cx.observe_release(&workspace, move |_, _| { + // let _ = done_tx.send(()); + // }) + // }); + // drop(workspace); + // let _ = done_rx.await; + // } + // } else { + // let _ = + // futures::future::try_join_all(item_release_futures).await; + // }; + // } + // .fuse(); + // futures::pin_mut!(wait); + + // loop { + // // Repeatedly check if CLI is still open to avoid wasting resources + // // waiting for files or workspaces to close. + // let mut timer = background.timer(Duration::from_secs(1)).fuse(); + // futures::select_biased! { + // _ = wait => break, + // _ = timer => { + // if responses.send(CliResponse::Ping).is_err() { + // break; + // } + // } + // } + // } + // } + // } + // Err(error) => { + // errored = true; + // responses + // .send(CliResponse::Stderr { + // message: format!("error opening {:?}: {}", paths, error), + // }) + // .log_err(); + // } + // } + + // responses + // .send(CliResponse::Exit { + // status: i32::from(errored), + // }) + // .log_err(); + } + } + } +} + +// pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { +// &[ +// ("Go to file", &file_finder::Toggle), +// ("Open command palette", &command_palette::Toggle), +// ("Open recent projects", &recent_projects::OpenRecent), +// ("Change your settings", &zed_actions::OpenSettings), +// ] +// } diff --git a/crates/zed2/src/only_instance.rs b/crates/zed2/src/only_instance.rs new file mode 100644 index 0000000000..b252f72ce5 --- /dev/null +++ b/crates/zed2/src/only_instance.rs @@ -0,0 +1,104 @@ +use std::{ + io::{Read, Write}, + net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use util::channel::ReleaseChannel; + +const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); +const CONNECT_TIMEOUT: Duration = Duration::from_millis(10); +const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35); +const SEND_TIMEOUT: Duration = Duration::from_millis(20); + +fn address() -> SocketAddr { + let port = match *util::channel::RELEASE_CHANNEL { + ReleaseChannel::Dev => 43737, + ReleaseChannel::Preview => 43738, + ReleaseChannel::Stable => 43739, + }; + + SocketAddr::V4(SocketAddrV4::new(LOCALHOST, port)) +} + +fn instance_handshake() -> &'static str { + match *util::channel::RELEASE_CHANNEL { + ReleaseChannel::Dev => "Zed Editor Dev Instance Running", + ReleaseChannel::Preview => "Zed Editor Preview Instance Running", + ReleaseChannel::Stable => "Zed Editor Stable Instance Running", + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IsOnlyInstance { + Yes, + No, +} + +pub fn ensure_only_instance() -> IsOnlyInstance { + // todo!("zed_stateless") + // if *db::ZED_STATELESS { + // return IsOnlyInstance::Yes; + // } + + if check_got_handshake() { + return IsOnlyInstance::No; + } + + let listener = match TcpListener::bind(address()) { + Ok(listener) => listener, + + Err(err) => { + log::warn!("Error binding to single instance port: {err}"); + if check_got_handshake() { + return IsOnlyInstance::No; + } + + // Avoid failing to start when some other application by chance already has + // a claim on the port. This is sub-par as any other instance that gets launched + // will be unable to communicate with this instance and will duplicate + log::warn!("Backup handshake request failed, continuing without handshake"); + return IsOnlyInstance::Yes; + } + }; + + thread::spawn(move || { + for stream in listener.incoming() { + let mut stream = match stream { + Ok(stream) => stream, + Err(_) => return, + }; + + _ = stream.set_nodelay(true); + _ = stream.set_read_timeout(Some(SEND_TIMEOUT)); + _ = stream.write_all(instance_handshake().as_bytes()); + } + }); + + IsOnlyInstance::Yes +} + +fn check_got_handshake() -> bool { + match TcpStream::connect_timeout(&address(), CONNECT_TIMEOUT) { + Ok(mut stream) => { + let mut buf = vec![0u8; instance_handshake().len()]; + + stream.set_read_timeout(Some(RECEIVE_TIMEOUT)).unwrap(); + if let Err(err) = stream.read_exact(&mut buf) { + log::warn!("Connected to single instance port but failed to read: {err}"); + return false; + } + + if buf == instance_handshake().as_bytes() { + log::info!("Got instance handshake"); + return true; + } + + log::warn!("Got wrong instance handshake value"); + false + } + + Err(_) => false, + } +} diff --git a/crates/zed2/src/open_listener.rs b/crates/zed2/src/open_listener.rs new file mode 100644 index 0000000000..9b416e14be --- /dev/null +++ b/crates/zed2/src/open_listener.rs @@ -0,0 +1,98 @@ +use anyhow::anyhow; +use cli::{ipc::IpcSender, CliRequest, CliResponse}; +use futures::channel::mpsc; +use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use std::ffi::OsStr; +use std::os::unix::prelude::OsStrExt; +use std::sync::atomic::Ordering; +use std::{path::PathBuf, sync::atomic::AtomicBool}; +use util::channel::parse_zed_link; +use util::ResultExt; + +use crate::connect_to_cli; + +pub enum OpenRequest { + Paths { + paths: Vec, + }, + CliConnection { + connection: (mpsc::Receiver, IpcSender), + }, + JoinChannel { + channel_id: u64, + }, +} + +pub struct OpenListener { + tx: UnboundedSender, + pub triggered: AtomicBool, +} + +impl OpenListener { + pub fn new() -> (Self, UnboundedReceiver) { + let (tx, rx) = mpsc::unbounded(); + ( + OpenListener { + tx, + triggered: AtomicBool::new(false), + }, + rx, + ) + } + + pub fn open_urls(&self, urls: Vec) { + self.triggered.store(true, Ordering::Release); + let request = if let Some(server_name) = + urls.first().and_then(|url| url.strip_prefix("zed-cli://")) + { + self.handle_cli_connection(server_name) + } else if let Some(request_path) = urls.first().and_then(|url| parse_zed_link(url)) { + self.handle_zed_url_scheme(request_path) + } else { + self.handle_file_urls(urls) + }; + + if let Some(request) = request { + self.tx + .unbounded_send(request) + .map_err(|_| anyhow!("no listener for open requests")) + .log_err(); + } + } + + fn handle_cli_connection(&self, server_name: &str) -> Option { + if let Some(connection) = connect_to_cli(server_name).log_err() { + return Some(OpenRequest::CliConnection { connection }); + } + + None + } + + fn handle_zed_url_scheme(&self, request_path: &str) -> Option { + let mut parts = request_path.split("/"); + if parts.next() == Some("channel") { + if let Some(slug) = parts.next() { + if let Some(id_str) = slug.split("-").last() { + if let Ok(channel_id) = id_str.parse::() { + return Some(OpenRequest::JoinChannel { channel_id }); + } + } + } + } + log::error!("invalid zed url: {}", request_path); + None + } + + fn handle_file_urls(&self, urls: Vec) -> Option { + let paths: Vec<_> = urls + .iter() + .flat_map(|url| url.strip_prefix("file://")) + .map(|url| { + let decoded = urlencoding::decode_binary(url.as_bytes()); + PathBuf::from(OsStr::from_bytes(decoded.as_ref())) + }) + .collect(); + + Some(OpenRequest::Paths { paths }) + } +} diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs new file mode 100644 index 0000000000..00d5b9a700 --- /dev/null +++ b/crates/zed2/src/zed2.rs @@ -0,0 +1,203 @@ +mod assets; +mod only_instance; +mod open_listener; + +pub use assets::*; +use gpui2::AsyncAppContext; +pub use only_instance::*; +pub use open_listener::*; + +use anyhow::{Context, Result}; +use cli::{ + ipc::{self, IpcSender}, + CliRequest, CliResponse, IpcHandshake, +}; +use futures::{channel::mpsc, SinkExt, StreamExt}; +use std::{sync::Arc, thread}; + +pub fn connect_to_cli( + server_name: &str, +) -> Result<(mpsc::Receiver, IpcSender)> { + let handshake_tx = cli::ipc::IpcSender::::connect(server_name.to_string()) + .context("error connecting to cli")?; + let (request_tx, request_rx) = ipc::channel::()?; + let (response_tx, response_rx) = ipc::channel::()?; + + handshake_tx + .send(IpcHandshake { + requests: request_tx, + responses: response_rx, + }) + .context("error sending ipc handshake")?; + + let (mut async_request_tx, async_request_rx) = + futures::channel::mpsc::channel::(16); + thread::spawn(move || { + while let Ok(cli_request) = request_rx.recv() { + if smol::block_on(async_request_tx.send(cli_request)).is_err() { + break; + } + } + Ok::<_, anyhow::Error>(()) + }); + + Ok((async_request_rx, response_tx)) +} + +pub struct AppState; + +pub async fn handle_cli_connection( + (mut requests, responses): (mpsc::Receiver, IpcSender), + app_state: Arc, + mut cx: AsyncAppContext, +) { + if let Some(request) = requests.next().await { + match request { + CliRequest::Open { paths, wait } => { + // let mut caret_positions = HashMap::new(); + + // let paths = if paths.is_empty() { + // todo!() + // workspace::last_opened_workspace_paths() + // .await + // .map(|location| location.paths().to_vec()) + // .unwrap_or_default() + // } else { + // paths + // .into_iter() + // .filter_map(|path_with_position_string| { + // let path_with_position = PathLikeWithPosition::parse_str( + // &path_with_position_string, + // |path_str| { + // Ok::<_, std::convert::Infallible>( + // Path::new(path_str).to_path_buf(), + // ) + // }, + // ) + // .expect("Infallible"); + // let path = path_with_position.path_like; + // if let Some(row) = path_with_position.row { + // if path.is_file() { + // let row = row.saturating_sub(1); + // let col = + // path_with_position.column.unwrap_or(0).saturating_sub(1); + // caret_positions.insert(path.clone(), Point::new(row, col)); + // } + // } + // Some(path) + // }) + // .collect() + // }; + + // let mut errored = false; + // todo!("workspace") + // match cx + // .update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) + // .await + // { + // Ok((workspace, items)) => { + // let mut item_release_futures = Vec::new(); + + // for (item, path) in items.into_iter().zip(&paths) { + // match item { + // Some(Ok(item)) => { + // if let Some(point) = caret_positions.remove(path) { + // if let Some(active_editor) = item.downcast::() { + // active_editor + // .downgrade() + // .update(&mut cx, |editor, cx| { + // let snapshot = + // editor.snapshot(cx).display_snapshot; + // let point = snapshot + // .buffer_snapshot + // .clip_point(point, Bias::Left); + // editor.change_selections( + // Some(Autoscroll::center()), + // cx, + // |s| s.select_ranges([point..point]), + // ); + // }) + // .log_err(); + // } + // } + + // let released = oneshot::channel(); + // cx.update(|cx| { + // item.on_release( + // cx, + // Box::new(move |_| { + // let _ = released.0.send(()); + // }), + // ) + // .detach(); + // }); + // item_release_futures.push(released.1); + // } + // Some(Err(err)) => { + // responses + // .send(CliResponse::Stderr { + // message: format!("error opening {:?}: {}", path, err), + // }) + // .log_err(); + // errored = true; + // } + // None => {} + // } + // } + + // if wait { + // let background = cx.background(); + // let wait = async move { + // if paths.is_empty() { + // let (done_tx, done_rx) = oneshot::channel(); + // if let Some(workspace) = workspace.upgrade(&cx) { + // let _subscription = cx.update(|cx| { + // cx.observe_release(&workspace, move |_, _| { + // let _ = done_tx.send(()); + // }) + // }); + // drop(workspace); + // let _ = done_rx.await; + // } + // } else { + // let _ = + // futures::future::try_join_all(item_release_futures).await; + // }; + // } + // .fuse(); + // futures::pin_mut!(wait); + + // loop { + // // Repeatedly check if CLI is still open to avoid wasting resources + // // waiting for files or workspaces to close. + // let mut timer = background.timer(Duration::from_secs(1)).fuse(); + // futures::select_biased! { + // _ = wait => break, + // _ = timer => { + // if responses.send(CliResponse::Ping).is_err() { + // break; + // } + // } + // } + // } + // } + // } + // Err(error) => { + // errored = true; + // responses + // .send(CliResponse::Stderr { + // message: format!("error opening {:?}: {}", paths, error), + // }) + // .log_err(); + // } + // } + + // responses + // .send(CliResponse::Exit { + // status: i32::from(errored), + // }) + // .log_err(); + } + } + } +}