feat(core): the file explorer, text editor and image viewer's file operations after Java, measured against Java

FileCases does what the windows of FILIUS' file explorer and text editor
do to a host's files: new folder, delete, copy, cut and paste, rename,
import, save and save as. The core's filePrograms.ts does the same on the
node's file system; the file system renames an entry in its place, as
Java's tree does.

Departures from Java: a folder name that comes to no name makes nothing,
a paste onto a name there already is refused, a rename to a name with a
slash or blanks at its ends is refused and one of the case only is done,
save as and import onto a folder's name are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 11:59:11 +00:00
co-authored by Claude Opus 5.5
parent f622959ecc
commit 9d42b90157
13 changed files with 7653 additions and 3 deletions
@@ -343,6 +343,32 @@ export class VirtualFileSystem {
return this.existingPath(destination);
}
/**
* Java's rename (Datei.setName, DefaultMutableTreeNode.setUserObject): the
* entry keeps its place among its folder's entries, also when only its
* case changes; another entry of the folder may not have the name yet.
* Returns the new path.
*/
renameInPlace(rawPath: string, newName: string): string {
if (newName.includes('/') || newName.includes('\\')) fail('invalidPathComponent', newName);
const sourcePath = this.existingPath(rawPath);
if (sourcePath === '/') fail('cannotMutateRoot');
const destination = normalizedAbsolutePath(join(parentPath(sourcePath), newName));
if (entryName(destination) !== newName) fail('invalidPathComponent', newName);
const taken = this.existingPathIfPresent(destination);
if (taken !== undefined && !swiftEqual(taken, sourcePath)) fail('itemAlreadyExists', destination);
this.transaction((candidate) => {
const store: Store = new Map();
for (const [key, entry] of candidate.store) {
const inside = entry.path === sourcePath || entry.path.startsWith(`${sourcePath}/`);
const path = inside ? destination + entry.path.slice(sourcePath.length) : entry.path;
store.set(inside ? keyOf(path) : key, { path, content: entry.content });
}
candidate.store = store;
});
return destination;
}
deleteItem(rawPath: string, recursive = false): void {
const path = this.existingPath(rawPath);
if (path === '/') fail('cannotMutateRoot');