This commit is contained in:
Antonio Scandurra 2023-10-21 17:52:47 +02:00
parent e4fe9538d7
commit b7d30fca2b
30 changed files with 3644 additions and 29 deletions

118
Cargo.lock generated
View File

@ -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"

View File

@ -81,6 +81,7 @@ members = [
"crates/welcome",
"crates/xtask",
"crates/zed",
"crates/zed2",
"crates/zed-actions"
]
default-members = ["crates/zed"]

View File

@ -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"

View File

@ -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),

View File

@ -9,10 +9,10 @@ use refineable::Refineable;
use smallvec::SmallVec;
use crate::{
current_platform, image_cache::ImageCache, AssetSource, Context, DisplayId, Executor,
current_platform, image_cache::ImageCache, Action, AssetSource, Context, DisplayId, Executor,
FocusEvent, FocusHandle, FocusId, KeyBinding, Keymap, LayoutId, MainThread, MainThreadOnly,
Platform, SubscriberSet, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View,
Window, WindowContext, WindowHandle, WindowId,
Platform, SharedString, SubscriberSet, SvgRenderer, Task, TextStyle, TextStyleRefinement,
TextSystem, View, Window, WindowContext, WindowHandle, WindowId,
};
use anyhow::{anyhow, Result};
use collections::{HashMap, HashSet, VecDeque};
@ -55,10 +55,10 @@ impl App {
Mutex::new(AppContext {
this: this.clone(),
text_system: Arc::new(TextSystem::new(platform.text_system())),
pending_updates: 0,
flushing_effects: false,
next_frame_callbacks: Default::default(),
platform: MainThreadOnly::new(platform, executor.clone()),
flushing_effects: false,
pending_updates: 0,
next_frame_callbacks: Default::default(),
executor,
svg_renderer: SvgRenderer::new(asset_source),
image_cache: ImageCache::new(http_client),
@ -68,6 +68,7 @@ impl App {
entities,
windows: SlotMap::with_key(),
keymap: Arc::new(RwLock::new(Keymap::default())),
action_builders: HashMap::default(),
pending_notifications: Default::default(),
pending_effects: Default::default(),
observers: SubscriberSet::new(),
@ -90,12 +91,17 @@ impl App {
on_finish_launching(cx);
}));
}
pub fn executor(&self) -> Executor {
self.0.lock().executor.clone()
}
}
type Handler = Box<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> {

View File

@ -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,
) -> Self::Result<R> {
let app = self
.0
.upgrade()
.ok_or_else(|| anyhow!("app was released"))?;
let mut lock = app.lock();
Ok(lock.update_entity(handle, update))
}
fn read_global<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 lock = app.lock(); // Does not compile without this variable.
Ok(lock.update_entity(handle, update))
let mut app_context = app.lock();
app_context.read_window(handle.id, update)
}
}
impl AsyncAppContext {
pub fn update_window<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))
}
}

View File

@ -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)
}
}

View File

@ -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;

View File

@ -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)

View File

@ -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> {

View File

@ -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

View File

@ -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();
}
}

View File

@ -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")
}

View File

@ -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);
}

File diff suppressed because it is too large Load Diff

View File

@ -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"] }

View File

@ -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;

180
crates/zed2/Cargo.toml Normal file
View File

@ -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"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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>

33
crates/zed2/src/assets.rs Normal file
View File

@ -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())
}
}

912
crates/zed2/src/main.rs Normal file
View File

@ -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),
// ]
// }

View File

@ -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,
}
}

View File

@ -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 })
}
}

203
crates/zed2/src/zed2.rs Normal file
View File

@ -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();
}
}
}
}