Merge branch 'zed2' of github.com:zed-industries/zed into zed2

Marshall Bowers created

Change summary

Cargo.lock                                   |   33 
Cargo.toml                                   |    1 
crates/client2/Cargo.toml                    |    2 
crates/fs2/Cargo.toml                        |   41 
crates/fs2/src/fs2.rs                        | 1283 ++++++++++++++++++++++
crates/fs2/src/repository.rs                 |  417 +++++++
crates/gpui2/src/app.rs                      |  127 +-
crates/gpui2/src/app/entity_map.rs           |    9 
crates/gpui2/src/app/test_context.rs         |   43 
crates/gpui2/src/executor.rs                 |    4 
crates/gpui2/src/platform.rs                 |    5 
crates/gpui2/src/platform/mac/dispatcher.rs  |    5 
crates/gpui2/src/platform/test/dispatcher.rs |   35 
crates/gpui2/src/platform/test/platform.rs   |   36 
crates/gpui2/src/text_system/line_wrapper.rs |    8 
crates/settings2/Cargo.toml                  |    2 
16 files changed, 1,931 insertions(+), 120 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1471,7 +1471,7 @@ dependencies = [
  "async-recursion 0.3.2",
  "async-tungstenite",
  "collections",
- "db",
+ "db2",
  "feature_flags2",
  "futures 0.3.28",
  "gpui2",
@@ -2991,6 +2991,35 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "fs2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "collections",
+ "fsevent",
+ "futures 0.3.28",
+ "git2",
+ "gpui2",
+ "lazy_static",
+ "libc",
+ "log",
+ "parking_lot 0.11.2",
+ "rand 0.8.5",
+ "regex",
+ "rope",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "smol",
+ "sum_tree",
+ "tempfile",
+ "text",
+ "time",
+ "util",
+]
+
 [[package]]
 name = "fsevent"
 version = "2.0.2"
@@ -7485,7 +7514,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "collections",
- "feature_flags",
+ "feature_flags2",
  "fs",
  "futures 0.3.28",
  "gpui2",

Cargo.toml 🔗

