Файли
openidea/crates/ide-workspace/src/file_ops.rs
T
Hacbit 7e9acdc59b feat(unity): detect projects and preserve asset metadata (#1207)
Unity projects (a folder with Assets and ProjectSettings/ProjectVersion.txt)
are detected without a generated solution: Assets / Packages become source
roots, ProjectSettings resources, and Unity's cache and build folders are
excluded before they exist. A Unity pane in the Project view shows the
editor version and hides .meta files, generated C# project files and
caches (each can be shown again).

Operations on assets under Assets keep their .meta files consistent:
rename and move carry the metadata (same GUID), delete removes both, copy
generates new GUIDs and updates references inside the copied group, and
file-move undo and workspace edits include the metadata. Destination
metadata conflicts and links inside the project are rejected before
anything changes.

Co-authored-by: hacbit <[email protected]>
2026-11-05 02:16:06 +08:00

95 рядки
3.3 KiB
Rust

//! Project-aware file operations. Plain files retain the VFS behavior; project
//! rules may expand a resource operation before the common transaction runs.
use crate::FileMove;
use ide_core::{IdeError, IdeResult, WorkspaceEdit};
use std::path::{Path, PathBuf};
mod project;
pub fn copy(from: &Path, to: &Path) -> IdeResult<()> {
project::copy(from, to).unwrap_or_else(|| ide_vfs::copy(from, to))
}
pub fn delete(path: &Path) -> IdeResult<()> {
project::delete(path).unwrap_or_else(|| ide_vfs::delete(path))
}
/// Files Local History must capture before deletion.
pub fn deletion_paths(path: &Path) -> IdeResult<Vec<PathBuf>> {
project::deletion_paths(path).unwrap_or_else(|| Ok(vec![path.to_path_buf()]))
}
pub fn rename(from: &Path, to: &Path) -> IdeResult<()> {
if !project::has_move_rules(from, to) {
return ide_vfs::rename(from, to);
}
let moves = expand_moves(&[FileMove { from: from.into(), to: to.into() }])?;
move_files(&moves)
}
pub(crate) fn expand_moves(moves: &[FileMove]) -> IdeResult<Vec<FileMove>> {
let expanded = project::expand_moves(moves)?;
validate_moves(&expanded)?;
Ok(expanded)
}
fn path_key(path: &Path) -> PathBuf {
#[cfg(windows)]
return path.to_string_lossy().replace('\\', "/").to_lowercase().into();
#[cfg(not(windows))]
path.to_path_buf()
}
fn validate_moves(moves: &[FileMove]) -> IdeResult<()> {
let mut sources = std::collections::HashSet::new();
let mut targets = std::collections::HashSet::new();
for m in moves {
ide_vfs::validate_rename(&m.from, &m.to)?;
let from = path_key(&m.from);
let to = path_key(&m.to);
if to != from && to.starts_with(&from) {
return Err(IdeError::invalid("Cannot move a directory into itself"));
}
if !sources.insert(from) || !targets.insert(to) {
return Err(IdeError::invalid("Duplicate source or destination in file operation"));
}
}
Ok(())
}
pub(crate) fn move_files(moves: &[FileMove]) -> IdeResult<()> {
execute_moves(moves, &|from, to| project::rename_entry(from, to).unwrap_or_else(|| ide_vfs::rename(from, to)))
}
/// Executes an expanded batch. Rollback never overwrites a newly occupied path;
/// failures report where the caller can recover the remaining files.
pub(crate) fn execute_moves(moves: &[FileMove], rename: &dyn Fn(&Path, &Path) -> IdeResult<()>) -> IdeResult<()> {
validate_moves(moves)?;
for (i, m) in moves.iter().enumerate() {
if let Err(mut error) = rename(&m.from, &m.to) {
for done in moves[..i].iter().rev() {
if let Err(rollback) =
ide_vfs::validate_rename(&done.to, &done.from).and_then(|()| rename(&done.to, &done.from))
{
error.message.push_str(&format!(
"; rollback {} -> {} failed: {rollback}",
done.to.display(),
done.from.display()
));
}
}
return Err(error);
}
}
Ok(())
}
pub(crate) fn validate_workspace_edit_paths(edit: &WorkspaceEdit) -> IdeResult<()> {
project::validate_workspace_edit_paths(edit)
}
pub(crate) fn expand_workspace_edit(edit: &mut WorkspaceEdit) -> IdeResult<()> {
project::expand_workspace_edit(edit)
}