first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+66 -67
View File
@@ -1,4 +1,4 @@
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
/// A model for operating on files.
///
/// Allows opening and saving files in a single, central model. Subscribers can watch for content
@@ -6,34 +6,25 @@ use std::collections::HashSet;
use std::future::Future;
use std::io;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::rc::Rc;
use std::time::{Duration, SystemTime};
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use galaxy_core::HostId;
use galaxy_util::standardized_path::StandardizedPath;
use remote_server::client::RemoteServerClient;
use remote_server::manager::RemoteServerManager;
use futures::io::{AsyncBufReadExt, BufReader};
use futures::StreamExt;
use async_channel::Sender;
use galaxy_util::content_version::ContentVersion;
use galaxy_util::file::FileSaveError;
use galaxy_util::file::{FileId, FileLoadError};
use galaxyui::ModelHandle;
use galaxyui::{r#async::SpawnedFutureHandle, AppContext, Entity, ModelContext, SingletonEntity};
use futures::io::{AsyncBufReadExt, BufReader};
use futures::StreamExt;
use notify_debouncer_full::notify::{RecursiveMode, WatchFilter};
use repo_metadata::{
repositories::DetectedRepositories,
repository::{RepositorySubscriber, SubscriberId},
CanonicalizedPath, Repository, RepositoryUpdate,
};
use remote_server::manager::RemoteServerManager;
use repo_metadata::repositories::DetectedRepositories;
use repo_metadata::repository::{RepositorySubscriber, SubscriberId};
use repo_metadata::{CanonicalizedPath, Repository, RepositoryUpdate};
use galaxy_core::HostId;
use galaxy_util::content_version::ContentVersion;
use galaxy_util::file::{FileId, FileLoadError, FileSaveError};
use galaxy_util::standardized_path::StandardizedPath;
use galaxyui_core::r#async::SpawnedFutureHandle;
use galaxyui_core::{Entity, ModelContext, ModelHandle, SingletonEntity};
use watcher::{BulkFilesystemWatcher, BulkFilesystemWatcherEvent};
pub mod text_file_reader;
@@ -91,13 +82,14 @@ enum WatcherType {
}
/// Per-file backing store.
/// Remote files dispatch through [`RemoteServerClient`] via [`RemoteServerManager`].
/// Remote files dispatch host-scoped requests through a
/// [`RemoteServerManager`] `HostRequestHandle`.
enum FileBackend {
Local(LocalFile),
Remote {
/// Identifies the remote host. The actual client is looked up from
/// Identifies the remote host. A `HostRequestHandle` is resolved from
/// [`RemoteServerManager`] at call time, which naturally handles
/// disconnect (lookup returns `Err`) without holding an `Arc` alive
/// disconnect (the request fails) without holding an `Arc` alive
/// per file.
host_id: HostId,
/// Platform-aware path on the remote host.
@@ -309,7 +301,7 @@ impl FileModel {
let watcher =
ctx.add_model(|ctx| BulkFilesystemWatcher::new(Duration::from_millis(200), ctx));
ctx.subscribe_to_model(&watcher, |me, event, ctx| {
ctx.subscribe_to_model(&watcher, |me, _, event, ctx| {
me.handle_watcher_event(event, ctx);
});
@@ -345,7 +337,7 @@ impl FileModel {
/// Register a remote file path and return a `FileId`.
///
/// The returned `FileId` can be used with `save()` and `delete()` which
/// will dispatch to the remote backend via [`RemoteServerClient`].
/// will dispatch to the remote backend via `RemoteServerClient`.
pub fn register_remote_file(&mut self, host_id: HostId, path: StandardizedPath) -> FileId {
let file_id = FileId::new();
self.file_state.insert_remote(file_id, host_id, path);
@@ -429,13 +421,22 @@ impl FileModel {
let version = ContentVersion::new();
me.set_version(file_id, version);
// Only register individual watcher if not using repo subscription
// Only register individual watcher if not using repo subscription.
// Watch the parent directory (NonRecursive) instead of the file
// itself so the watch survives editors that use a
// delete+create/rename pattern (vim, sed -i, etc.). Watching
// the file directly would lose the inotify watch when the
// original inode is deleted.
if use_individual_watcher {
let watch_path = file_path_clone
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| file_path_clone.clone());
me.watcher.update(ctx, |watcher, _ctx| {
std::mem::drop(watcher.register_path(
&file_path_clone,
&watch_path,
WatchFilter::accept_all(),
RecursiveMode::Recursive,
RecursiveMode::NonRecursive,
));
});
}
@@ -638,9 +639,28 @@ impl FileModel {
if !path_still_used {
match watcher_type {
WatcherType::Individual => {
self.watcher.update(ctx, |watcher, _ctx| {
std::mem::drop(watcher.unregister_path(path.as_path()));
});
// Unwatch the parent directory (matching the register
// in open() which watches the parent, not the file).
// Only unregister if no other individually-watched
// files share the same parent directory.
let watch_path = path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| path.clone());
let other_files_share_parent =
self.file_state.local_values().any(|f| {
f.watcher_type == WatcherType::Individual
&& f.path
.as_deref()
.and_then(|p| p.parent())
.map(|p| p == watch_path)
.unwrap_or(false)
});
if !other_files_share_parent {
self.watcher.update(ctx, |watcher, _ctx| {
std::mem::drop(watcher.unregister_path(&watch_path));
});
}
}
WatcherType::Repository => {
if let Some((repo_root, unused_repo)) =
@@ -710,17 +730,11 @@ impl FileModel {
);
}
FileBackend::Remote { host_id, path } => {
let client = Self::resolve_remote_client(host_id, ctx)?;
let handle = RemoteServerManager::as_ref(ctx).host_request_handle(host_id);
let path = path.as_str().to_string();
let future = async move {
client
.write_file(path, content)
.await
.map_err(|e| e.to_string())
};
ctx.spawn(
future,
move |me, result: Result<(), String>, ctx| match result {
async move { handle.write_file(path, content).await },
move |me, result, ctx| match result {
Ok(()) => {
me.set_version(file_id, version);
ctx.emit(FileModelEvent::FileSaved {
@@ -728,10 +742,10 @@ impl FileModel {
version,
});
}
Err(err) => {
Err(e) => {
ctx.emit(FileModelEvent::FailedToSave {
id: file_id,
error: Rc::new(FileSaveError::RemoteError(err)),
error: Rc::new(FileSaveError::RemoteError(e.to_string())),
});
}
},
@@ -861,13 +875,11 @@ impl FileModel {
);
}
FileBackend::Remote { host_id, path } => {
let client = Self::resolve_remote_client(host_id, ctx)?;
let handle = RemoteServerManager::as_ref(ctx).host_request_handle(host_id);
let path = path.as_str().to_string();
let future =
async move { client.delete_file(path).await.map_err(|e| e.to_string()) };
ctx.spawn(
future,
move |me, result: Result<(), String>, ctx| match result {
async move { handle.delete_file(path).await },
move |me, result, ctx| match result {
Ok(()) => {
me.set_version(file_id, version);
ctx.emit(FileModelEvent::FileSaved {
@@ -875,10 +887,10 @@ impl FileModel {
version,
});
}
Err(err) => {
Err(e) => {
ctx.emit(FileModelEvent::FailedToSave {
id: file_id,
error: Rc::new(FileSaveError::RemoteError(err)),
error: Rc::new(FileSaveError::RemoteError(e.to_string())),
});
}
},
@@ -889,19 +901,6 @@ impl FileModel {
Ok(())
}
/// Look up the `RemoteServerClient` for a given host at call time.
fn resolve_remote_client(
host_id: &HostId,
ctx: &AppContext,
) -> Result<std::sync::Arc<RemoteServerClient>, FileSaveError> {
RemoteServerManager::as_ref(ctx)
.client_for_host(host_id)
.cloned()
.ok_or_else(|| {
FileSaveError::RemoteError(format!("Remote host {host_id} is not connected"))
})
}
pub fn set_version(&mut self, file_id: FileId, version: ContentVersion) {
if let Some(backend) = self.file_state.get_mut(file_id) {
backend.set_version(version);
@@ -928,7 +927,7 @@ impl FileModel {
// Try to find a repository for this path
let repository =
DetectedRepositories::as_ref(ctx).get_watched_repo_for_path(file_path, ctx)?;
DetectedRepositories::as_ref(ctx).get_local_watched_repo_for_path(file_path, ctx)?;
let repo_root = repository.as_ref(ctx).root_dir().to_local_path_lossy();
@@ -1195,5 +1194,5 @@ impl RepositorySubscriber for FileRepositorySubscriber {
}
#[cfg(test)]
#[path = "lib_test.rs"]
#[path = "lib_tests.rs"]
mod tests;