@@ -34,6 +34,7 @@ members = [
     "crates/feedback",
     "crates/file_finder",
     "crates/fs",
+    "crates/fs2",
     "crates/fsevent",
     "crates/fuzzy",
     "crates/fuzzy2",

crates/client2/Cargo.toml 🔗

@@ -13,7 +13,7 @@ test-support = ["collections/test-support", "gpui2/test-support", "rpc/test-supp
 
 [dependencies]
 collections = { path = "../collections" }
-db = { path = "../db" }
+db2 = { path = "../db2" }
 gpui2 = { path = "../gpui2" }
 util = { path = "../util" }
 rpc = { path = "../rpc" }

crates/fs2/Cargo.toml 🔗

@@ -0,0 +1,41 @@
+[package]
+name = "fs2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/fs2.rs"
+
+[dependencies]
+collections = { path = "../collections" }
+rope = { path = "../rope" }
+text = { path = "../text" }
+util = { path = "../util" }
+sum_tree = { path = "../sum_tree" }
+
+anyhow.workspace = true
+async-trait.workspace = true
+futures.workspace = true
+tempfile = "3"
+fsevent = { path = "../fsevent" }
+lazy_static.workspace = true
+parking_lot.workspace = true
+smol.workspace = true
+regex.workspace = true
+git2.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+log.workspace = true
+libc = "0.2"
+time.workspace = true
+
+gpui2 = { path = "../gpui2", optional = true}
+
+[dev-dependencies]
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+rand.workspace = true
+
+[features]
+test-support = ["gpui2/test-support"]

crates/fs2/src/fs2.rs 🔗

@@ -0,0 +1,1283 @@
+pub mod repository;
+
+use anyhow::{anyhow, Result};
+use fsevent::EventStream;
+use futures::{future::BoxFuture, Stream, StreamExt};
+use git2::Repository as LibGitRepository;
+use parking_lot::Mutex;
+use repository::GitRepository;
+use rope::Rope;
+use smol::io::{AsyncReadExt, AsyncWriteExt};
+use std::io::Write;
+use std::sync::Arc;
+use std::{
+    io,
+    os::unix::fs::MetadataExt,
+    path::{Component, Path, PathBuf},
+    pin::Pin,
+    time::{Duration, SystemTime},
+};
+use tempfile::NamedTempFile;
+use text::LineEnding;
+use util::ResultExt;
+
+#[cfg(any(test, feature = "test-support"))]
+use collections::{btree_map, BTreeMap};
+#[cfg(any(test, feature = "test-support"))]
+use repository::{FakeGitRepositoryState, GitFileStatus};
+#[cfg(any(test, feature = "test-support"))]
+use std::ffi::OsStr;
+
+#[async_trait::async_trait]
+pub trait Fs: Send + Sync {
+    async fn create_dir(&self, path: &Path) -> Result<()>;
+    async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
+    async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
+    async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
+    async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
+    async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
+    async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
+    async fn load(&self, path: &Path) -> Result<String>;
+    async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
+    async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
+    async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
+    async fn is_file(&self, path: &Path) -> bool;
+    async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
+    async fn read_link(&self, path: &Path) -> Result<PathBuf>;
+    async fn read_dir(
+        &self,
+        path: &Path,
+    ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>;
+    async fn watch(
+        &self,
+        path: &Path,
+        latency: Duration,
+    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
+    fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>>;
+    fn is_fake(&self) -> bool;
+    #[cfg(any(test, feature = "test-support"))]
+    fn as_fake(&self) -> &FakeFs;
+}
+
+#[derive(Copy, Clone, Default)]
+pub struct CreateOptions {
+    pub overwrite: bool,
+    pub ignore_if_exists: bool,
+}
+
+#[derive(Copy, Clone, Default)]
+pub struct CopyOptions {
+    pub overwrite: bool,
+    pub ignore_if_exists: bool,
+}
+
+#[derive(Copy, Clone, Default)]
+pub struct RenameOptions {
+    pub overwrite: bool,
+    pub ignore_if_exists: bool,
+}
+
+#[derive(Copy, Clone, Default)]
+pub struct RemoveOptions {
+    pub recursive: bool,
+    pub ignore_if_not_exists: bool,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct Metadata {
+    pub inode: u64,
+    pub mtime: SystemTime,
+    pub is_symlink: bool,
+    pub is_dir: bool,
+}
+
+pub struct RealFs;
+
+#[async_trait::async_trait]
+impl Fs for RealFs {
+    async fn create_dir(&self, path: &Path) -> Result<()> {
+        Ok(smol::fs::create_dir_all(path).await?)
+    }
+
+    async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
+        let mut open_options = smol::fs::OpenOptions::new();
+        open_options.write(true).create(true);
+        if options.overwrite {
+            open_options.truncate(true);
+        } else if !options.ignore_if_exists {
+            open_options.create_new(true);
+        }
+        open_options.open(path).await?;
+        Ok(())
+    }
+
+    async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
+        if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
+            if options.ignore_if_exists {
+                return Ok(());
+            } else {
+                return Err(anyhow!("{target:?} already exists"));
+            }
+        }
+
+        smol::fs::copy(source, target).await?;
+        Ok(())
+    }
+
+    async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
+        if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
+            if options.ignore_if_exists {
+                return Ok(());
+            } else {
+                return Err(anyhow!("{target:?} already exists"));
+            }
+        }
+
+        smol::fs::rename(source, target).await?;
+        Ok(())
+    }
+
+    async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
+        let result = if options.recursive {
+            smol::fs::remove_dir_all(path).await
+        } else {
+            smol::fs::remove_dir(path).await
+        };
+        match result {
+            Ok(()) => Ok(()),
+            Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
+                Ok(())
+            }
+            Err(err) => Err(err)?,
+        }
+    }
+
+    async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
+        match smol::fs::remove_file(path).await {
+            Ok(()) => Ok(()),
+            Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
+                Ok(())
+            }
+            Err(err) => Err(err)?,
+        }
+    }
+
+    async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
+        Ok(Box::new(std::fs::File::open(path)?))
+    }
+
+    async fn load(&self, path: &Path) -> Result<String> {
+        let mut file = smol::fs::File::open(path).await?;
+        let mut text = String::new();
+        file.read_to_string(&mut text).await?;
+        Ok(text)
+    }
+
+    async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
+        smol::unblock(move || {
+            let mut tmp_file = NamedTempFile::new()?;
+            tmp_file.write_all(data.as_bytes())?;
+            tmp_file.persist(path)?;
+            Ok::<(), anyhow::Error>(())
+        })
+        .await?;
+
+        Ok(())
+    }
+
+    async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
+        let buffer_size = text.summary().len.min(10 * 1024);
+        if let Some(path) = path.parent() {
+            self.create_dir(path).await?;
+        }
+        let file = smol::fs::File::create(path).await?;
+        let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
+        for chunk in chunks(text, line_ending) {
+            writer.write_all(chunk.as_bytes()).await?;
+        }
+        writer.flush().await?;
+        Ok(())
+    }
+
+    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
+        Ok(smol::fs::canonicalize(path).await?)
+    }
+
+    async fn is_file(&self, path: &Path) -> bool {
+        smol::fs::metadata(path)
+            .await
+            .map_or(false, |metadata| metadata.is_file())
+    }
+
+    async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
+        let symlink_metadata = match smol::fs::symlink_metadata(path).await {
+            Ok(metadata) => metadata,
+            Err(err) => {
+                return match (err.kind(), err.raw_os_error()) {
+                    (io::ErrorKind::NotFound, _) => Ok(None),
+                    (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
+                    _ => Err(anyhow::Error::new(err)),
+                }
+            }
+        };
+
+        let is_symlink = symlink_metadata.file_type().is_symlink();
+        let metadata = if is_symlink {
+            smol::fs::metadata(path).await?
+        } else {
+            symlink_metadata
+        };
+        Ok(Some(Metadata {
+            inode: metadata.ino(),
+            mtime: metadata.modified().unwrap(),
+            is_symlink,
+            is_dir: metadata.file_type().is_dir(),
+        }))
+    }
+
+    async fn read_link(&self, path: &Path) -> Result<PathBuf> {
+        let path = smol::fs::read_link(path).await?;
+        Ok(path)
+    }
+
+    async fn read_dir(
+        &self,
+        path: &Path,
+    ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
+        let result = smol::fs::read_dir(path).await?.map(|entry| match entry {
+            Ok(entry) => Ok(entry.path()),
+            Err(error) => Err(anyhow!("failed to read dir entry {:?}", error)),
+        });
+        Ok(Box::pin(result))
+    }
+
+    async fn watch(
+        &self,
+        path: &Path,
+        latency: Duration,
+    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
+        let (tx, rx) = smol::channel::unbounded();
+        let (stream, handle) = EventStream::new(&[path], latency);
+        std::thread::spawn(move || {
+            stream.run(move |events| smol::block_on(tx.send(events)).is_ok());
+        });
+        Box::pin(rx.chain(futures::stream::once(async move {
+            drop(handle);
+            vec![]
+        })))
+    }
+
+    fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
+        LibGitRepository::open(&dotgit_path)
+            .log_err()
+            .and_then::<Arc<Mutex<dyn GitRepository>>, _>(|libgit_repository| {
+                Some(Arc::new(Mutex::new(libgit_repository)))
+            })
+    }
+
+    fn is_fake(&self) -> bool {
+        false
+    }
+    #[cfg(any(test, feature = "test-support"))]
+    fn as_fake(&self) -> &FakeFs {
+        panic!("called `RealFs::as_fake`")
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub struct FakeFs {
+    // Use an unfair lock to ensure tests are deterministic.
+    state: Mutex<FakeFsState>,
+    executor: gpui2::Executor,
+}
+
+#[cfg(any(test, feature = "test-support"))]
+struct FakeFsState {
+    root: Arc<Mutex<FakeFsEntry>>,
+    next_inode: u64,
+    next_mtime: SystemTime,
+    event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
+    events_paused: bool,
+    buffered_events: Vec<fsevent::Event>,
+    metadata_call_count: usize,
+    read_dir_call_count: usize,
+}
+
+#[cfg(any(test, feature = "test-support"))]
+#[derive(Debug)]
+enum FakeFsEntry {
+    File {
+        inode: u64,
+        mtime: SystemTime,
+        content: String,
+    },
+    Dir {
+        inode: u64,
+        mtime: SystemTime,
+        entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
+        git_repo_state: Option<Arc<Mutex<repository::FakeGitRepositoryState>>>,
+    },
+    Symlink {
+        target: PathBuf,
+    },
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl FakeFsState {
+    fn read_path<'a>(&'a self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
+        Ok(self
+            .try_read_path(target, true)
+            .ok_or_else(|| anyhow!("path does not exist: {}", target.display()))?
+            .0)
+    }
+
+    fn try_read_path<'a>(
+        &'a self,
+        target: &Path,
+        follow_symlink: bool,
+    ) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
+        let mut path = target.to_path_buf();
+        let mut canonical_path = PathBuf::new();
+        let mut entry_stack = Vec::new();
+        'outer: loop {
+            let mut path_components = path.components().peekable();
+            while let Some(component) = path_components.next() {
+                match component {
+                    Component::Prefix(_) => panic!("prefix paths aren't supported"),
+                    Component::RootDir => {
+                        entry_stack.clear();
+                        entry_stack.push(self.root.clone());
+                        canonical_path.clear();
+                        canonical_path.push("/");
+                    }
+                    Component::CurDir => {}
+                    Component::ParentDir => {
+                        entry_stack.pop()?;
+                        canonical_path.pop();
+                    }
+                    Component::Normal(name) => {
+                        let current_entry = entry_stack.last().cloned()?;
+                        let current_entry = current_entry.lock();
+                        if let FakeFsEntry::Dir { entries, .. } = &*current_entry {
+                            let entry = entries.get(name.to_str().unwrap()).cloned()?;
+                            if path_components.peek().is_some() || follow_symlink {
+                                let entry = entry.lock();
+                                if let FakeFsEntry::Symlink { target, .. } = &*entry {
+                                    let mut target = target.clone();
+                                    target.extend(path_components);
+                                    path = target;
+                                    continue 'outer;
+                                }
+                            }
+                            entry_stack.push(entry.clone());
+                            canonical_path.push(name);
+                        } else {
+                            return None;
+                        }
+                    }
+                }
+            }
+            break;
+        }
+        Some((entry_stack.pop()?, canonical_path))
+    }
+
+    fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
+    where
+        Fn: FnOnce(btree_map::Entry<String, Arc<Mutex<FakeFsEntry>>>) -> Result<T>,
+    {
+        let path = normalize_path(path);
+        let filename = path
+            .file_name()
+            .ok_or_else(|| anyhow!("cannot overwrite the root"))?;
+        let parent_path = path.parent().unwrap();
+
+        let parent = self.read_path(parent_path)?;
+        let mut parent = parent.lock();
+        let new_entry = parent
+            .dir_entries(parent_path)?
+            .entry(filename.to_str().unwrap().into());
+        callback(new_entry)
+    }
+
+    fn emit_event<I, T>(&mut self, paths: I)
+    where
+        I: IntoIterator<Item = T>,
+        T: Into<PathBuf>,
+    {
+        self.buffered_events
+            .extend(paths.into_iter().map(|path| fsevent::Event {
+                event_id: 0,
+                flags: fsevent::StreamFlags::empty(),
+                path: path.into(),
+            }));
+
+        if !self.events_paused {
+            self.flush_events(self.buffered_events.len());
+        }
+    }
+
+    fn flush_events(&mut self, mut count: usize) {
+        count = count.min(self.buffered_events.len());
+        let events = self.buffered_events.drain(0..count).collect::<Vec<_>>();
+        self.event_txs.retain(|tx| {
+            let _ = tx.try_send(events.clone());
+            !tx.is_closed()
+        });
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+lazy_static::lazy_static! {
+    pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git");
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl FakeFs {
+    pub fn new(executor: gpui2::Executor) -> Arc<Self> {
+        Arc::new(Self {
+            executor,
+            state: Mutex::new(FakeFsState {
+                root: Arc::new(Mutex::new(FakeFsEntry::Dir {
+                    inode: 0,
+                    mtime: SystemTime::UNIX_EPOCH,
+                    entries: Default::default(),
+                    git_repo_state: None,
+                })),
+                next_mtime: SystemTime::UNIX_EPOCH,
+                next_inode: 1,
+                event_txs: Default::default(),
+                buffered_events: Vec::new(),
+                events_paused: false,
+                read_dir_call_count: 0,
+                metadata_call_count: 0,
+            }),
+        })
+    }
+
+    pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
+        self.write_file_internal(path, content).unwrap()
+    }
+
+    pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
+        let mut state = self.state.lock();
+        let path = path.as_ref();
+        let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target }));
+        state
+            .write_path(path.as_ref(), move |e| match e {
+                btree_map::Entry::Vacant(e) => {
+                    e.insert(file);
+                    Ok(())
+                }
+                btree_map::Entry::Occupied(mut e) => {
+                    *e.get_mut() = file;
+                    Ok(())
+                }
+            })
+            .unwrap();
+        state.emit_event(&[path]);
+    }
+
+    pub fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
+        let mut state = self.state.lock();
+        let path = path.as_ref();
+        let inode = state.next_inode;
+        let mtime = state.next_mtime;
+        state.next_inode += 1;
+        state.next_mtime += Duration::from_nanos(1);
+        let file = Arc::new(Mutex::new(FakeFsEntry::File {
+            inode,
+            mtime,
+            content,
+        }));
+        state.write_path(path, move |entry| {
+            match entry {
+                btree_map::Entry::Vacant(e) => {
+                    e.insert(file);
+                }
+                btree_map::Entry::Occupied(mut e) => {
+                    *e.get_mut() = file;
+                }
+            }
+            Ok(())
+        })?;
+        state.emit_event(&[path]);
+        Ok(())
+    }
+
+    pub fn pause_events(&self) {
+        self.state.lock().events_paused = true;
+    }
+
+    pub fn buffered_event_count(&self) -> usize {
+        self.state.lock().buffered_events.len()
+    }
+
+    pub fn flush_events(&self, count: usize) {
+        self.state.lock().flush_events(count);
+    }
+
+    #[must_use]
+    pub fn insert_tree<'a>(
+        &'a self,
+        path: impl 'a + AsRef<Path> + Send,
+        tree: serde_json::Value,
+    ) -> futures::future::BoxFuture<'a, ()> {
+        use futures::FutureExt as _;
+        use serde_json::Value::*;
+
+        async move {
+            let path = path.as_ref();
+
+            match tree {
+                Object(map) => {
+                    self.create_dir(path).await.unwrap();
+                    for (name, contents) in map {
+                        let mut path = PathBuf::from(path);
+                        path.push(name);
+                        self.insert_tree(&path, contents).await;
+                    }
+                }
+                Null => {
+                    self.create_dir(path).await.unwrap();
+                }
+                String(contents) => {
+                    self.insert_file(&path, contents).await;
+                }
+                _ => {
+                    panic!("JSON object must contain only objects, strings, or null");
+                }
+            }
+        }
+        .boxed()
+    }
+
+    pub fn with_git_state<F>(&self, dot_git: &Path, emit_git_event: bool, f: F)
+    where
+        F: FnOnce(&mut FakeGitRepositoryState),
+    {
+        let mut state = self.state.lock();
+        let entry = state.read_path(dot_git).unwrap();
+        let mut entry = entry.lock();
+
+        if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
+            let repo_state = git_repo_state.get_or_insert_with(Default::default);
+            let mut repo_state = repo_state.lock();
+
+            f(&mut repo_state);
+
+            if emit_git_event {
+                state.emit_event([dot_git]);
+            }
+        } else {
+            panic!("not a directory");
+        }
+    }
+
+    pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
+        self.with_git_state(dot_git, true, |state| {
+            state.branch_name = branch.map(Into::into)
+        })
+    }
+
+    pub fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
+        self.with_git_state(dot_git, true, |state| {
+            state.index_contents.clear();
+            state.index_contents.extend(
+                head_state
+                    .iter()
+                    .map(|(path, content)| (path.to_path_buf(), content.clone())),
+            );
+        });
+    }
+
+    pub fn set_status_for_repo_via_working_copy_change(
+        &self,
+        dot_git: &Path,
+        statuses: &[(&Path, GitFileStatus)],
+    ) {
+        self.with_git_state(dot_git, false, |state| {
+            state.worktree_statuses.clear();
+            state.worktree_statuses.extend(
+                statuses
+                    .iter()
+                    .map(|(path, content)| ((**path).into(), content.clone())),
+            );
+        });
+        self.state.lock().emit_event(
+            statuses
+                .iter()
+                .map(|(path, _)| dot_git.parent().unwrap().join(path)),
+        );
+    }
+
+    pub fn set_status_for_repo_via_git_operation(
+        &self,
+        dot_git: &Path,
+        statuses: &[(&Path, GitFileStatus)],
+    ) {
+        self.with_git_state(dot_git, true, |state| {
+            state.worktree_statuses.clear();
+            state.worktree_statuses.extend(
+                statuses
+                    .iter()
+                    .map(|(path, content)| ((**path).into(), content.clone())),
+            );
+        });
+    }
+
+    pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
+        let mut result = Vec::new();
+        let mut queue = collections::VecDeque::new();
+        queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
+        while let Some((path, entry)) = queue.pop_front() {
+            if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
+                for (name, entry) in entries {
+                    queue.push_back((path.join(name), entry.clone()));
+                }
+            }
+            if include_dot_git
+                || !path
+                    .components()
+                    .any(|component| component.as_os_str() == *FS_DOT_GIT)
+            {
+                result.push(path);
+            }
+        }
+        result
+    }
+
+    pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
+        let mut result = Vec::new();
+        let mut queue = collections::VecDeque::new();
+        queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
+        while let Some((path, entry)) = queue.pop_front() {
+            if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
+                for (name, entry) in entries {
+                    queue.push_back((path.join(name), entry.clone()));
+                }
+                if include_dot_git
+                    || !path
+                        .components()
+                        .any(|component| component.as_os_str() == *FS_DOT_GIT)
+                {
+                    result.push(path);
+                }
+            }
+        }
+        result
+    }
+
+    pub fn files(&self) -> Vec<PathBuf> {
+        let mut result = Vec::new();
+        let mut queue = collections::VecDeque::new();
+        queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
+        while let Some((path, entry)) = queue.pop_front() {
+            let e = entry.lock();
+            match &*e {
+                FakeFsEntry::File { .. } => result.push(path),
+                FakeFsEntry::Dir { entries, .. } => {
+                    for (name, entry) in entries {
+                        queue.push_back((path.join(name), entry.clone()));
+                    }
+                }
+                FakeFsEntry::Symlink { .. } => {}
+            }
+        }
+        result
+    }
+
+    /// How many `read_dir` calls have been issued.
+    pub fn read_dir_call_count(&self) -> usize {
+        self.state.lock().read_dir_call_count
+    }
+
+    /// How many `metadata` calls have been issued.
+    pub fn metadata_call_count(&self) -> usize {
+        self.state.lock().metadata_call_count
+    }
+
+    fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
+        self.executor.simulate_random_delay()
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl FakeFsEntry {
+    fn is_file(&self) -> bool {
+        matches!(self, Self::File { .. })
+    }
+
+    fn is_symlink(&self) -> bool {
+        matches!(self, Self::Symlink { .. })
+    }
+
+    fn file_content(&self, path: &Path) -> Result<&String> {
+        if let Self::File { content, .. } = self {
+            Ok(content)
+        } else {
+            Err(anyhow!("not a file: {}", path.display()))
+        }
+    }
+
+    fn set_file_content(&mut self, path: &Path, new_content: String) -> Result<()> {
+        if let Self::File { content, mtime, .. } = self {
+            *mtime = SystemTime::now();
+            *content = new_content;
+            Ok(())
+        } else {
+            Err(anyhow!("not a file: {}", path.display()))
+        }
+    }
+
+    fn dir_entries(
+        &mut self,
+        path: &Path,
+    ) -> Result<&mut BTreeMap<String, Arc<Mutex<FakeFsEntry>>>> {
+        if let Self::Dir { entries, .. } = self {
+            Ok(entries)
+        } else {
+            Err(anyhow!("not a directory: {}", path.display()))
+        }
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+#[async_trait::async_trait]
+impl Fs for FakeFs {
+    async fn create_dir(&self, path: &Path) -> Result<()> {
+        self.simulate_random_delay().await;
+
+        let mut created_dirs = Vec::new();
+        let mut cur_path = PathBuf::new();
+        for component in path.components() {
+            let mut state = self.state.lock();
+            cur_path.push(component);
+            if cur_path == Path::new("/") {
+                continue;
+            }
+
+            let inode = state.next_inode;
+            let mtime = state.next_mtime;
+            state.next_mtime += Duration::from_nanos(1);
+            state.next_inode += 1;
+            state.write_path(&cur_path, |entry| {
+                entry.or_insert_with(|| {
+                    created_dirs.push(cur_path.clone());
+                    Arc::new(Mutex::new(FakeFsEntry::Dir {
+                        inode,
+                        mtime,
+                        entries: Default::default(),
+                        git_repo_state: None,
+                    }))
+                });
+                Ok(())
+            })?
+        }
+
+        self.state.lock().emit_event(&created_dirs);
+        Ok(())
+    }
+
+    async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
+        self.simulate_random_delay().await;
+        let mut state = self.state.lock();
+        let inode = state.next_inode;
+        let mtime = state.next_mtime;
+        state.next_mtime += Duration::from_nanos(1);
+        state.next_inode += 1;
+        let file = Arc::new(Mutex::new(FakeFsEntry::File {
+            inode,
+            mtime,
+            content: String::new(),
+        }));
+        state.write_path(path, |entry| {
+            match entry {
+                btree_map::Entry::Occupied(mut e) => {
+                    if options.overwrite {
+                        *e.get_mut() = file;
+                    } else if !options.ignore_if_exists {
+                        return Err(anyhow!("path already exists: {}", path.display()));
+                    }
+                }
+                btree_map::Entry::Vacant(e) => {
+                    e.insert(file);
+                }
+            }
+            Ok(())
+        })?;
+        state.emit_event(&[path]);
+        Ok(())
+    }
+
+    async fn rename(&self, old_path: &Path, new_path: &Path, options: RenameOptions) -> Result<()> {
+        self.simulate_random_delay().await;
+
+        let old_path = normalize_path(old_path);
+        let new_path = normalize_path(new_path);
+
+        let mut state = self.state.lock();
+        let moved_entry = state.write_path(&old_path, |e| {
+            if let btree_map::Entry::Occupied(e) = e {
+                Ok(e.get().clone())
+            } else {
+                Err(anyhow!("path does not exist: {}", &old_path.display()))
+            }
+        })?;
+
+        state.write_path(&new_path, |e| {
+            match e {
+                btree_map::Entry::Occupied(mut e) => {
+                    if options.overwrite {
+                        *e.get_mut() = moved_entry;
+                    } else if !options.ignore_if_exists {
+                        return Err(anyhow!("path already exists: {}", new_path.display()));
+                    }
+                }
+                btree_map::Entry::Vacant(e) => {
+                    e.insert(moved_entry);
+                }
+            }
+            Ok(())
+        })?;
+
+        state
+            .write_path(&old_path, |e| {
+                if let btree_map::Entry::Occupied(e) = e {
+                    Ok(e.remove())
+                } else {
+                    unreachable!()
+                }
+            })
+            .unwrap();
+
+        state.emit_event(&[old_path, new_path]);
+        Ok(())
+    }
+
+    async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
+        self.simulate_random_delay().await;
+
+        let source = normalize_path(source);
+        let target = normalize_path(target);
+        let mut state = self.state.lock();
+        let mtime = state.next_mtime;
+        let inode = util::post_inc(&mut state.next_inode);
+        state.next_mtime += Duration::from_nanos(1);
+        let source_entry = state.read_path(&source)?;
+        let content = source_entry.lock().file_content(&source)?.clone();
+        let entry = state.write_path(&target, |e| match e {
+            btree_map::Entry::Occupied(e) => {
+                if options.overwrite {
+                    Ok(Some(e.get().clone()))
+                } else if !options.ignore_if_exists {
+                    return Err(anyhow!("{target:?} already exists"));
+                } else {
+                    Ok(None)
+                }
+            }
+            btree_map::Entry::Vacant(e) => Ok(Some(
+                e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
+                    inode,
+                    mtime,
+                    content: String::new(),
+                })))
+                .clone(),
+            )),
+        })?;
+        if let Some(entry) = entry {
+            entry.lock().set_file_content(&target, content)?;
+        }
+        state.emit_event(&[target]);
+        Ok(())
+    }
+
+    async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
+        self.simulate_random_delay().await;
+
+        let path = normalize_path(path);
+        let parent_path = path
+            .parent()
+            .ok_or_else(|| anyhow!("cannot remove the root"))?;
+        let base_name = path.file_name().unwrap();
+
+        let mut state = self.state.lock();
+        let parent_entry = state.read_path(parent_path)?;
+        let mut parent_entry = parent_entry.lock();
+        let entry = parent_entry
+            .dir_entries(parent_path)?
+            .entry(base_name.to_str().unwrap().into());
+
+        match entry {
+            btree_map::Entry::Vacant(_) => {
+                if !options.ignore_if_not_exists {
+                    return Err(anyhow!("{path:?} does not exist"));
+                }
+            }
+            btree_map::Entry::Occupied(e) => {
+                {
+                    let mut entry = e.get().lock();
+                    let children = entry.dir_entries(&path)?;
+                    if !options.recursive && !children.is_empty() {
+                        return Err(anyhow!("{path:?} is not empty"));
+                    }
+                }
+                e.remove();
+            }
+        }
+        state.emit_event(&[path]);
+        Ok(())
+    }
+
+    async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
+        self.simulate_random_delay().await;
+
+        let path = normalize_path(path);
+        let parent_path = path
+            .parent()
+            .ok_or_else(|| anyhow!("cannot remove the root"))?;
+        let base_name = path.file_name().unwrap();
+        let mut state = self.state.lock();
+        let parent_entry = state.read_path(parent_path)?;
+        let mut parent_entry = parent_entry.lock();
+        let entry = parent_entry
+            .dir_entries(parent_path)?
+            .entry(base_name.to_str().unwrap().into());
+        match entry {
+            btree_map::Entry::Vacant(_) => {
+                if !options.ignore_if_not_exists {
+                    return Err(anyhow!("{path:?} does not exist"));
+                }
+            }
+            btree_map::Entry::Occupied(e) => {
+                e.get().lock().file_content(&path)?;
+                e.remove();
+            }
+        }
+        state.emit_event(&[path]);
+        Ok(())
+    }
+
+    async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
+        let text = self.load(path).await?;
+        Ok(Box::new(io::Cursor::new(text)))
+    }
+
+    async fn load(&self, path: &Path) -> Result<String> {
+        let path = normalize_path(path);
+        self.simulate_random_delay().await;
+        let state = self.state.lock();
+        let entry = state.read_path(&path)?;
+        let entry = entry.lock();
+        entry.file_content(&path).cloned()
+    }
+
+    async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
+        self.simulate_random_delay().await;
+        let path = normalize_path(path.as_path());
+        self.write_file_internal(path, data.to_string())?;
+
+        Ok(())
+    }
+
+    async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
+        self.simulate_random_delay().await;
+        let path = normalize_path(path);
+        let content = chunks(text, line_ending).collect();
+        if let Some(path) = path.parent() {
+            self.create_dir(path).await?;
+        }
+        self.write_file_internal(path, content)?;
+        Ok(())
+    }
+
+    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
+        let path = normalize_path(path);
+        self.simulate_random_delay().await;
+        let state = self.state.lock();
+        if let Some((_, canonical_path)) = state.try_read_path(&path, true) {
+            Ok(canonical_path)
+        } else {
+            Err(anyhow!("path does not exist: {}", path.display()))
+        }
+    }
+
+    async fn is_file(&self, path: &Path) -> bool {
+        let path = normalize_path(path);
+        self.simulate_random_delay().await;
+        let state = self.state.lock();
+        if let Some((entry, _)) = state.try_read_path(&path, true) {
+            entry.lock().is_file()
+        } else {
+            false
+        }
+    }
+
+    async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
+        self.simulate_random_delay().await;
+        let path = normalize_path(path);
+        let mut state = self.state.lock();
+        state.metadata_call_count += 1;
+        if let Some((mut entry, _)) = state.try_read_path(&path, false) {
+            let is_symlink = entry.lock().is_symlink();
+            if is_symlink {
+                if let Some(e) = state.try_read_path(&path, true).map(|e| e.0) {
+                    entry = e;
+                } else {
+                    return Ok(None);
+                }
+            }
+
+            let entry = entry.lock();
+            Ok(Some(match &*entry {
+                FakeFsEntry::File { inode, mtime, .. } => Metadata {
+                    inode: *inode,
+                    mtime: *mtime,
+                    is_dir: false,
+                    is_symlink,
+                },
+                FakeFsEntry::Dir { inode, mtime, .. } => Metadata {
+                    inode: *inode,
+                    mtime: *mtime,
+                    is_dir: true,
+                    is_symlink,
+                },
+                FakeFsEntry::Symlink { .. } => unreachable!(),
+            }))
+        } else {
+            Ok(None)
+        }
+    }
+
+    async fn read_link(&self, path: &Path) -> Result<PathBuf> {
+        self.simulate_random_delay().await;
+        let path = normalize_path(path);
+        let state = self.state.lock();
+        if let Some((entry, _)) = state.try_read_path(&path, false) {
+            let entry = entry.lock();
+            if let FakeFsEntry::Symlink { target } = &*entry {
+                Ok(target.clone())
+            } else {
+                Err(anyhow!("not a symlink: {}", path.display()))
+            }
+        } else {
+            Err(anyhow!("path does not exist: {}", path.display()))
+        }
+    }
+
+    async fn read_dir(
+        &self,
+        path: &Path,
+    ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
+        self.simulate_random_delay().await;
+        let path = normalize_path(path);
+        let mut state = self.state.lock();
+        state.read_dir_call_count += 1;
+        let entry = state.read_path(&path)?;
+        let mut entry = entry.lock();
+        let children = entry.dir_entries(&path)?;
+        let paths = children
+            .keys()
+            .map(|file_name| Ok(path.join(file_name)))
+            .collect::<Vec<_>>();
+        Ok(Box::pin(futures::stream::iter(paths)))
+    }
+
+    async fn watch(
+        &self,
+        path: &Path,
+        _: Duration,
+    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
+        self.simulate_random_delay().await;
+        let (tx, rx) = smol::channel::unbounded();
+        self.state.lock().event_txs.push(tx);
+        let path = path.to_path_buf();
+        let executor = self.executor.clone();
+        Box::pin(futures::StreamExt::filter(rx, move |events| {
+            let result = events.iter().any(|event| event.path.starts_with(&path));
+            let executor = executor.clone();
+            async move {
+                executor.simulate_random_delay().await;
+                result
+            }
+        }))
+    }
+
+    fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
+        let state = self.state.lock();
+        let entry = state.read_path(abs_dot_git).unwrap();
+        let mut entry = entry.lock();
+        if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
+            let state = git_repo_state
+                .get_or_insert_with(|| Arc::new(Mutex::new(FakeGitRepositoryState::default())))
+                .clone();
+            Some(repository::FakeGitRepository::open(state))
+        } else {
+            None
+        }
+    }
+
+    fn is_fake(&self) -> bool {
+        true
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    fn as_fake(&self) -> &FakeFs {
+        self
+    }
+}
+
+fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
+    rope.chunks().flat_map(move |chunk| {
+        let mut newline = false;
+        chunk.split('\n').flat_map(move |line| {
+            let ending = if newline {
+                Some(line_ending.as_str())
+            } else {
+                None
+            };
+            newline = true;
+            ending.into_iter().chain([line])
+        })
+    })
+}
+
+pub fn normalize_path(path: &Path) -> PathBuf {
+    let mut components = path.components().peekable();
+    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
+        components.next();
+        PathBuf::from(c.as_os_str())
+    } else {
+        PathBuf::new()
+    };
+
+    for component in components {
+        match component {
+            Component::Prefix(..) => unreachable!(),
+            Component::RootDir => {
+                ret.push(component.as_os_str());
+            }
+            Component::CurDir => {}
+            Component::ParentDir => {
+                ret.pop();
+            }
+            Component::Normal(c) => {
+                ret.push(c);
+            }
+        }
+    }
+    ret
+}
+
+pub fn copy_recursive<'a>(
+    fs: &'a dyn Fs,
+    source: &'a Path,
+    target: &'a Path,
+    options: CopyOptions,
+) -> BoxFuture<'a, Result<()>> {
+    use futures::future::FutureExt;
+
+    async move {
+        let metadata = fs
+            .metadata(source)
+            .await?
+            .ok_or_else(|| anyhow!("path does not exist: {}", source.display()))?;
+        if metadata.is_dir {
+            if !options.overwrite && fs.metadata(target).await.is_ok() {
+                if options.ignore_if_exists {
+                    return Ok(());
+                } else {
+                    return Err(anyhow!("{target:?} already exists"));
+                }
+            }
+
+            let _ = fs
+                .remove_dir(
+                    target,
+                    RemoveOptions {
+                        recursive: true,
+                        ignore_if_not_exists: true,
+                    },
+                )
+                .await;
+            fs.create_dir(target).await?;
+            let mut children = fs.read_dir(source).await?;
+            while let Some(child_path) = children.next().await {
+                if let Ok(child_path) = child_path {
+                    if let Some(file_name) = child_path.file_name() {
+                        let child_target_path = target.join(file_name);
+                        copy_recursive(fs, &child_path, &child_target_path, options).await?;
+                    }
+                }
+            }
+
+            Ok(())
+        } else {
+            fs.copy_file(source, target, options).await
+        }
+    }
+    .boxed()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui2::{Executor, TestDispatcher};
+    use rand::prelude::*;
+    use serde_json::json;
+
+    #[test]
+    fn test_fake_fs() {
+        let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
+        let executor = Executor::new(Arc::new(dispatcher));
+        let fs = FakeFs::new(executor.clone());
+        executor.block(async move {
+            fs.insert_tree(
+                "/root",
+                json!({
+                    "dir1": {
+                        "a": "A",
+                        "b": "B"
+                    },
+                    "dir2": {
+                        "c": "C",
+                        "dir3": {
+                            "d": "D"
+                        }
+                    }
+                }),
+            )
+            .await;
+
+            assert_eq!(
+                fs.files(),
+                vec![
+                    PathBuf::from("/root/dir1/a"),
+                    PathBuf::from("/root/dir1/b"),
+                    PathBuf::from("/root/dir2/c"),
+                    PathBuf::from("/root/dir2/dir3/d"),
+                ]
+            );
+
+            fs.insert_symlink("/root/dir2/link-to-dir3", "./dir3".into())
+                .await;
+
+            assert_eq!(
+                fs.canonicalize("/root/dir2/link-to-dir3".as_ref())
+                    .await
+                    .unwrap(),
+                PathBuf::from("/root/dir2/dir3"),
+            );
+            assert_eq!(
+                fs.canonicalize("/root/dir2/link-to-dir3/d".as_ref())
+                    .await
+                    .unwrap(),
+                PathBuf::from("/root/dir2/dir3/d"),
+            );
+            assert_eq!(
+                fs.load("/root/dir2/link-to-dir3/d".as_ref()).await.unwrap(),
+                "D",
+            );
+        });
+    }
+}

