mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 00:07:29 +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 <107751081+hacbit@users.noreply.github.com>
616 lines
26 KiB
Rust
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");
|
|
}
|
|
}
|