beet_build 0.0.8

Codegen and compilation tooling for beet
use crate::prelude::*;
use beet_core::exports::notify::EventKind;
use beet_core::exports::notify::event::CreateKind;
use beet_core::exports::notify::event::ModifyKind;
use beet_core::exports::notify::event::RemoveKind;
use beet_core::prelude::*;
use std::path::Path;

/// Adde to an entity used to represent an file included
/// in the [`WorkspaceConfig`]. These are loaded for different
/// purposes by [`SnippetsPlugin`] and [`RouteCodegenSequence`].
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Component, Deref)]
#[require(FileExprHash)]
pub struct SourceFile {
	/// The path to this source file
	path: AbsPathBuf,
}

impl SourceFile {
	pub fn new(path: AbsPathBuf) -> Self { Self { path } }
	pub fn path(&self) -> &AbsPathBuf { &self.path }
}
impl AsRef<Path> for SourceFile {
	fn as_ref(&self) -> &Path { self.path.as_ref() }
}


/// Parent of every [`SourceFile`] entity that exists outside of a [`RouteFileCollection`].
#[derive(Component)]
pub struct NonCollectionSourceFiles;

/// A [`SourceFile`] watched by another [`SourceFile`]
#[derive(Deref, Component)]
#[relationship(relationship_target = WatchedFiles)]
// TODO many-many relations
pub struct FileWatchedBy(pub Entity);


/// A collection of [`SourceFile`] entities that this [`SourceFile`] is watching.
/// If any child changes this should also change.
#[derive(Deref, Component)]
#[relationship_target(relationship = FileWatchedBy, linked_spawn)]
pub struct WatchedFiles(Vec<Entity>);


/// Update [`SourceFile`] entities based on file watch events,
/// including marking as [`Changed`] on modification.
pub fn parse_dir_watch_events(
	ev: On<DirEvent>,
	mut commands: Commands,
	root_entity: Populated<Entity, With<NonCollectionSourceFiles>>,
	config: When<Res<WorkspaceConfig>>,
	mut existing: Query<(Entity, &mut SourceFile)>,
) -> Result {
	for ev in ev
		.iter()
		// we only care about files specified in the config
		.filter(|ev| config.passes(&ev.path))
	{
		tracing::debug!("SourceFile event: {}", ev);

		let matches = existing
			.iter_mut()
			.filter(|(_, file)| ***file == ev.path)
			.map(|(en, _)| en)
			.collect::<Vec<_>>();

		match ev.kind {
			EventKind::Create(CreateKind::File) => {
				commands.spawn((
					ChildOf(root_entity.single()?),
					SourceFile::new(ev.path.clone()),
				));
			}
			EventKind::Create(CreateKind::Folder) => {
				// noop
			}
			// emitted for both the from and to renames so
			// assume if no matches, its the To event
			EventKind::Modify(ModifyKind::Name(_)) => {
				if matches.is_empty() {
					commands.spawn((
						ChildOf(root_entity.single()?),
						SourceFile::new(ev.path.clone()),
					));
				} else {
					for entity in matches {
						commands.entity(entity).despawn();
					}
				}
			}
			EventKind::Remove(RemoveKind::File) => {
				for entity in matches {
					commands.entity(entity).despawn();
				}
			}
			EventKind::Modify(_) => {
				for entity in matches {
					commands.run_system_cached_with(reset_file, entity);
				}
			}
			other => {
				tracing::warn!("Unhandled file event: {:?}", other);
			}
		}
	}
	Ok(())
}


/// Runs for any [`SourceFile`] that changes:
/// - mark it as [`Added`]
/// - remove all [`Children`]
/// If it has a [`FileWatchedBy`] component, also run for that parent
fn reset_file(
	In(entity): In<Entity>,
	mut commands: Commands,
	mut files: Query<(Entity, &mut SourceFile, Option<&FileWatchedBy>)>,
) {
	if let Ok((entity, mut file, parent)) = files.get_mut(entity) {
		trace!("Resetting Source File: {}", file.path);
		file.set_added();
		commands.entity(entity).despawn_related::<Children>();
		if let Some(parent) = parent {
			commands.run_system_cached_with(reset_file, **parent);
		}
	}
}