Files
openidea/crates/ide-workspace/tests/workspace_edits.rs
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 <107751081+hacbit@users.noreply.github.com>
2026-11-05 02:16:06 +08:00

616 lines
26 KiB
Rust

use ide_core::{ChangeSet, FileEdit, LanguageId, NullSink, Pos, Range, TextChange, TextEdit, WorkspaceEdit};
use ide_core::{IdeEvent, error::ErrorKind};
use ide_workspace::Documents;
use parking_lot::Mutex;
use std::path::Path;
use std::sync::Arc;
fn documents() -> Documents {
Documents::new(Arc::new(NullSink), Box::new(|_, _| LanguageId::plain_text()))
}
fn meta(path: &std::path::Path) -> std::path::PathBuf {
let mut name = path.as_os_str().to_os_string();
name.push(".meta");
name.into()
}
fn unity_project() -> tempfile::TempDir {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("Assets")).unwrap();
std::fs::create_dir_all(tmp.path().join("ProjectSettings")).unwrap();
std::fs::write(tmp.path().join("ProjectSettings/ProjectVersion.txt"), "m_EditorVersion: 2022.3.40f1\n").unwrap();
tmp
}
#[test]
fn unity_rename_chain_and_delete_share_metadata_undo() {
let tmp = unity_project();
let original = tmp.path().join("Assets/Player.cs");
let renamed = tmp.path().join("Assets/Hero.cs");
std::fs::write(&original, "old").unwrap();
std::fs::write(meta(&original), "guid: unchanged").unwrap();
let (docs, events) = recorded_documents();
docs.apply_workspace_edit(&WorkspaceEdit {
files: vec![
FileEdit::Rename { from: original.clone().into(), to: renamed.clone().into() },
FileEdit::Delete { path: renamed.clone().into(), recursive: false },
],
..Default::default()
})
.unwrap();
for path in [&original, &renamed, &meta(&original), &meta(&renamed)] {
assert!(!path.exists(), "{} should be removed", path.display());
}
docs.undo_workspace_edit(undo_id(&events)).unwrap();
assert_eq!(std::fs::read_to_string(&original).unwrap(), "old");
assert_eq!(std::fs::read_to_string(meta(&original)).unwrap(), "guid: unchanged");
assert!(!renamed.exists() && !meta(&renamed).exists());
}
#[test]
fn unity_metadata_conflict_keeps_earlier_edits_unchanged() {
let tmp = unity_project();
let original = tmp.path().join("Assets/Player.cs");
let renamed = tmp.path().join("Assets/Hero.cs");
std::fs::write(&original, "old").unwrap();
std::fs::write(meta(&original), "guid: original").unwrap();
std::fs::write(meta(&renamed), "guid: orphan").unwrap();
assert!(
documents()
.apply_workspace_edit(&WorkspaceEdit {
files: vec![
edit(&original, "new"),
FileEdit::Rename { from: original.clone().into(), to: renamed.clone().into() }
],
..Default::default()
})
.is_err()
);
assert_eq!(std::fs::read_to_string(&original).unwrap(), "old");
assert_eq!(std::fs::read_to_string(meta(&original)).unwrap(), "guid: original");
assert_eq!(std::fs::read_to_string(meta(&renamed)).unwrap(), "guid: orphan");
}
#[test]
fn unity_folder_rename_then_child_rename_preserves_metadata() {
let tmp = unity_project();
let folder = tmp.path().join("Assets/Before");
let moved = tmp.path().join("Assets/After");
std::fs::create_dir(&folder).unwrap();
std::fs::write(meta(&folder), "folder guid").unwrap();
std::fs::write(folder.join("Player.cs"), "old").unwrap();
std::fs::write(meta(&folder.join("Player.cs")), "script guid").unwrap();
let (docs, events) = recorded_documents();
docs.apply_workspace_edit(&WorkspaceEdit {
files: vec![
FileEdit::Rename { from: folder.clone().into(), to: moved.clone().into() },
FileEdit::Rename { from: moved.join("Player.cs").into(), to: moved.join("Hero.cs").into() },
],
..Default::default()
})
.unwrap();
assert_eq!(std::fs::read_to_string(meta(&moved)).unwrap(), "folder guid");
assert_eq!(std::fs::read_to_string(meta(&moved.join("Hero.cs"))).unwrap(), "script guid");
assert!(!meta(&moved.join("Player.cs")).exists());
docs.undo_workspace_edit(undo_id(&events)).unwrap();
assert_eq!(std::fs::read_to_string(meta(&folder.join("Player.cs"))).unwrap(), "script guid");
assert!(!moved.exists() && !meta(&moved).exists());
}
fn edit(path: &Path, text: &str) -> FileEdit {
FileEdit::Edit {
path: path.to_owned().into(),
version: None,
edits: vec![TextEdit { range: Range::new(Pos::new(0, 0), Pos::new(0, 3)), new_text: text.into() }],
}
}
#[test]
fn later_failure_preserves_disk_and_unsaved_buffer() {
let tmp = tempfile::tempdir().unwrap();
let first = tmp.path().join("first.txt");
let missing = tmp.path().join("missing.txt");
std::fs::write(&first, "old\n").unwrap();
let docs = documents();
let info = docs.open(&first).unwrap();
docs.edit(info.id, info.version, ChangeSet { changes: vec![TextChange { from: 0, to: 3, insert: "own".into() }] })
.unwrap();
let result = docs.apply_workspace_edit(&WorkspaceEdit {
files: vec![edit(&first, "new"), edit(&missing, "bad")],
..Default::default()
});
assert!(result.is_err());
assert_eq!(std::fs::read_to_string(&first).unwrap(), "old\n");
let after = docs.snapshot(info.id).unwrap();
assert_eq!(after.text, "own\n");
assert!(after.dirty);
}
#[test]
fn create_conflict_does_not_modify_earlier_files() {
let tmp = tempfile::tempdir().unwrap();
let first = tmp.path().join("first.txt");
let existing = tmp.path().join("exists.txt");
std::fs::write(&first, "old\n").unwrap();
std::fs::write(&existing, "keep").unwrap();
let docs = documents();
let result = docs.apply_workspace_edit(&WorkspaceEdit {
files: vec![edit(&first, "new"), FileEdit::Create { path: existing.clone().into(), overwrite: false }],
..Default::default()
});
assert!(result.is_err());
assert_eq!(std::fs::read_to_string(&first).unwrap(), "old\n");
assert_eq!(std::fs::read_to_string(existing).unwrap(), "keep");
}
fn recorded_documents() -> (Documents, Arc<Mutex<Vec<IdeEvent>>>) {
let events = Arc::new(Mutex::new(Vec::new()));
let sink = events.clone();
(Documents::new(Arc::new(move |event| sink.lock().push(event)), Box::new(|_, _| LanguageId::plain_text())), events)
}
fn undo_id(events: &Mutex<Vec<IdeEvent>>) -> u32 {
events
.lock()
.iter()
.rev()
.find_map(|event| match event {
IdeEvent::WorkspaceEditApplied { id, .. } => Some(*id),
_ => None,
})
.expect("successful transaction has one undo record")
}
#[test]
fn stale_document_version_rejects_the_whole_edit() {
let tmp = tempfile::tempdir().unwrap();
let a = tmp.path().join("a.txt");
let b = tmp.path().join("b.txt");
std::fs::write(&a, "old\n").unwrap();
std::fs::write(&b, "old\n").unwrap();
let docs = documents();
let info = docs.open(&b).unwrap();
docs.edit(info.id, 0, ChangeSet { changes: vec![TextChange { from: 0, to: 0, insert: "own ".into() }] }).unwrap();
let mut second = edit(&b, "new");
if let FileEdit::Edit { version, .. } = &mut second {
*version = Some(0);
}
let result =
docs.apply_workspace_edit(&WorkspaceEdit { files: vec![edit(&a, "new"), second], ..Default::default() });
assert_eq!(result.unwrap_err().kind, ErrorKind::OutOfSync);
assert_eq!(std::fs::read_to_string(&a).unwrap(), "old\n");
assert_eq!(docs.snapshot(info.id).unwrap().text, "own old\n");
assert!(docs.is_dirty(info.id).unwrap());
}
#[test]
fn matching_document_version_is_applied() {
let tmp = tempfile::tempdir().unwrap();
let a = tmp.path().join("a.txt");
std::fs::write(&a, "old\n").unwrap();
let docs = documents();
let info = docs.open(&a).unwrap();
let mut operation = edit(&a, "new");
if let FileEdit::Edit { version, .. } = &mut operation {
*version = Some(info.version as i32);
}
docs.apply_workspace_edit(&WorkspaceEdit { files: vec![operation], ..Default::default() }).unwrap();
assert_eq!(docs.snapshot(info.id).unwrap().text, "new\n");
assert_eq!(std::fs::read_to_string(a).unwrap(), "new\n");
}
#[test]
fn versioned_edit_of_a_closed_file_is_guarded_by_its_preconditions() {
// gopls versions every file of a rename, including ones the editor never opened.
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("closed.txt");
std::fs::write(&path, "old\n").unwrap();
let docs = documents();
let versioned = || {
let mut operation = edit(&path, "new");
if let FileEdit::Edit { version, .. } = &mut operation {
*version = Some(0);
}
WorkspaceEdit { files: vec![operation], ..Default::default() }
};
// Without a snapshot of what the server saw, a version for a closed file can't be trusted.
assert_eq!(docs.apply_workspace_edit(&versioned()).unwrap_err().kind, ErrorKind::OutOfSync);
assert_eq!(std::fs::read_to_string(&path).unwrap(), "old\n");
let mut changed = versioned();
docs.prepare_workspace_edit(&mut changed).unwrap();
std::fs::write(&path, "own\n").unwrap();
assert_eq!(docs.apply_workspace_edit(&changed).unwrap_err().kind, ErrorKind::OutOfSync);
assert_eq!(std::fs::read_to_string(&path).unwrap(), "own\n");
std::fs::write(&path, "old\n").unwrap();
let mut prepared = versioned();
docs.prepare_workspace_edit(&mut prepared).unwrap();
docs.apply_workspace_edit(&prepared).unwrap();
assert_eq!(std::fs::read_to_string(&path).unwrap(), "new\n");
}
#[test]
fn unversioned_preview_rejects_changed_disk_and_dirty_buffers() {
let tmp = tempfile::tempdir().unwrap();
for open in [false, true] {
let path = tmp.path().join(if open { "open.txt" } else { "closed.txt" });
std::fs::write(&path, "old\n").unwrap();
let docs = documents();
let info = open.then(|| docs.open(&path).unwrap());
let mut change = WorkspaceEdit { files: vec![edit(&path, "new")], ..Default::default() };
docs.prepare_workspace_edit(&mut change).unwrap();
if let Some(info) = &info {
docs.edit(
info.id,
info.version,
ChangeSet { changes: vec![TextChange { from: 0, to: 3, insert: "own".into() }] },
)
.unwrap();
} else {
std::fs::write(&path, "own\n").unwrap();
}
let original = change.preconditions.clone();
docs.prepare_workspace_edit(&mut change).unwrap();
assert_eq!(change.preconditions, original, "resolution must not refresh an old preview's guard");
assert_eq!(docs.apply_workspace_edit(&change).unwrap_err().kind, ErrorKind::OutOfSync);
if let Some(info) = info {
assert_eq!(docs.snapshot(info.id).unwrap().text, "own\n");
assert_eq!(std::fs::read_to_string(&path).unwrap(), "old\n");
} else {
assert_eq!(std::fs::read_to_string(&path).unwrap(), "own\n");
}
}
}
#[test]
fn saving_between_preview_and_apply_keeps_the_edit_valid() {
let tmp = tempfile::tempdir().unwrap();
// A new scratch (empty on disk) and an edited file, saved after the edit was computed.
for (name, disk, typed) in [("scratch.c", "", "int\n"), ("a.txt", "old\n", "own")] {
let path = tmp.path().join(name);
std::fs::write(&path, disk).unwrap();
let docs = documents();
let info = docs.open(&path).unwrap();
let replace = if disk.is_empty() { 0 } else { 3 };
docs.edit(
info.id,
info.version,
ChangeSet { changes: vec![TextChange { from: 0, to: replace, insert: typed.into() }] },
)
.unwrap();
let mut change = WorkspaceEdit { files: vec![edit(&path, "new")], ..Default::default() };
docs.prepare_workspace_edit(&mut change).unwrap();
docs.save(info.id).unwrap();
docs.apply_workspace_edit(&change).unwrap_or_else(|e| panic!("{name}: {e}"));
assert!(docs.snapshot(info.id).unwrap().text.starts_with("new"), "{name}");
// A real change after the preview still invalidates it.
let mut change = WorkspaceEdit { files: vec![edit(&path, "xyz")], ..Default::default() };
docs.prepare_workspace_edit(&mut change).unwrap();
let version = docs.snapshot(info.id).unwrap().version;
docs.edit(info.id, version, ChangeSet { changes: vec![TextChange { from: 0, to: 0, insert: "!".into() }] })
.unwrap();
docs.save(info.id).unwrap();
assert!(docs.apply_workspace_edit(&change).is_err(), "{name}");
}
}
#[test]
fn grouped_undo_restores_open_closed_and_unsaved_files() {
let tmp = tempfile::tempdir().unwrap();
let a = tmp.path().join("a.txt");
let b = tmp.path().join("b.txt");
let c = tmp.path().join("c.txt");
for path in [&a, &b, &c] {
std::fs::write(path, "old\r\n").unwrap();
}
let (docs, events) = recorded_documents();
let dirty = docs.open(&a).unwrap();
let clean = docs.open(&b).unwrap();
docs.edit(dirty.id, 0, ChangeSet { changes: vec![TextChange { from: 0, to: 3, insert: "own".into() }] }).unwrap();
let change =
WorkspaceEdit { files: [&a, &b, &c].into_iter().map(|p| edit(p, "new")).collect(), ..Default::default() };
docs.apply_workspace_edit(&change).unwrap();
assert_eq!(docs.snapshot(dirty.id).unwrap().text, "new\n");
assert!(docs.is_dirty(dirty.id).unwrap());
assert_eq!(std::fs::read(&a).unwrap(), b"old\r\n", "unrelated unsaved work is not saved");
assert_eq!(std::fs::read(&c).unwrap(), b"new\r\n");
let opened_later = docs.open(&c).unwrap();
let id = undo_id(&events);
docs.undo_workspace_edit(id).unwrap();
assert_eq!(docs.snapshot(dirty.id).unwrap().text, "own\n");
assert!(docs.is_dirty(dirty.id).unwrap());
for info in [clean, opened_later] {
assert_eq!(docs.snapshot(info.id).unwrap().text, "old\n");
assert!(!docs.is_dirty(info.id).unwrap());
}
for path in [&a, &b, &c] {
assert_eq!(std::fs::read(path).unwrap(), b"old\r\n");
}
assert!(docs.undo_workspace_edit(id).is_err());
}
#[test]
fn undo_refuses_later_changes_and_retains_the_record() {
let tmp = tempfile::tempdir().unwrap();
let a = tmp.path().join("a.txt");
std::fs::write(&a, "old\n").unwrap();
let (docs, events) = recorded_documents();
docs.apply_workspace_edit(&WorkspaceEdit { files: vec![edit(&a, "new")], ..Default::default() }).unwrap();
let id = undo_id(&events);
std::fs::write(&a, "own\n").unwrap();
assert_eq!(docs.undo_workspace_edit(id).unwrap_err().kind, ErrorKind::OutOfSync);
assert_eq!(std::fs::read_to_string(&a).unwrap(), "own\n");
std::fs::write(&a, "new\n").unwrap();
docs.undo_workspace_edit(id).unwrap();
assert_eq!(std::fs::read_to_string(a).unwrap(), "old\n");
}
#[test]
fn ordered_resource_operations_and_binary_deletion_are_undoable() {
let tmp = tempfile::tempdir().unwrap();
let created = tmp.path().join("new/deep/file.txt");
let renamed = tmp.path().join("renamed.txt");
let deleted = tmp.path().join("binary.dat");
let overwritten = tmp.path().join("overwrite.txt");
std::fs::write(&deleted, [0, 255, 1, 128]).unwrap();
std::fs::write(&overwritten, [255, 254, 65, 0]).unwrap();
let (docs, events) = recorded_documents();
let change = WorkspaceEdit {
files: vec![
FileEdit::Create { path: created.clone().into(), overwrite: false },
edit(&created, "hello"),
FileEdit::Rename { from: created.clone().into(), to: renamed.clone().into() },
FileEdit::Delete { path: deleted.clone().into(), recursive: false },
FileEdit::Create { path: overwritten.clone().into(), overwrite: true },
],
..Default::default()
};
docs.apply_workspace_edit(&change).unwrap();
assert_eq!(std::fs::read_to_string(&renamed).unwrap(), "hello");
assert!(!deleted.exists());
assert!(std::fs::read(&overwritten).unwrap().is_empty());
docs.undo_workspace_edit(undo_id(&events)).unwrap();
assert!(!tmp.path().join("new").exists());
assert!(!renamed.exists());
assert_eq!(std::fs::read(deleted).unwrap(), [0, 255, 1, 128]);
assert_eq!(std::fs::read(overwritten).unwrap(), [255, 254, 65, 0]);
}
#[test]
fn nonrecursive_directory_delete_is_rejected_before_any_write() {
let tmp = tempfile::tempdir().unwrap();
let a = tmp.path().join("a.txt");
let dir = tmp.path().join("directory");
std::fs::write(&a, "old\n").unwrap();
std::fs::create_dir(&dir).unwrap();
std::fs::write(dir.join("keep.txt"), "keep").unwrap();
let change = WorkspaceEdit {
files: vec![edit(&a, "new"), FileEdit::Delete { path: dir.clone().into(), recursive: false }],
..Default::default()
};
assert!(documents().apply_workspace_edit(&change).is_err());
assert_eq!(std::fs::read_to_string(a).unwrap(), "old\n");
assert!(dir.join("keep.txt").exists());
}
#[test]
fn external_write_not_yet_seen_by_watcher_is_not_overwritten() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("a.txt");
std::fs::write(&path, "old\n").unwrap();
let docs = documents();
let info = docs.open(&path).unwrap();
std::fs::write(&path, "own\n").unwrap();
let mut change = WorkspaceEdit { files: vec![edit(&path, "new")], ..Default::default() };
docs.prepare_workspace_edit(&mut change).unwrap();
assert_eq!(docs.apply_workspace_edit(&change).unwrap_err().kind, ErrorKind::OutOfSync);
assert_eq!(std::fs::read_to_string(path).unwrap(), "own\n");
assert_eq!(docs.snapshot(info.id).unwrap().text, "old\n");
}
#[cfg(unix)]
#[test]
fn undo_restores_deleted_directory_with_symlinks_and_permissions() {
use std::os::unix::fs::{PermissionsExt, symlink};
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("tree");
std::fs::create_dir(&dir).unwrap();
std::fs::write(dir.join("script"), "old\n").unwrap();
std::fs::set_permissions(dir.join("script"), std::fs::Permissions::from_mode(0o751)).unwrap();
symlink("missing-target", dir.join("link")).unwrap();
let (docs, events) = recorded_documents();
docs.apply_workspace_edit(&WorkspaceEdit {
files: vec![FileEdit::Delete { path: dir.clone().into(), recursive: true }],
..Default::default()
})
.unwrap();
assert!(!dir.exists());
docs.undo_workspace_edit(undo_id(&events)).unwrap();
assert_eq!(std::fs::read(dir.join("script")).unwrap(), b"old\n");
assert_eq!(std::fs::metadata(dir.join("script")).unwrap().permissions().mode() & 0o777, 0o751);
assert_eq!(std::fs::read_link(dir.join("link")).unwrap(), Path::new("missing-target"));
}
#[cfg(unix)]
#[test]
fn preflight_never_writes_through_copied_or_moved_symlinks() {
use std::os::unix::fs::symlink;
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("target.txt");
let link = tmp.path().join("link.txt");
let moved = tmp.path().join("moved.txt");
std::fs::write(&target, "old\n").unwrap();
symlink(&target, &link).unwrap();
let docs = documents();
for files in [
vec![FileEdit::Create { path: link.clone().into(), overwrite: true }],
vec![FileEdit::Rename { from: link.clone().into(), to: moved.clone().into() }, edit(&moved, "new")],
] {
assert!(docs.apply_workspace_edit(&WorkspaceEdit { files, ..Default::default() }).is_err());
assert_eq!(std::fs::read_to_string(&target).unwrap(), "old\n");
assert!(std::fs::symlink_metadata(&link).unwrap().file_type().is_symlink());
assert!(!moved.exists());
}
}
#[test]
fn rename_after_restoring_inplace_typing_saves_but_real_unsaved_work_stays_dirty() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("main.ts");
std::fs::write(&path, "old").unwrap();
let (docs, events) = recorded_documents();
let info = docs.open(&path).unwrap();
for (version, text) in [(0, "own"), (1, "old")] {
docs.edit(info.id, version, ChangeSet { changes: vec![TextChange { from: 0, to: 3, insert: text.into() }] })
.unwrap();
}
assert!(docs.snapshot(info.id).unwrap().dirty);
docs.apply_workspace_edit(&WorkspaceEdit { files: vec![edit(&path, "new")], ..Default::default() }).unwrap();
assert_eq!(std::fs::read_to_string(&path).unwrap(), "new");
assert!(!docs.snapshot(info.id).unwrap().dirty);
let id = undo_id(&events);
docs.undo_workspace_edit(id).unwrap();
assert_eq!(std::fs::read_to_string(&path).unwrap(), "old");
assert!(!docs.snapshot(info.id).unwrap().dirty, "restoring saved bytes must clear a stale dirty flag");
docs.redo_workspace_edit(id).unwrap();
assert_eq!(std::fs::read_to_string(&path).unwrap(), "new");
assert!(!docs.snapshot(info.id).unwrap().dirty);
docs.edit(
info.id,
docs.version(info.id).unwrap(),
ChangeSet { changes: vec![TextChange { from: 3, to: 3, insert: " own".into() }] },
)
.unwrap();
assert_eq!(docs.undo_workspace_edit(id).unwrap_err().kind, ErrorKind::OutOfSync);
}
#[test]
fn duplicate_edits_keep_versions_and_apply_one_replacement() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("main.cpp");
std::fs::write(&path, "old").unwrap();
let docs = documents();
let info = docs.open(&path).unwrap();
let mut versioned = edit(&path, "new_long");
if let FileEdit::Edit { version, .. } = &mut versioned {
*version = Some(info.version as i32);
}
let mut workspace = WorkspaceEdit { files: vec![edit(&path, "new_long"), versioned], ..Default::default() };
workspace.deduplicate();
assert_eq!(workspace.files.len(), 1);
assert!(matches!(workspace.files[0], FileEdit::Edit { version: Some(0), .. }));
docs.apply_workspace_edit(&workspace).unwrap();
assert_eq!(std::fs::read_to_string(&path).unwrap(), "new_long");
let mut conflicting = workspace.files[0].clone();
if let FileEdit::Edit { version, .. } = &mut conflicting {
*version = Some(99);
}
workspace.files.push(conflicting);
workspace.deduplicate();
assert_eq!(workspace.files.len(), 2, "deduplication must not discard incompatible versions");
assert_eq!(docs.apply_workspace_edit(&workspace).unwrap_err().kind, ErrorKind::OutOfSync);
assert_eq!(std::fs::read_to_string(path).unwrap(), "new_long");
}
#[test]
fn undo_accepts_typing_that_was_reverted_to_the_saved_refactoring() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("main.ts");
std::fs::write(&path, "old").unwrap();
let (docs, events) = recorded_documents();
let info = docs.open(&path).unwrap();
docs.apply_workspace_edit(&WorkspaceEdit { files: vec![edit(&path, "new")], ..Default::default() }).unwrap();
let id = undo_id(&events);
for text in ["own", "new"] {
docs.edit(
info.id,
docs.version(info.id).unwrap(),
ChangeSet { changes: vec![TextChange { from: 0, to: 3, insert: text.into() }] },
)
.unwrap();
}
assert!(docs.snapshot(info.id).unwrap().dirty);
docs.undo_workspace_edit(id).unwrap();
assert_eq!(std::fs::read_to_string(path).unwrap(), "old");
assert_eq!(docs.snapshot(info.id).unwrap().text, "old");
assert!(!docs.snapshot(info.id).unwrap().dirty);
}
#[cfg(unix)]
#[test]
fn replacing_a_symlink_then_editing_the_new_file_never_writes_its_old_target() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("precious.txt");
let link = tmp.path().join("new.txt");
std::fs::write(&target, "old target\n").unwrap();
std::os::unix::fs::symlink(&target, &link).unwrap();
let (docs, events) = recorded_documents();
let target_doc = docs.open(&target).unwrap();
let mut edit = WorkspaceEdit {
files: vec![
FileEdit::Delete { path: link.clone().into(), recursive: false },
FileEdit::Create { path: link.clone().into(), overwrite: false },
FileEdit::Edit {
path: link.clone().into(),
version: None,
edits: vec![TextEdit { range: Range::point(Pos::new(0, 0)), new_text: "new file\n".into() }],
},
],
..Default::default()
};
docs.prepare_workspace_edit(&mut edit).unwrap();
docs.apply_workspace_edit(&edit).unwrap();
assert_eq!(std::fs::read_to_string(&target).unwrap(), "old target\n");
assert_eq!(docs.snapshot(target_doc.id).unwrap().text, "old target\n");
assert_eq!(std::fs::read_to_string(&link).unwrap(), "new file\n");
assert!(!std::fs::symlink_metadata(&link).unwrap().file_type().is_symlink());
let id = undo_id(&events);
docs.undo_workspace_edit(id).unwrap();
assert_eq!(std::fs::read_link(&link).unwrap(), target);
docs.redo_workspace_edit(id).unwrap();
assert_eq!(std::fs::read_to_string(&target).unwrap(), "old target\n");
assert_eq!(std::fs::read_to_string(&link).unwrap(), "new file\n");
}
#[cfg(unix)]
#[test]
fn replacing_a_directory_symlink_does_not_resolve_later_child_paths_through_its_old_target() {
for alias in 0..3 {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("precious");
let link = tmp.path().join("new");
std::fs::create_dir(&target).unwrap();
std::fs::write(target.join("file.txt"), "old target\n").unwrap();
std::os::unix::fs::symlink(&target, &link).unwrap();
let alternate = tmp.path().join("alternate");
std::os::unix::fs::symlink(tmp.path(), &alternate).unwrap();
let shortcut = tmp.path().join("shortcut");
std::os::unix::fs::symlink(&link, &shortcut).unwrap();
let edited = match alias {
0 => link.join("file.txt"),
1 => alternate.join("new/file.txt"),
_ => shortcut.join("file.txt"),
};
let docs = documents();
let mut edit = WorkspaceEdit {
files: vec![
FileEdit::Delete { path: link.clone().into(), recursive: false },
FileEdit::Create { path: link.join("file.txt").into(), overwrite: false },
FileEdit::Edit {
path: edited.into(),
version: None,
edits: vec![TextEdit { range: Range::point(Pos::new(0, 0)), new_text: "new file\n".into() }],
},
],
..Default::default()
};
docs.prepare_workspace_edit(&mut edit).unwrap();
docs.apply_workspace_edit(&edit).unwrap();
assert_eq!(std::fs::read_to_string(target.join("file.txt")).unwrap(), "old target\n");
assert_eq!(std::fs::read_to_string(link.join("file.txt")).unwrap(), "new file\n");
}
}