Files
openidea/crates/ide-vfs/src/ops.rs
l0ng-ai df6ed5ccba feat(editor): paste and drop images into Markdown files (#489)
Pasting or dropping an image into a Markdown editor saves it beside the document in `assets/` and inserts a URL-encoded relative link as one undo step, in local and remote projects. Existing names get numbered suffixes; the folder and clipboard name pattern are in Settings › Editor › General. Non-Markdown editors, text paste and other dropped files keep their behavior.

Fixes #308
2026-10-31 08:48:10 +08:00

195 lines
5.4 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 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));
}
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(())
}
}