crates/fs2/src/repository.rs 🔗

@@ -0,0 +1,417 @@
+use anyhow::Result;
+use collections::HashMap;
+use git2::{BranchType, StatusShow};
+use parking_lot::Mutex;
+use serde_derive::{Deserialize, Serialize};
+use std::{
+    cmp::Ordering,
+    ffi::OsStr,
+    os::unix::prelude::OsStrExt,
+    path::{Component, Path, PathBuf},
+    sync::Arc,
+    time::SystemTime,
+};
+use sum_tree::{MapSeekTarget, TreeMap};
+use util::ResultExt;
+
+pub use git2::Repository as LibGitRepository;
+
+#[derive(Clone, Debug, Hash, PartialEq)]
+pub struct Branch {
+    pub name: Box<str>,
+    /// Timestamp of most recent commit, normalized to Unix Epoch format.
+    pub unix_timestamp: Option<i64>,
+}
+
+#[async_trait::async_trait]
+pub trait GitRepository: Send {
+    fn reload_index(&self);
+    fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
+    fn branch_name(&self) -> Option<String>;
+
+    /// Get the statuses of all of the files in the index that start with the given
+    /// path and have changes with resepect to the HEAD commit. This is fast because
+    /// the index stores hashes of trees, so that unchanged directories can be skipped.
+    fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus>;
+
+    /// Get the status of a given file in the working directory with respect to
+    /// the index. In the common case, when there are no changes, this only requires
+    /// an index lookup. The index stores the mtime of each file when it was added,
+    /// so there's no work to do if the mtime matches.
+    fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus>;
+
+    /// Get the status of a given file in the working directory with respect to
+    /// the HEAD commit. In the common case, when there are no changes, this only
+    /// requires an index lookup and blob comparison between the index and the HEAD
+    /// commit. The index stores the mtime of each file when it was added, so there's
+    /// no need to consider the working directory file if the mtime matches.
+    fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus>;
+
+    fn branches(&self) -> Result<Vec<Branch>>;
+    fn change_branch(&self, _: &str) -> Result<()>;
+    fn create_branch(&self, _: &str) -> Result<()>;
+}
+
+impl std::fmt::Debug for dyn GitRepository {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("dyn GitRepository<...>").finish()
+    }
+}
+
+impl GitRepository for LibGitRepository {
+    fn reload_index(&self) {
+        if let Ok(mut index) = self.index() {
+            _ = index.read(false);
+        }
+    }
+
+    fn load_index_text(&self, relative_file_path: &Path) -> Option<String> {
+        fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result<Option<String>> {
+            const STAGE_NORMAL: i32 = 0;
+            let index = repo.index()?;
+
+            // This check is required because index.get_path() unwraps internally :(
+            check_path_to_repo_path_errors(relative_file_path)?;
+
+            let oid = match index.get_path(&relative_file_path, STAGE_NORMAL) {
+                Some(entry) => entry.id,
+                None => return Ok(None),
+            };
+
+            let content = repo.find_blob(oid)?.content().to_owned();
+            Ok(Some(String::from_utf8(content)?))
+        }
+
+        match logic(&self, relative_file_path) {
+            Ok(value) => return value,
+            Err(err) => log::error!("Error loading head text: {:?}", err),
+        }
+        None
+    }
+
+    fn branch_name(&self) -> Option<String> {
+        let head = self.head().log_err()?;
+        let branch = String::from_utf8_lossy(head.shorthand_bytes());
+        Some(branch.to_string())
+    }
+
+    fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus> {
+        let mut map = TreeMap::default();
+
+        let mut options = git2::StatusOptions::new();
+        options.pathspec(path_prefix);
+        options.show(StatusShow::Index);
+
+        if let Some(statuses) = self.statuses(Some(&mut options)).log_err() {
+            for status in statuses.iter() {
+                let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes())));
+                let status = status.status();
+                if !status.contains(git2::Status::IGNORED) {
+                    if let Some(status) = read_status(status) {
+                        map.insert(path, status)
+                    }
+                }
+            }
+        }
+        map
+    }
+
+    fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus> {
+        // If the file has not changed since it was added to the index, then
+        // there can't be any changes.
+        if matches_index(self, path, mtime) {
+            return None;
+        }
+
+        let mut options = git2::StatusOptions::new();
+        options.pathspec(&path.0);
+        options.disable_pathspec_match(true);
+        options.include_untracked(true);
+        options.recurse_untracked_dirs(true);
+        options.include_unmodified(true);
+        options.show(StatusShow::Workdir);
+
+        let statuses = self.statuses(Some(&mut options)).log_err()?;
+        let status = statuses.get(0).and_then(|s| read_status(s.status()));
+        status
+    }
+
+    fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus> {
+        let mut options = git2::StatusOptions::new();
+        options.pathspec(&path.0);
+        options.disable_pathspec_match(true);
+        options.include_untracked(true);
+        options.recurse_untracked_dirs(true);
+        options.include_unmodified(true);
+
+        // If the file has not changed since it was added to the index, then
+        // there's no need to examine the working directory file: just compare
+        // the blob in the index to the one in the HEAD commit.
+        if matches_index(self, path, mtime) {
+            options.show(StatusShow::Index);
+        }
+
+        let statuses = self.statuses(Some(&mut options)).log_err()?;
+        let status = statuses.get(0).and_then(|s| read_status(s.status()));
+        status
+    }
+
+    fn branches(&self) -> Result<Vec<Branch>> {
+        let local_branches = self.branches(Some(BranchType::Local))?;
+        let valid_branches = local_branches
+            .filter_map(|branch| {
+                branch.ok().and_then(|(branch, _)| {
+                    let name = branch.name().ok().flatten().map(Box::from)?;
+                    let timestamp = branch.get().peel_to_commit().ok()?.time();
+                    let unix_timestamp = timestamp.seconds();
+                    let timezone_offset = timestamp.offset_minutes();
+                    let utc_offset =
+                        time::UtcOffset::from_whole_seconds(timezone_offset * 60).ok()?;
+                    let unix_timestamp =
+                        time::OffsetDateTime::from_unix_timestamp(unix_timestamp).ok()?;
+                    Some(Branch {
+                        name,
+                        unix_timestamp: Some(unix_timestamp.to_offset(utc_offset).unix_timestamp()),
+                    })
+                })
+            })
+            .collect();
+        Ok(valid_branches)
+    }
+    fn change_branch(&self, name: &str) -> Result<()> {
+        let revision = self.find_branch(name, BranchType::Local)?;
+        let revision = revision.get();
+        let as_tree = revision.peel_to_tree()?;
+        self.checkout_tree(as_tree.as_object(), None)?;
+        self.set_head(
+            revision
+                .name()
+                .ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
+        )?;
+        Ok(())
+    }
+    fn create_branch(&self, name: &str) -> Result<()> {
+        let current_commit = self.head()?.peel_to_commit()?;
+        self.branch(name, &current_commit, false)?;
+
+        Ok(())
+    }
+}
+
+fn matches_index(repo: &LibGitRepository, path: &RepoPath, mtime: SystemTime) -> bool {
+    if let Some(index) = repo.index().log_err() {
+        if let Some(entry) = index.get_path(&path, 0) {
+            if let Some(mtime) = mtime.duration_since(SystemTime::UNIX_EPOCH).log_err() {
+                if entry.mtime.seconds() == mtime.as_secs() as i32
+                    && entry.mtime.nanoseconds() == mtime.subsec_nanos()
+                {
+                    return true;
+                }
+            }
+        }
+    }
+    false
+}
+
+fn read_status(status: git2::Status) -> Option<GitFileStatus> {
+    if status.contains(git2::Status::CONFLICTED) {
+        Some(GitFileStatus::Conflict)
+    } else if status.intersects(
+        git2::Status::WT_MODIFIED
+            | git2::Status::WT_RENAMED
+            | git2::Status::INDEX_MODIFIED
+            | git2::Status::INDEX_RENAMED,
+    ) {
+        Some(GitFileStatus::Modified)
+    } else if status.intersects(git2::Status::WT_NEW | git2::Status::INDEX_NEW) {
+        Some(GitFileStatus::Added)
+    } else {
+        None
+    }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct FakeGitRepository {
+    state: Arc<Mutex<FakeGitRepositoryState>>,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct FakeGitRepositoryState {
+    pub index_contents: HashMap<PathBuf, String>,
+    pub worktree_statuses: HashMap<RepoPath, GitFileStatus>,
+    pub branch_name: Option<String>,
+}
+
+impl FakeGitRepository {
+    pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<Mutex<dyn GitRepository>> {
+        Arc::new(Mutex::new(FakeGitRepository { state }))
+    }
+}
+
+#[async_trait::async_trait]
+impl GitRepository for FakeGitRepository {
+    fn reload_index(&self) {}
+
+    fn load_index_text(&self, path: &Path) -> Option<String> {
+        let state = self.state.lock();
+        state.index_contents.get(path).cloned()
+    }
+
+    fn branch_name(&self) -> Option<String> {
+        let state = self.state.lock();
+        state.branch_name.clone()
+    }
+
+    fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus> {
+        let mut map = TreeMap::default();
+        let state = self.state.lock();
+        for (repo_path, status) in state.worktree_statuses.iter() {
+            if repo_path.0.starts_with(path_prefix) {
+                map.insert(repo_path.to_owned(), status.to_owned());
+            }
+        }
+        map
+    }
+
+    fn unstaged_status(&self, _path: &RepoPath, _mtime: SystemTime) -> Option<GitFileStatus> {
+        None
+    }
+
+    fn status(&self, path: &RepoPath, _mtime: SystemTime) -> Option<GitFileStatus> {
+        let state = self.state.lock();
+        state.worktree_statuses.get(path).cloned()
+    }
+
+    fn branches(&self) -> Result<Vec<Branch>> {
+        Ok(vec![])
+    }
+
+    fn change_branch(&self, name: &str) -> Result<()> {
+        let mut state = self.state.lock();
+        state.branch_name = Some(name.to_owned());
+        Ok(())
+    }
+
+    fn create_branch(&self, name: &str) -> Result<()> {
+        let mut state = self.state.lock();
+        state.branch_name = Some(name.to_owned());
+        Ok(())
+    }
+}
+
+fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
+    match relative_file_path.components().next() {
+        None => anyhow::bail!("repo path should not be empty"),
+        Some(Component::Prefix(_)) => anyhow::bail!(
+            "repo path `{}` should be relative, not a windows prefix",
+            relative_file_path.to_string_lossy()
+        ),
+        Some(Component::RootDir) => {
+            anyhow::bail!(
+                "repo path `{}` should be relative",
+                relative_file_path.to_string_lossy()
+            )
+        }
+        Some(Component::CurDir) => {
+            anyhow::bail!(
+                "repo path `{}` should not start with `.`",
+                relative_file_path.to_string_lossy()
+            )
+        }
+        Some(Component::ParentDir) => {
+            anyhow::bail!(
+                "repo path `{}` should not start with `..`",
+                relative_file_path.to_string_lossy()
+            )
+        }
+        _ => Ok(()),
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum GitFileStatus {
+    Added,
+    Modified,
+    Conflict,
+}
+
+impl GitFileStatus {
+    pub fn merge(
+        this: Option<GitFileStatus>,
+        other: Option<GitFileStatus>,
+        prefer_other: bool,
+    ) -> Option<GitFileStatus> {
+        if prefer_other {
+            return other;
+        } else {
+            match (this, other) {
+                (Some(GitFileStatus::Conflict), _) | (_, Some(GitFileStatus::Conflict)) => {
+                    Some(GitFileStatus::Conflict)
+                }
+                (Some(GitFileStatus::Modified), _) | (_, Some(GitFileStatus::Modified)) => {
+                    Some(GitFileStatus::Modified)
+                }
+                (Some(GitFileStatus::Added), _) | (_, Some(GitFileStatus::Added)) => {
+                    Some(GitFileStatus::Added)
+                }
+                _ => None,
+            }
+        }
+    }
+}
+
+#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
+pub struct RepoPath(pub PathBuf);
+
+impl RepoPath {
+    pub fn new(path: PathBuf) -> Self {
+        debug_assert!(path.is_relative(), "Repo paths must be relative");
+
+        RepoPath(path)
+    }
+}
+
+impl From<&Path> for RepoPath {
+    fn from(value: &Path) -> Self {
+        RepoPath::new(value.to_path_buf())
+    }
+}
+
+impl From<PathBuf> for RepoPath {
+    fn from(value: PathBuf) -> Self {
+        RepoPath::new(value)
+    }
+}
+
+impl Default for RepoPath {
+    fn default() -> Self {
+        RepoPath(PathBuf::new())
+    }
+}
+
+impl AsRef<Path> for RepoPath {
+    fn as_ref(&self) -> &Path {
+        self.0.as_ref()
+    }
+}
+
+impl std::ops::Deref for RepoPath {
+    type Target = PathBuf;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+#[derive(Debug)]
+pub struct RepoPathDescendants<'a>(pub &'a Path);
+
+impl<'a> MapSeekTarget<RepoPath> for RepoPathDescendants<'a> {
+    fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
+        if key.starts_with(&self.0) {
+            Ordering::Greater
+        } else {
+            self.0.cmp(key)
+        }
+    }
+}

crates/gpui2/src/app.rs 🔗

@@ -39,72 +39,11 @@ pub struct App(Arc<Mutex<AppContext>>);
 
 impl App {
     pub fn production(asset_source: Arc<dyn AssetSource>) -> Self {
-        let http_client = http::client();
-        Self::new(current_platform(), asset_source, http_client)
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn test(seed: u64) -> Self {
-        let platform = Arc::new(crate::TestPlatform::new(seed));
-        let asset_source = Arc::new(());
-        let http_client = util::http::FakeHttpClient::with_404_response();
-        Self::new(platform, asset_source, http_client)
-    }
-
-    fn new(
-        platform: Arc<dyn Platform>,
-        asset_source: Arc<dyn AssetSource>,
-        http_client: Arc<dyn HttpClient>,
-    ) -> Self {
-        let executor = platform.executor();
-        assert!(
-            executor.is_main_thread(),
-            "must construct App on main thread"
-        );
-
-        let text_system = Arc::new(TextSystem::new(platform.text_system()));
-        let mut entities = EntityMap::new();
-        let unit_entity = entities.insert(entities.reserve(), ());
-        let app_metadata = AppMetadata {
-            os_name: platform.os_name(),
-            os_version: platform.os_version().ok(),
-            app_version: platform.app_version().ok(),
-        };
-
-        Self(Arc::new_cyclic(|this| {
-            Mutex::new(AppContext {
-                this: this.clone(),
-                text_system,
-                platform: MainThreadOnly::new(platform, executor.clone()),
-                app_metadata,
-                flushing_effects: false,
-                pending_updates: 0,
-                next_frame_callbacks: Default::default(),
-                executor,
-                svg_renderer: SvgRenderer::new(asset_source.clone()),
-                asset_source,
-                image_cache: ImageCache::new(http_client),
-                text_style_stack: Vec::new(),
-                globals_by_type: HashMap::default(),
-                unit_entity,
-                entities,
-                windows: SlotMap::with_key(),
-                keymap: Arc::new(RwLock::new(Keymap::default())),
-                global_action_listeners: HashMap::default(),
-                action_builders: HashMap::default(),
-                pending_effects: VecDeque::new(),
-                pending_notifications: HashSet::default(),
-                pending_global_notifications: HashSet::default(),
-                observers: SubscriberSet::new(),
-                event_listeners: SubscriberSet::new(),
-                release_listeners: SubscriberSet::new(),
-                global_observers: SubscriberSet::new(),
-                quit_observers: SubscriberSet::new(),
-                layout_id_buffer: Default::default(),
-                propagate_event: true,
-                active_drag: None,
-            })
-        }))
+        Self(AppContext::new(
+            current_platform(),
+            asset_source,
+            http::client(),
+        ))
     }
 
     pub fn run<F>(self, on_finish_launching: F)
@@ -210,6 +149,62 @@ pub struct AppContext {
 }
 
 impl AppContext {
+    pub(crate) fn new(
+        platform: Arc<dyn Platform>,
+        asset_source: Arc<dyn AssetSource>,
+        http_client: Arc<dyn HttpClient>,
+    ) -> Arc<Mutex<Self>> {
+        let executor = platform.executor();
+        assert!(
+            executor.is_main_thread(),
+            "must construct App on main thread"
+        );
+
+        let text_system = Arc::new(TextSystem::new(platform.text_system()));
+        let mut entities = EntityMap::new();
+        let unit_entity = entities.insert(entities.reserve(), ());
+        let app_metadata = AppMetadata {
+            os_name: platform.os_name(),
+            os_version: platform.os_version().ok(),
+            app_version: platform.app_version().ok(),
+        };
+
+        Arc::new_cyclic(|this| {
+            Mutex::new(AppContext {
+                this: this.clone(),
+                text_system,
+                platform: MainThreadOnly::new(platform, executor.clone()),
+                app_metadata,
+                flushing_effects: false,
+                pending_updates: 0,
+                next_frame_callbacks: Default::default(),
+                executor,
+                svg_renderer: SvgRenderer::new(asset_source.clone()),
+                asset_source,
+                image_cache: ImageCache::new(http_client),
+                text_style_stack: Vec::new(),
+                globals_by_type: HashMap::default(),
+                unit_entity,
+                entities,
+                windows: SlotMap::with_key(),
+                keymap: Arc::new(RwLock::new(Keymap::default())),
+                global_action_listeners: HashMap::default(),
+                action_builders: HashMap::default(),
+                pending_effects: VecDeque::new(),
+                pending_notifications: HashSet::default(),
+                pending_global_notifications: HashSet::default(),
+                observers: SubscriberSet::new(),
+                event_listeners: SubscriberSet::new(),
+                release_listeners: SubscriberSet::new(),
+                global_observers: SubscriberSet::new(),
+                quit_observers: SubscriberSet::new(),
+                layout_id_buffer: Default::default(),
+                propagate_event: true,
+                active_drag: None,
+            })
+        })
+    }
+
     pub fn quit(&mut self) {
         let mut futures = Vec::new();
 

crates/gpui2/src/app/entity_map.rs 🔗

@@ -68,6 +68,7 @@ impl EntityMap {
 
     /// Move an entity to the stack.
     pub fn lease<'a, T>(&mut self, handle: &'a Handle<T>) -> Lease<'a, T> {
+        self.assert_valid_context(handle);
         let entity = Some(
             self.entities
                 .remove(handle.entity_id)
@@ -87,9 +88,17 @@ impl EntityMap {
     }
 
     pub fn read<T: 'static>(&self, handle: &Handle<T>) -> &T {
+        self.assert_valid_context(handle);
         self.entities[handle.entity_id].downcast_ref().unwrap()
     }
 
+    fn assert_valid_context(&self, handle: &AnyHandle) {
+        debug_assert!(
+            Weak::ptr_eq(&handle.entity_map, &Arc::downgrade(&self.ref_counts)),
+            "used a handle with the wrong context"
+        );
+    }
+
     pub fn take_dropped(&mut self) -> Vec<(EntityId, AnyBox)> {
         let dropped_entity_ids = mem::take(&mut self.ref_counts.write().dropped_entity_ids);
         dropped_entity_ids

crates/gpui2/src/app/test_context.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     AnyWindowHandle, AppContext, AsyncAppContext, Context, Executor, Handle, MainThread,
-    ModelContext, Result, Task, WindowContext,
+    ModelContext, Result, Task, TestDispatcher, TestPlatform, WindowContext,
 };
 use parking_lot::Mutex;
 use std::{any::Any, future::Future, sync::Arc};
@@ -37,6 +37,17 @@ impl Context for TestAppContext {
 }
 
 impl TestAppContext {
+    pub fn new(dispatcher: TestDispatcher) -> Self {
+        let executor = Executor::new(Arc::new(dispatcher));
+        let platform = Arc::new(TestPlatform::new(executor.clone()));
+        let asset_source = Arc::new(());
+        let http_client = util::http::FakeHttpClient::with_404_response();
+        Self {
+            app: AppContext::new(platform, asset_source, http_client),
+            executor,
+        }
+    }
+
     pub fn refresh(&mut self) -> Result<()> {
         let mut lock = self.app.lock();
         lock.refresh();
@@ -47,27 +58,27 @@ impl TestAppContext {
         &self.executor
     }
 
-    pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> Result<R> {
+    pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> R {
         let mut lock = self.app.lock();
-        Ok(f(&mut *lock))
+        f(&mut *lock)
     }
 
     pub fn read_window<R>(
         &self,
         handle: AnyWindowHandle,
-        update: impl FnOnce(&WindowContext) -> R,
-    ) -> Result<R> {
+        read: impl FnOnce(&WindowContext) -> R,
+    ) -> R {
         let mut app_context = self.app.lock();
-        app_context.read_window(handle.id, update)
+        app_context.read_window(handle.id, read).unwrap()
     }
 
     pub fn update_window<R>(
         &self,
         handle: AnyWindowHandle,
         update: impl FnOnce(&mut WindowContext) -> R,
-    ) -> Result<R> {
+    ) -> R {
         let mut app = self.app.lock();
-        app.update_window(handle.id, update)
+        app.update_window(handle.id, update).unwrap()
     }
 
     pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut + Send + 'static) -> Task<R>
@@ -94,22 +105,22 @@ impl TestAppContext {
     pub fn run_on_main<R>(
         &self,
         f: impl FnOnce(&mut MainThread<AppContext>) -> R + Send + 'static,
-    ) -> Result<Task<R>>
+    ) -> Task<R>
     where
         R: Send + 'static,
     {
         let mut app_context = self.app.lock();
-        Ok(app_context.run_on_main(f))
+        app_context.run_on_main(f)
     }
 
-    pub fn has_global<G: 'static>(&self) -> Result<bool> {
+    pub fn has_global<G: 'static>(&self) -> bool {
         let lock = self.app.lock();
-        Ok(lock.has_global::<G>())
+        lock.has_global::<G>()
     }
 
-    pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> Result<R> {
+    pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> R {
         let lock = self.app.lock();
-        Ok(read(lock.global(), &lock))
+        read(lock.global(), &lock)
     }
 
     pub fn try_read_global<G: 'static, R>(
@@ -123,9 +134,9 @@ impl TestAppContext {
     pub fn update_global<G: 'static, R>(
         &mut self,
         update: impl FnOnce(&mut G, &mut AppContext) -> R,
-    ) -> Result<R> {
+    ) -> R {
         let mut lock = self.app.lock();
-        Ok(lock.update_global(update))
+        lock.update_global(update)
     }
 
     fn to_async(&self) -> AsyncAppContext {

crates/gpui2/src/executor.rs 🔗

@@ -207,8 +207,8 @@ impl Executor {
     }
 
     #[cfg(any(test, feature = "test-support"))]
-    pub async fn simulate_random_delay(&self) {
-        todo!("simulate_random_delay")
+    pub fn simulate_random_delay(&self) -> impl Future<Output = ()> {
+        self.dispatcher.as_test().unwrap().simulate_random_delay()
     }
 
     pub fn num_cpus(&self) -> usize {

crates/gpui2/src/platform.rs 🔗

@@ -162,8 +162,11 @@ pub trait PlatformDispatcher: Send + Sync {
     fn dispatch_on_main_thread(&self, runnable: Runnable);
     fn dispatch_after(&self, duration: Duration, runnable: Runnable);
     fn poll(&self) -> bool;
+
     #[cfg(any(test, feature = "test-support"))]
-    fn advance_clock(&self, duration: Duration);
+    fn as_test(&self) -> Option<&TestDispatcher> {
+        None
+    }
 }
 
 pub trait PlatformTextSystem: Send + Sync {

crates/gpui2/src/platform/mac/dispatcher.rs 🔗

@@ -71,11 +71,6 @@ impl PlatformDispatcher for MacDispatcher {
     fn poll(&self) -> bool {
         false
     }
-
-    #[cfg(any(test, feature = "test-support"))]
-    fn advance_clock(&self, _: Duration) {
-        unimplemented!()
-    }
 }
 
 extern "C" fn trampoline(runnable: *mut c_void) {

crates/gpui2/src/platform/test/dispatcher.rs 🔗

@@ -4,7 +4,10 @@ use collections::{BTreeMap, HashMap, VecDeque};
 use parking_lot::Mutex;
 use rand::prelude::*;
 use std::{
+    future::Future,
+    pin::Pin,
     sync::Arc,
+    task::{Context, Poll},
     time::{Duration, Instant},
 };
 use util::post_inc;
@@ -44,6 +47,34 @@ impl TestDispatcher {
             state: Arc::new(Mutex::new(state)),
         }
     }
+
+    pub fn advance_clock(&self, by: Duration) {
+        self.state.lock().time += by;
+    }
+
+    pub fn simulate_random_delay(&self) -> impl Future<Output = ()> {
+        pub struct YieldNow {
+            count: usize,
+        }
+
+        impl Future for YieldNow {
+            type Output = ();
+
+            fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
+                if self.count > 0 {
+                    self.count -= 1;
+                    cx.waker().wake_by_ref();
+                    Poll::Pending
+                } else {
+                    Poll::Ready(())
+                }
+            }
+        }
+
+        YieldNow {
+            count: self.state.lock().random.gen_range(0..10),
+        }
+    }
 }
 
 impl Clone for TestDispatcher {
@@ -131,8 +162,8 @@ impl PlatformDispatcher for TestDispatcher {
         true
     }
 
-    fn advance_clock(&self, by: Duration) {
-        self.state.lock().time += by;
+    fn as_test(&self) -> Option<&TestDispatcher> {
+        Some(self)
     }
 }
 

crates/gpui2/src/platform/test/platform.rs 🔗

@@ -1,5 +1,5 @@
-use crate::{DisplayId, Executor, Platform, PlatformTextSystem, TestDispatcher};
-use rand::prelude::*;
+use crate::{DisplayId, Executor, Platform, PlatformTextSystem};
+use anyhow::{anyhow, Result};
 use std::sync::Arc;
 
 pub struct TestPlatform {
@@ -7,11 +7,8 @@ pub struct TestPlatform {
 }
 
 impl TestPlatform {
-    pub fn new(seed: u64) -> Self {
-        let rng = StdRng::seed_from_u64(seed);
-        TestPlatform {
-            executor: Executor::new(Arc::new(TestDispatcher::new(rng))),
-        }
+    pub fn new(executor: Executor) -> Self {
+        TestPlatform { executor }
     }
 }
 
@@ -136,18 +133,18 @@ impl Platform for TestPlatform {
     }
 
     fn os_name(&self) -> &'static str {
-        unimplemented!()
+        "test"
     }
 
-    fn os_version(&self) -> anyhow::Result<crate::SemanticVersion> {
-        unimplemented!()
+    fn os_version(&self) -> Result<crate::SemanticVersion> {
+        Err(anyhow!("os_version called on TestPlatform"))
     }
 
-    fn app_version(&self) -> anyhow::Result<crate::SemanticVersion> {
-        unimplemented!()
+    fn app_version(&self) -> Result<crate::SemanticVersion> {
+        Err(anyhow!("app_version called on TestPlatform"))
     }
 
-    fn app_path(&self) -> anyhow::Result<std::path::PathBuf> {
+    fn app_path(&self) -> Result<std::path::PathBuf> {
         unimplemented!()
     }
 
@@ -155,7 +152,7 @@ impl Platform for TestPlatform {
         unimplemented!()
     }
 
-    fn path_for_auxiliary_executable(&self, _name: &str) -> anyhow::Result<std::path::PathBuf> {
+    fn path_for_auxiliary_executable(&self, _name: &str) -> Result<std::path::PathBuf> {
         unimplemented!()
     }
 
@@ -175,20 +172,15 @@ impl Platform for TestPlatform {
         unimplemented!()
     }
 
-    fn write_credentials(
-        &self,
-        _url: &str,
-        _username: &str,
-        _password: &[u8],
-    ) -> anyhow::Result<()> {
+    fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Result<()> {
         unimplemented!()
     }
 
-    fn read_credentials(&self, _url: &str) -> anyhow::Result<Option<(String, Vec<u8>)>> {
+    fn read_credentials(&self, _url: &str) -> Result<Option<(String, Vec<u8>)>> {
         unimplemented!()
     }
 
-    fn delete_credentials(&self, _url: &str) -> anyhow::Result<()> {
+    fn delete_credentials(&self, _url: &str) -> Result<()> {
         unimplemented!()
     }
 }

crates/gpui2/src/text_system/line_wrapper.rs 🔗

@@ -139,11 +139,15 @@ impl Boundary {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{font, App};
+    use crate::{font, TestAppContext, TestDispatcher};
+    use rand::prelude::*;
 
     #[test]
     fn test_wrap_line() {
-        App::test(0).run(|cx| {
+        let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
+        let cx = TestAppContext::new(dispatcher);
+
+        cx.update(|cx| {
             let text_system = cx.text_system().clone();
             let mut wrapper = LineWrapper::new(
                 text_system.font_id(&font("Courier")).unwrap(),

crates/settings2/Cargo.toml 🔗

@@ -16,7 +16,7 @@ collections = { path = "../collections" }
 gpui2 = { path = "../gpui2" }
 sqlez = { path = "../sqlez" }
 fs = { path = "../fs" }
-feature_flags = { path = "../feature_flags" }
+feature_flags2 = { path = "../feature_flags2" }
 util = { path = "../util" }
 
 anyhow.workspace = true