mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
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]>
203 lines
5.6 KiB
Rust
203 lines
5.6 KiB
Rust
//! Create / rename / delete / copy.
|
|
|
|
use ide_core::error::ErrorKind;
|
|
use ide_core::{IdeError, IdeResult};
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
fn already_exists(path: &Path) -> IdeError {
|
|
IdeError::new(ErrorKind::AlreadyExists, format!("{} already exists", path.display()))
|
|
}
|
|
|
|
fn not_found(path: &Path) -> IdeError {
|
|
IdeError::not_found(format!("{} does not exist", path.display()))
|
|
}
|
|
|
|
fn exists(path: &Path) -> bool {
|
|
fs::symlink_metadata(path).is_ok()
|
|
}
|
|
|
|
fn ensure_parent(path: &Path) -> IdeResult<()> {
|
|
if let Some(parent) = path.parent()
|
|
&& !parent.as_os_str().is_empty()
|
|
{
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn create_file(path: &Path) -> IdeResult<()> {
|
|
if exists(path) {
|
|
return Err(already_exists(path));
|
|
}
|
|
ensure_parent(path)?;
|
|
fs::OpenOptions::new().write(true).create_new(true).open(path)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn create_file_bytes(path: &Path, data: &[u8]) -> IdeResult<()> {
|
|
use std::io::Write;
|
|
if exists(path) {
|
|
return Err(already_exists(path));
|
|
}
|
|
ensure_parent(path)?;
|
|
let mut file = fs::OpenOptions::new().write(true).create_new(true).open(path)?;
|
|
file.write_all(data)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// IDEA's "Toggle Read-only Attribute": read-only clears every write bit,
|
|
/// writable adds the owner's.
|
|
pub(crate) fn set_writable(path: &Path, writable: bool) -> IdeResult<()> {
|
|
let meta = fs::metadata(path).map_err(|_| not_found(path))?;
|
|
let mut perms = meta.permissions();
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let mode = perms.mode();
|
|
perms.set_mode(if writable { mode | 0o200 } else { mode & !0o222 });
|
|
}
|
|
#[cfg(not(unix))]
|
|
perms.set_readonly(!writable);
|
|
fs::set_permissions(path, perms)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn create_dir(path: &Path) -> IdeResult<()> {
|
|
if exists(path) {
|
|
return Err(already_exists(path));
|
|
}
|
|
fs::create_dir_all(path)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn same_file(a: &Path, b: &Path) -> bool {
|
|
use std::os::unix::fs::MetadataExt;
|
|
match (fs::symlink_metadata(a), fs::symlink_metadata(b)) {
|
|
(Ok(x), Ok(y)) => x.dev() == y.dev() && x.ino() == y.ino(),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn same_file(a: &Path, b: &Path) -> bool {
|
|
match (fs::canonicalize(a), fs::canonicalize(b)) {
|
|
(Ok(x), Ok(y)) => x == y,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn validate_rename(from: &Path, to: &Path) -> IdeResult<()> {
|
|
if !exists(from) {
|
|
return Err(not_found(from));
|
|
}
|
|
if from == to {
|
|
return Ok(());
|
|
}
|
|
// A case-only rename on a case-insensitive file system sees `to` as existing.
|
|
if exists(to) && !same_file(from, to) {
|
|
return Err(already_exists(to));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn rename(from: &Path, to: &Path) -> IdeResult<()> {
|
|
validate_rename(from, to)?;
|
|
if from == to {
|
|
return Ok(());
|
|
}
|
|
ensure_parent(to)?;
|
|
match fs::rename(from, to) {
|
|
Ok(()) => Ok(()),
|
|
Err(e) if e.kind() == std::io::ErrorKind::CrossesDevices => {
|
|
copy_recursive(from, to)?;
|
|
remove_permanently(from)
|
|
}
|
|
Err(e) => Err(e.into()),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn remove_permanently(path: &Path) -> IdeResult<()> {
|
|
let meta = fs::symlink_metadata(path)?;
|
|
if meta.is_dir() {
|
|
fs::remove_dir_all(path)?;
|
|
} else {
|
|
fs::remove_file(path)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn delete(path: &Path, use_trash: bool) -> IdeResult<()> {
|
|
if !exists(path) {
|
|
return Err(not_found(path));
|
|
}
|
|
if use_trash {
|
|
#[allow(unused_mut)]
|
|
let mut ctx = trash::TrashContext::default();
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
use trash::macos::{DeleteMethod, TrashContextExtMacos};
|
|
// No Finder scripting: no permission prompt, no sound.
|
|
ctx.set_delete_method(DeleteMethod::NsFileManager);
|
|
}
|
|
match ctx.delete(path) {
|
|
Ok(()) => return Ok(()),
|
|
Err(e) => tracing::warn!("cannot move {} to trash ({e}); deleting permanently", path.display()),
|
|
}
|
|
}
|
|
remove_permanently(path)
|
|
}
|
|
|
|
pub(crate) fn copy(from: &Path, to: &Path) -> IdeResult<()> {
|
|
let meta = fs::symlink_metadata(from).map_err(|_| not_found(from))?;
|
|
if exists(to) {
|
|
return Err(already_exists(to));
|
|
}
|
|
if meta.is_dir() {
|
|
let src = fs::canonicalize(from)?;
|
|
let dst_parent = to.parent().map(|p| fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf()));
|
|
if dst_parent.is_some_and(|p| p.starts_with(&src)) {
|
|
return Err(IdeError::invalid(format!("cannot copy {} into itself", from.display())));
|
|
}
|
|
}
|
|
ensure_parent(to)?;
|
|
copy_recursive(from, to)
|
|
}
|
|
|
|
fn copy_recursive(from: &Path, to: &Path) -> IdeResult<()> {
|
|
let meta = fs::symlink_metadata(from)?;
|
|
let ft = meta.file_type();
|
|
if ft.is_symlink() {
|
|
copy_symlink(from, to)
|
|
} else if ft.is_dir() {
|
|
fs::create_dir(to)?;
|
|
for entry in fs::read_dir(from)? {
|
|
let entry = entry?;
|
|
copy_recursive(&entry.path(), &to.join(entry.file_name()))?;
|
|
}
|
|
fs::set_permissions(to, meta.permissions())?;
|
|
Ok(())
|
|
} else {
|
|
fs::copy(from, to)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn copy_symlink(from: &Path, to: &Path) -> IdeResult<()> {
|
|
let target = fs::read_link(from)?;
|
|
std::os::unix::fs::symlink(target, to)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn copy_symlink(from: &Path, to: &Path) -> IdeResult<()> {
|
|
if fs::metadata(from)?.is_dir() {
|
|
copy_recursive(&fs::canonicalize(from)?, to)
|
|
} else {
|
|
fs::copy(from, to)?;
|
|
Ok(())
|
|
}
|
|
}
|