Detailed changes
@@ -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"
@@ -81,6 +81,7 @@ members = [
"crates/welcome",
"crates/xtask",
"crates/zed",
+ "crates/zed2",
"crates/zed-actions"
]
default-members = ["crates/zed"]
@@ -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"
@@ -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<serde_json::Value>) -> Result<Box<dyn Action>>
+ where
+ Self: Sized;
+
fn partial_eq(&self, action: &dyn Action) -> bool;
fn boxed_clone(&self) -> Box<dyn Action>;
fn as_any(&self) -> &dyn Any;
}
-impl<T> Action for T
+impl<A> 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::<A>().into()
+ }
+
+ fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>>
+ 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),
@@ -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,
+ platform: MainThreadOnly::new(platform, executor.clone()),
flushing_effects: false,
+ pending_updates: 0,
next_frame_callbacks: Default::default(),
- platform: MainThreadOnly::new(platform, executor.clone()),
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<dyn Fn(&mut AppContext) -> bool + Send + Sync + 'static>;
type EventHandler = Box<dyn Fn(&dyn Any, &mut AppContext) -> bool + Send + Sync + 'static>;
type ReleaseHandler = Box<dyn Fn(&mut dyn Any, &mut AppContext) + Send + Sync + 'static>;
type FrameCallback = Box<dyn FnOnce(&mut WindowContext) + Send>;
+type ActionBuilder = fn(json: Option<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
pub struct AppContext {
this: Weak<Mutex<AppContext>>,
@@ -113,6 +119,7 @@ pub struct AppContext {
pub(crate) entities: EntityMap,
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
pub(crate) keymap: Arc<RwLock<Keymap>>,
+ action_builders: HashMap<SharedString, ActionBuilder>,
pub(crate) pending_notifications: HashSet<EntityId>,
pending_effects: VecDeque<Effect>,
pub(crate) observers: SubscriberSet<EntityId, Handler>,
@@ -134,6 +141,20 @@ impl AppContext {
result
}
+ pub(crate) fn read_window<R>(
+ &mut self,
+ id: WindowId,
+ read: impl FnOnce(&WindowContext) -> R,
+ ) -> Result<R> {
+ 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<R>(
&mut self,
id: WindowId,
@@ -385,6 +406,24 @@ impl AppContext {
.unwrap()
}
+ pub fn update_global<G, R>(&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::<G>())
+ .and_then(|stack| stack.pop())
+ .ok_or_else(|| anyhow!("no state of type {} exists", type_name::<G>()))
+ .unwrap();
+ let result = f(global.downcast_mut().unwrap(), self);
+ self.global_stacks_by_type
+ .get_mut(&TypeId::of::<G>())
+ .unwrap()
+ .push(global);
+ result
+ }
+
pub fn default_global<G: 'static + Default + Sync + Send>(&mut self) -> &mut G {
let stack = self
.global_stacks_by_type
@@ -396,6 +435,19 @@ impl AppContext {
stack.last_mut().unwrap().downcast_mut::<G>().unwrap()
}
+ pub fn set_global<T: Send + Sync + 'static>(&mut self, global: T) {
+ let global = Box::new(global);
+ let stack = self
+ .global_stacks_by_type
+ .entry(TypeId::of::<T>())
+ .or_default();
+ if let Some(last) = stack.last_mut() {
+ *last = global;
+ } else {
+ stack.push(global)
+ }
+ }
+
pub(crate) fn push_global<T: Send + Sync + 'static>(&mut self, state: T) {
self.global_stacks_by_type
.entry(TypeId::of::<T>())
@@ -422,9 +474,26 @@ impl AppContext {
self.keymap.write().add_bindings(bindings);
self.push_effect(Effect::Refresh);
}
+
+ pub fn register_action_type<A: Action>(&mut self) {
+ self.action_builders.insert(A::qualified_name(), A::build);
+ }
+
+ pub fn build_action(
+ &mut self,
+ name: &str,
+ params: Option<serde_json::Value>,
+ ) -> Result<Box<dyn Action>> {
+ 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> = T;
@@ -451,6 +520,10 @@ impl Context for AppContext {
result
})
}
+
+ fn read_global<G: 'static + Send + Sync, R>(&self, read: impl FnOnce(&G, &Self) -> R) -> R {
+ read(self.global(), self)
+ }
}
impl MainThread<AppContext> {
@@ -9,18 +9,19 @@ use std::sync::Weak;
pub struct AsyncAppContext(pub(crate) Weak<Mutex<AppContext>>);
impl Context for AsyncAppContext {
+ type BorrowedContext<'a, 'w> = AppContext;
type EntityContext<'a, 'w, T: 'static + Send + Sync> = ModelContext<'a, T>;
type Result<T> = Result<T>;
fn entity<T: Send + Sync + 'static>(
&mut self,
build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
- ) -> Result<Handle<T>> {
+ ) -> Self::Result<Handle<T>> {
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<T>,
update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
- ) -> Result<R> {
+ ) -> Self::Result<R> {
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.update_entity(handle, update))
}
+
+ fn read_global<G: 'static + Send + Sync, R>(
+ &self,
+ read: impl FnOnce(&G, &Self::BorrowedContext<'_, '_>) -> R,
+ ) -> Self::Result<R> {
+ 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<R>(
+ &self,
+ handle: AnyWindowHandle,
+ update: impl FnOnce(&WindowContext) -> R,
+ ) -> Result<R> {
+ let app = self
+ .0
+ .upgrade()
+ .ok_or_else(|| anyhow!("app was released"))?;
+ let mut app_context = app.lock();
+ app_context.read_window(handle.id, update)
+ }
+
pub fn update_window<R>(
&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<T> = Result<T>;
@@ -95,4 +122,11 @@ impl Context for AsyncWindowContext {
self.app
.update_window(self.window, |cx| cx.update_entity(handle, update))
}
+
+ fn read_global<G: 'static + Send + Sync, R>(
+ &self,
+ read: impl FnOnce(&G, &Self::BorrowedContext<'_, '_>) -> R,
+ ) -> Result<R> {
+ self.app.read_window(self.window, |cx| cx.read_global(read))
+ }
}
@@ -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> = U;
@@ -149,4 +150,11 @@ impl<'a, T: 'static> Context for ModelContext<'a, T> {
) -> R {
self.app.update_entity(handle, update)
}
+
+ fn read_global<G: 'static + Send + Sync, R>(
+ &self,
+ read: impl FnOnce(&G, &Self::BorrowedContext<'_, '_>) -> R,
+ ) -> R {
+ read(self.app.global(), self)
+ }
}
@@ -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<T> Task<T> {
}
}
+impl<E, T> Task<Result<T, E>>
+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<T> Future for Task<T> {
type Output = T;
@@ -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<dyn Any + Send + Sync>;
pub trait Context {
+ type BorrowedContext<'a, 'w>: Context;
type EntityContext<'a, 'w, T: 'static + Send + Sync>;
type Result<T>;
@@ -78,6 +80,11 @@ pub trait Context {
handle: &Handle<T>,
update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
) -> Self::Result<R>;
+
+ fn read_global<G: 'static + Send + Sync, R>(
+ &self,
+ read: impl FnOnce(&G, &Self::BorrowedContext<'_, '_>) -> R,
+ ) -> Self::Result<R>;
}
pub enum GlobalKey {
@@ -104,6 +111,7 @@ impl<T> DerefMut for MainThread<T> {
}
impl<C: Context> Context for MainThread<C> {
+ type BorrowedContext<'a, 'w> = MainThread<C::BorrowedContext<'a, 'w>>;
type EntityContext<'a, 'w, T: 'static + Send + Sync> = MainThread<C::EntityContext<'a, 'w, T>>;
type Result<T> = C::Result<T>;
@@ -137,6 +145,21 @@ impl<C: Context> Context for MainThread<C> {
update(entity, cx)
})
}
+
+ fn read_global<G: 'static + Send + Sync, R>(
+ &self,
+ read: impl FnOnce(&G, &Self::BorrowedContext<'_, '_>) -> R,
+ ) -> Self::Result<R> {
+ self.0.read_global(|global, cx| {
+ let cx = unsafe {
+ mem::transmute::<
+ &C::BorrowedContext<'_, '_>,
+ &MainThread<C::BorrowedContext<'_, '_>>,
+ >(cx)
+ };
+ read(global, cx)
+ })
+ }
}
pub trait BorrowAppContext {
@@ -152,15 +175,19 @@ pub trait BorrowAppContext {
result
}
- fn with_global<T: Send + Sync + 'static, F, R>(&mut self, state: T, f: F) -> R
+ fn with_global<T: Send + Sync + 'static, F, R>(&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::<T>();
result
}
+
+ fn set_global<T: Send + Sync + 'static>(&mut self, global: T) {
+ self.app_mut().set_global(global)
+ }
}
pub trait EventEmitter {
@@ -198,6 +225,12 @@ impl AsRef<str> for SharedString {
}
}
+impl Borrow<str> 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)
@@ -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<WindowContext<'a, 'w>> {
}
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> = T;
@@ -1078,6 +1086,10 @@ impl Context for WindowContext<'_, '_> {
self.entities.end_lease(entity);
result
}
+
+ fn read_global<G: 'static + Send + Sync, R>(&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> = U;
@@ -1531,13 +1547,20 @@ impl<'a, 'w, S> Context for ViewContext<'a, 'w, S> {
self.window_cx.entity(build_entity)
}
- fn update_entity<U: Send + Sync + 'static, R>(
+ fn update_entity<U: 'static + Send + Sync, R>(
&mut self,
handle: &Handle<U>,
update: impl FnOnce(&mut U, &mut Self::EntityContext<'_, '_, U>) -> R,
) -> R {
self.window_cx.update_entity(handle, update)
}
+
+ fn read_global<G: 'static + Send + Sync, R>(
+ &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> {
@@ -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
@@ -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<KeymapBlock>);
+
+#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
+pub struct KeymapBlock {
+ #[serde(default)]
+ context: Option<String>,
+ bindings: BTreeMap<String, KeymapAction>,
+}
+
+#[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<str>, Value);
+
+impl KeymapFile {
+ pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
+ let content = asset_str::<SettingsAssets>(asset_path);
+
+ Self::parse(content.as_ref())?.add_to_cx(cx)
+ }
+
+ pub fn parse(content: &str) -> Result<Self> {
+ parse_json_with_comments::<Self>(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::<Result<Vec<_>>>()?;
+
+ 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::<KeymapFile>();
+
+ 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<dyn gpui2::Action> {
+ 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();
+ }
+}
@@ -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::<SettingsAssets>("settings/default.json")
+}
+
+pub fn default_keymap() -> Cow<'static, str> {
+ asset_str::<SettingsAssets>("keymaps/default.json")
+}
+
+pub fn vim_keymap() -> Cow<'static, str> {
+ asset_str::<SettingsAssets>("keymaps/vim.json")
+}
+
+pub fn initial_user_settings_content() -> Cow<'static, str> {
+ asset_str::<SettingsAssets>("settings/initial_user_settings.json")
+}
+
+pub fn initial_local_settings_content() -> Cow<'static, str> {
+ asset_str::<SettingsAssets>("settings/initial_local_settings.json")
+}
@@ -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<T: Setting>(cx: &mut AppContext) {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.register_setting::<T>(cx);
+ });
+}
+
+pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T {
+ cx.global::<SettingsStore>().get(None)
+}
+
+pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a T {
+ cx.global::<SettingsStore>().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::<serde_json::Value>(
+ 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<Background>,
+ fs: Arc<dyn Fs>,
+ path: PathBuf,
+) -> mpsc::UnboundedReceiver<String> {
+ 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<String>,
+ 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<dyn Fs>) -> Result<String> {
+ match fs.load(&paths::SETTINGS).await {
+ result @ Ok(_) => result,
+ Err(err) => {
+ if let Some(e) = err.downcast_ref::<std::io::Error>() {
+ if e.kind() == ErrorKind::NotFound {
+ return Ok(crate::initial_user_settings_content().to_string());
+ }
+ }
+ return Err(err);
+ }
+ }
+}
+
+pub fn update_settings_file<T: Setting>(
+ fs: Arc<dyn Fs>,
+ 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::<T>(old_text, update)
+ });
+ fs.atomic_write(paths::SETTINGS.clone(), new_text).await?;
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+}
@@ -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<Self>
+ where
+ Self: Sized;
+
+ fn json_schema(
+ generator: &mut SchemaGenerator,
+ _: &SettingsJsonSchemaParams,
+ _: &AppContext,
+ ) -> RootSchema {
+ generator.root_schema_for::<Self::FileContent>()
+ }
+
+ fn json_merge(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ ) -> Result<Self::FileContent> {
+ 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<Self>
+ 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<TypeId, Box<dyn AnySettingValue>>,
+ raw_default_settings: serde_json::Value,
+ raw_user_settings: serde_json::Value,
+ raw_local_settings: BTreeMap<(usize, Arc<Path>), serde_json::Value>,
+ tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>,
+}
+
+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<T> {
+ global_value: Option<T>,
+ local_values: Vec<(usize, Arc<Path>, 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<DeserializedSetting>;
+ fn load_setting(
+ &self,
+ default_value: &DeserializedSetting,
+ custom: &[DeserializedSetting],
+ cx: &AppContext,
+ ) -> Result<Box<dyn Any>>;
+ fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any;
+ fn set_global_value(&mut self, value: Box<dyn Any>);
+ fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>);
+ fn json_schema(
+ &self,
+ generator: &mut SchemaGenerator,
+ _: &SettingsJsonSchemaParams,
+ cx: &AppContext,
+ ) -> RootSchema;
+}
+
+struct DeserializedSetting(Box<dyn Any>);
+
+impl SettingsStore {
+ /// Add a new type of setting to the store.
+ pub fn register_setting<T: Setting>(&mut self, cx: &AppContext) {
+ let setting_type_id = TypeId::of::<T>();
+ 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::<T> {
+ 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<T: Setting>(&self, path: Option<(usize, &Path)>) -> &T {
+ self.setting_values
+ .get(&TypeId::of::<T>())
+ .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
+ .value_for_path(path)
+ .downcast_ref::<T>()
+ .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<T: Setting>(&mut self, value: T) {
+ self.setting_values
+ .get_mut(&TypeId::of::<T>())
+ .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
+ .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<T: Setting>(
+ &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::<T>(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<T: Setting>(
+ &self,
+ old_text: String,
+ update: impl FnOnce(&mut T::FileContent),
+ ) -> String {
+ let edits = self.edits_for_update::<T>(&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<T: Setting>(
+ &self,
+ text: &str,
+ update: impl FnOnce(&mut T::FileContent),
+ ) -> Vec<(Range<usize>, String)> {
+ let setting_type_id = TypeId::of::<T>();
+
+ let setting = self
+ .setting_values
+ .get(&setting_type_id)
+ .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()));
+ let raw_settings = parse_json_with_comments::<serde_json::Value>(text).unwrap_or_default();
+ let old_content = match setting.deserialize_setting(&raw_settings) {
+ Ok(content) => content.0.downcast::<T::FileContent>().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<T: Setting>(
+ &mut self,
+ get_tab_size: fn(&T) -> Option<usize>,
+ ) {
+ self.tab_size_callback = Some((
+ TypeId::of::<T>(),
+ Box::new(move |value| get_tab_size(value.downcast_ref::<T>().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<Path>,
+ 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<Item = (Arc<Path>, 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<T>(target: &mut Option<T>, source: Option<T>) {
+ 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::<DeserializedSetting>::new();
+ let mut paths_stack = Vec::<Option<(usize, &Path)>>::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::<Vec<_>>(),
+ )
+ .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<T: Setting> AnySettingValue for SettingValue<T> {
+ fn key(&self) -> Option<&'static str> {
+ T::KEY
+ }
+
+ fn setting_type_name(&self) -> &'static str {
+ type_name::<T>()
+ }
+
+ fn load_setting(
+ &self,
+ default_value: &DeserializedSetting,
+ user_values: &[DeserializedSetting],
+ cx: &AppContext,
+ ) -> Result<Box<dyn Any>> {
+ let default_value = default_value.0.downcast_ref::<T::FileContent>().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<DeserializedSetting> {
+ 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<dyn Any>) {
+ self.global_value = Some(*value.downcast().unwrap());
+ }
+
+ fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>) {
+ 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<usize>, 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<usize>, 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<T: DeserializeOwned>(content: &str) -> Result<T> {
+ 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::<UserSettings>(cx);
+// store.register_setting::<TurboSetting>(cx);
+// store.register_setting::<MultiKeySettings>(cx);
+// store
+// .set_default_settings(
+// r#"{
+// "turbo": false,
+// "user": {
+// "name": "John Doe",
+// "age": 30,
+// "staff": false
+// }
+// }"#,
+// cx,
+// )
+// .unwrap();
+
+// assert_eq!(store.get::<TurboSetting>(None), &TurboSetting(false));
+// assert_eq!(
+// store.get::<UserSettings>(None),
+// &UserSettings {
+// name: "John Doe".to_string(),
+// age: 30,
+// staff: false,
+// }
+// );
+// assert_eq!(
+// store.get::<MultiKeySettings>(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::<TurboSetting>(None), &TurboSetting(true));
+// assert_eq!(
+// store.get::<UserSettings>(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::<UserSettings>(Some((1, Path::new("/root1/something")))),
+// &UserSettings {
+// name: "John Doe".to_string(),
+// age: 31,
+// staff: true
+// }
+// );
+// assert_eq!(
+// store.get::<UserSettings>(Some((1, Path::new("/root1/subdir/something")))),
+// &UserSettings {
+// name: "Jane Doe".to_string(),
+// age: 31,
+// staff: true
+// }
+// );
+// assert_eq!(
+// store.get::<UserSettings>(Some((1, Path::new("/root2/something")))),
+// &UserSettings {
+// name: "John Doe".to_string(),
+// age: 42,
+// staff: false
+// }
+// );
+// assert_eq!(
+// store.get::<MultiKeySettings>(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::<UserSettings>(cx);
+// store.register_setting::<TurboSetting>(cx);
+
+// assert_eq!(store.get::<TurboSetting>(None), &TurboSetting(false));
+// assert_eq!(
+// store.get::<UserSettings>(None),
+// &UserSettings {
+// name: "John Doe".to_string(),
+// age: 30,
+// staff: false,
+// }
+// );
+
+// store.register_setting::<MultiKeySettings>(cx);
+// assert_eq!(
+// store.get::<MultiKeySettings>(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::<MultiKeySettings>(cx);
+// store.register_setting::<UserSettings>(cx);
+// store.register_setting::<LanguageSettings>(cx);
+
+// // entries added and updated
+// check_settings_update::<LanguageSettings>(
+// &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::<UserSettings>(
+// &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::<MultiKeySettings>(
+// &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::<UserSettings>(
+// &mut store,
+// r#"{
+// "user": {}
+// }"#
+// .unindent(),
+// |settings| settings.age = Some(37),
+// r#"{
+// "user": {
+// "age": 37
+// }
+// }"#
+// .unindent(),
+// cx,
+// );
+
+// // no content
+// check_settings_update::<UserSettings>(
+// &mut store,
+// r#""#.unindent(),
+// |settings| settings.age = Some(37),
+// r#"{
+// "user": {
+// "age": 37
+// }
+// }
+// "#
+// .unindent(),
+// cx,
+// );
+
+// check_settings_update::<UserSettings>(
+// &mut store,
+// r#"{
+// }
+// "#
+// .unindent(),
+// |settings| settings.age = Some(37),
+// r#"{
+// "user": {
+// "age": 37
+// }
+// }
+// "#
+// .unindent(),
+// cx,
+// );
+// }
+
+// fn check_settings_update<T: Setting>(
+// 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::<T>(&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<String>,
+// age: Option<u32>,
+// staff: Option<bool>,
+// }
+
+// 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> {
+// 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<bool>;
+
+// fn load(
+// default_value: &Option<bool>,
+// user_values: &[&Option<bool>],
+// _: &AppContext,
+// ) -> Result<Self> {
+// 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<String>,
+// key2: Option<String>,
+// }
+
+// impl Setting for MultiKeySettings {
+// const KEY: Option<&'static str> = None;
+
+// type FileContent = MultiKeySettingsJson;
+
+// fn load(
+// default_value: &MultiKeySettingsJson,
+// user_values: &[&MultiKeySettingsJson],
+// _: &AppContext,
+// ) -> Result<Self> {
+// 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<String>,
+// pub hour_format: Option<HourFormat>,
+// }
+
+// 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> {
+// Self::load_via_json_merge(default_value, user_values)
+// }
+// }
+
+// #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+// struct LanguageSettings {
+// #[serde(default)]
+// languages: HashMap<String, LanguageSettingEntry>,
+// }
+
+// #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+// struct LanguageSettingEntry {
+// language_setting_1: Option<bool>,
+// language_setting_2: Option<bool>,
+// }
+
+// impl Setting for LanguageSettings {
+// const KEY: Option<&'static str> = None;
+
+// type FileContent = Self;
+
+// fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Result<Self> {
+// Self::load_via_json_merge(default_value, user_values)
+// }
+// }
+// }
@@ -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"] }
@@ -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::<ActionA>();
+ cx.register_action_type::<ActionB>();
let theme = rose_pine();
let color_1 = theme.lowest.negative.default.foreground;
@@ -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"]
@@ -0,0 +1,62 @@
+<key>CFBundleDocumentTypes</key>
+<array>
+ <dict>
+ <key>CFBundleTypeIconFile</key>
+ <string>Document</string>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ <key>LSHandlerRank</key>
+ <string>Alternate</string>
+ <key>LSItemContentTypes</key>
+ <array>
+ <string>public.text</string>
+ <string>public.plain-text</string>
+ <string>public.utf8-plain-text</string>
+ </array>
+ </dict>
+ <dict>
+ <key>CFBundleTypeIconFile</key>
+ <string>Document</string>
+ <key>CFBundleTypeName</key>
+ <string>Zed Text Document</string>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ <key>CFBundleTypeOSTypes</key>
+ <array>
+ <string>****</string>
+ </array>
+ <key>LSHandlerRank</key>
+ <string>Default</string>
+ <key>CFBundleTypeExtensions</key>
+ <array>
+ <string>Gemfile</string>
+ <string>c</string>
+ <string>c++</string>
+ <string>cc</string>
+ <string>cpp</string>
+ <string>css</string>
+ <string>erb</string>
+ <string>ex</string>
+ <string>exs</string>
+ <string>go</string>
+ <string>h</string>
+ <string>h++</string>
+ <string>hh</string>
+ <string>hpp</string>
+ <string>html</string>
+ <string>js</string>
+ <string>json</string>
+ <string>jsx</string>
+ <string>md</string>
+ <string>py</string>
+ <string>rb</string>
+ <string>rkt</string>
+ <string>rs</string>
+ <string>scm</string>
+ <string>toml</string>
+ <string>ts</string>
+ <string>tsx</string>
+ <string>txt</string>
+ </array>
+ </dict>
+</array>
@@ -0,0 +1,24 @@
+<key>NSSystemAdministrationUsageDescription</key>
+<string>The operation being performed by a program in Zed requires elevated permission.</string>
+<key>NSAppleEventsUsageDescription</key>
+<string>An application in Zed wants to use AppleScript.</string>
+<key>NSBluetoothAlwaysUsageDescription</key>
+<string>An application in Zed wants to use Bluetooth.</string>
+<key>NSCalendarsUsageDescription</key>
+<string>An application in Zed wants to use Calendar data.</string>
+<key>NSCameraUsageDescription</key>
+<string>An application in Zed wants to use the camera.</string>
+<key>NSContactsUsageDescription</key>
+<string>An application in Zed wants to use your contacts.</string>
+<key>NSLocationAlwaysUsageDescription</key>
+<string>An application in Zed wants to use your location information, even in the background.</string>
+<key>NSLocationUsageDescription</key>
+<string>An application in Zed wants to use your location information.</string>
+<key>NSLocationWhenInUseUsageDescription</key>
+<string>An application in Zed wants to use your location information while active.</string>
+<key>NSMicrophoneUsageDescription</key>
+<string>An application in Zed wants to use your microphone.</string>
+<key>NSSpeechRecognitionUsageDescription</key>
+<string>An application in Zed wants to use speech recognition.</string>
+<key>NSRemindersUsageDescription</key>
+<string>An application in Zed wants to use your reminders.</string>
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>com.apple.security.automation.apple-events</key>
+ <true/>
+ <key>com.apple.security.cs.allow-jit</key>
+ <true/>
+ <key>com.apple.security.device.audio-input</key>
+ <true/>
+ <key>com.apple.security.device.camera</key>
+ <true/>
+ <key>com.apple.security.personal-information.addressbook</key>
+ <true/>
+ <key>com.apple.security.personal-information.calendars</key>
+ <true/>
+ <key>com.apple.security.personal-information.location</key>
+ <true/>
+ <key>com.apple.security.personal-information.photos-library</key>
+ <true/>
+ <!-- <key>com.apple.security.cs.disable-library-validation</key>
+ <true/> -->
+</dict>
+</plist>
@@ -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<std::borrow::Cow<[u8]>> {
+ Self::get(path)
+ .map(|f| f.data)
+ .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
+ }
+
+ fn list(&self, path: &SharedString) -> Result<Vec<SharedString>> {
+ Ok(Self::iter()
+ .filter_map(|p| {
+ if p.starts_with(path.as_ref()) {
+ Some(p.into())
+ } else {
+ None
+ }
+ })
+ .collect())
+ }
+}
@@ -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::<Weak<AppState>>() {
+ if let Some(app_state) = cx.global::<Weak<AppState>>().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::<SettingsStore, _>({
+ // 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<Client>, 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<String> {
+// 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<AppState>, 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<LocationData>,
+ backtrace: Vec<String>,
+ app_version: String,
+ release_channel: String,
+ os_name: String,
+ os_version: Option<String>,
+ architecture: String,
+ panicked_on: u128,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ installation_id: Option<String>,
+ 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<String>, 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("<unnamed>");
+
+// let payload = info
+// .payload()
+// .downcast_ref::<&str>()
+// .map(|s| s.to_string())
+// .or_else(|| info.payload().downcast_ref::<String>().map(|s| s.clone()))
+// .unwrap_or_else(|| "Box<Any>".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::<Vec<_>>();
+
+// // 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<dyn HttpClient>, cx: &mut AppContext) {
+// let telemetry_settings = *settings::get::<TelemetrySettings>(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<String> {
+ 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<dyn Fs>, 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<dyn Fs>, languages: Arc<LanguageRegistry>) -> 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<dyn Fs>, 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<dyn Fs>, _cx: AsyncAppContext) -> Option<()> {
+// None
+// }
+
+// #[cfg(not(debug_assertions))]
+// async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
+// None
+// }
+
+// #[cfg(not(debug_assertions))]
+// fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
+
+fn connect_to_cli(
+ server_name: &str,
+) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
+ let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
+ .context("error connecting to cli")?;
+ let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
+ let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
+
+ 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::<CliRequest>(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<CliRequest>, IpcSender<CliResponse>),
+ app_state: Arc<AppState>,
+ 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::<Editor>() {
+ // 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),
+// ]
+// }
@@ -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,
+ }
+}
@@ -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<PathBuf>,
+ },
+ CliConnection {
+ connection: (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
+ },
+ JoinChannel {
+ channel_id: u64,
+ },
+}
+
+pub struct OpenListener {
+ tx: UnboundedSender<OpenRequest>,
+ pub triggered: AtomicBool,
+}
+
+impl OpenListener {
+ pub fn new() -> (Self, UnboundedReceiver<OpenRequest>) {
+ let (tx, rx) = mpsc::unbounded();
+ (
+ OpenListener {
+ tx,
+ triggered: AtomicBool::new(false),
+ },
+ rx,
+ )
+ }
+
+ pub fn open_urls(&self, urls: Vec<String>) {
+ 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<OpenRequest> {
+ 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<OpenRequest> {
+ 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::<u64>() {
+ return Some(OpenRequest::JoinChannel { channel_id });
+ }
+ }
+ }
+ }
+ log::error!("invalid zed url: {}", request_path);
+ None
+ }
+
+ fn handle_file_urls(&self, urls: Vec<String>) -> Option<OpenRequest> {
+ 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 })
+ }
+}
@@ -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<CliRequest>, IpcSender<CliResponse>)> {
+ let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
+ .context("error connecting to cli")?;
+ let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
+ let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
+
+ 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::<CliRequest>(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<CliRequest>, IpcSender<CliResponse>),
+ app_state: Arc<AppState>,
+ 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::<Editor>() {
+ // 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();
+ }
+ }
+ }
+}