Normalize vfs (#558)

This commit is contained in:
Will Chen
2025-07-03 12:15:58 -07:00
committed by GitHub
parent 8260aa86e2
commit e3ea765de8
2 changed files with 98 additions and 109 deletions

View File

@@ -5,6 +5,7 @@ import {
getDyadRenameTags,
getDyadDeleteTags,
} from "../ipc/processors/response_processor";
import { normalizePath } from "../ipc/processors/normalizePath";
import log from "electron-log";
@@ -39,7 +40,39 @@ export abstract class BaseVirtualFileSystem {
protected baseDir: string;
constructor(baseDir: string) {
this.baseDir = baseDir;
this.baseDir = path.resolve(baseDir);
}
/**
* Normalize path for consistent cross-platform behavior
*/
private normalizePathForKey(filePath: string): string {
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.resolve(this.baseDir, filePath);
// Normalize separators and handle case-insensitive Windows paths
const normalized = normalizePath(path.normalize(absolutePath));
// Intentionally do NOT lowercase for Windows which is case-insensitive
// because this avoids issues with path comparison.
//
// This is a trade-off and introduces a small edge case where
// e.g. foo.txt and Foo.txt are treated as different files by the VFS
// even though Windows treats them as the same file.
//
// This should be a pretty rare occurence and it's not worth the extra
// complexity to handle it.
return normalized;
}
/**
* Convert normalized path back to platform-appropriate format
*/
private denormalizePath(normalizedPath: string): string {
return process.platform === "win32"
? normalizedPath.replace(/\//g, "\\")
: normalizedPath;
}
/**
@@ -69,43 +102,49 @@ export abstract class BaseVirtualFileSystem {
/**
* Write a file to the virtual filesystem
*/
public writeFile(relativePath: string, content: string): void {
protected writeFile(relativePath: string, content: string): void {
const absolutePath = path.resolve(this.baseDir, relativePath);
this.virtualFiles.set(absolutePath, content);
const normalizedKey = this.normalizePathForKey(absolutePath);
this.virtualFiles.set(normalizedKey, content);
// Remove from deleted files if it was previously deleted
this.deletedFiles.delete(absolutePath);
this.deletedFiles.delete(normalizedKey);
}
/**
* Delete a file from the virtual filesystem
*/
public deleteFile(relativePath: string): void {
protected deleteFile(relativePath: string): void {
const absolutePath = path.resolve(this.baseDir, relativePath);
this.deletedFiles.add(absolutePath);
const normalizedKey = this.normalizePathForKey(absolutePath);
this.deletedFiles.add(normalizedKey);
// Remove from virtual files if it exists there
this.virtualFiles.delete(absolutePath);
this.virtualFiles.delete(normalizedKey);
}
/**
* Rename a file in the virtual filesystem
*/
public renameFile(fromPath: string, toPath: string): void {
protected renameFile(fromPath: string, toPath: string): void {
const fromAbsolute = path.resolve(this.baseDir, fromPath);
const toAbsolute = path.resolve(this.baseDir, toPath);
const fromNormalized = this.normalizePathForKey(fromAbsolute);
const toNormalized = this.normalizePathForKey(toAbsolute);
// Mark old file as deleted
this.deletedFiles.add(fromAbsolute);
this.deletedFiles.add(fromNormalized);
// If the source file exists in virtual files, move its content
if (this.virtualFiles.has(fromAbsolute)) {
const content = this.virtualFiles.get(fromAbsolute)!;
this.virtualFiles.delete(fromAbsolute);
this.virtualFiles.set(toAbsolute, content);
if (this.virtualFiles.has(fromNormalized)) {
const content = this.virtualFiles.get(fromNormalized)!;
this.virtualFiles.delete(fromNormalized);
this.virtualFiles.set(toNormalized, content);
} else {
// Try to read from actual filesystem
try {
const content = fs.readFileSync(fromAbsolute, "utf8");
this.virtualFiles.set(toAbsolute, content);
this.virtualFiles.set(toNormalized, content);
} catch (error) {
// If we can't read the source file, we'll let the consumer handle it
logger.warn(
@@ -116,7 +155,7 @@ export abstract class BaseVirtualFileSystem {
}
// Remove destination from deleted files if it was previously deleted
this.deletedFiles.delete(toAbsolute);
this.deletedFiles.delete(toNormalized);
}
/**
@@ -124,10 +163,15 @@ export abstract class BaseVirtualFileSystem {
*/
public getVirtualFiles(): VirtualFile[] {
return Array.from(this.virtualFiles.entries()).map(
([absolutePath, content]) => ({
path: path.relative(this.baseDir, absolutePath),
content,
}),
([normalizedKey, content]) => {
// Convert normalized key back to relative path
const denormalizedPath = this.denormalizePath(normalizedKey);
return {
path: path.relative(this.baseDir, denormalizedPath),
content,
};
},
);
}
@@ -135,84 +179,35 @@ export abstract class BaseVirtualFileSystem {
* Get all deleted file paths (relative to base directory)
*/
public getDeletedFiles(): string[] {
return Array.from(this.deletedFiles).map((absolutePath) =>
path.relative(this.baseDir, absolutePath),
);
}
/**
* Get all files that should be considered (existing + virtual - deleted)
*/
public getAllFiles(): string[] {
const allFiles = new Set<string>();
// Add virtual files
for (const [absolutePath] of this.virtualFiles.entries()) {
allFiles.add(path.relative(this.baseDir, absolutePath));
}
// Add existing files (this is a simplified version - in practice you might want to scan the directory)
// This method is mainly for getting the current state, consumers can combine with directory scanning
return Array.from(allFiles);
}
/**
* Check if a file has been modified in the virtual filesystem
*/
public isFileModified(filePath: string): boolean {
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.resolve(this.baseDir, filePath);
return (
this.virtualFiles.has(absolutePath) || this.deletedFiles.has(absolutePath)
);
}
/**
* Clear all virtual changes
*/
public clear(): void {
this.virtualFiles.clear();
this.deletedFiles.clear();
}
/**
* Get the base directory
*/
public getBaseDir(): string {
return this.baseDir;
return Array.from(this.deletedFiles).map((normalizedKey) => {
// Convert normalized key back to relative path
const denormalizedPath = this.denormalizePath(normalizedKey);
return path.relative(this.baseDir, denormalizedPath);
});
}
/**
* Check if a file is deleted in the virtual filesystem
*/
protected isDeleted(filePath: string): boolean {
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.resolve(this.baseDir, filePath);
return this.deletedFiles.has(absolutePath);
const normalizedKey = this.normalizePathForKey(filePath);
return this.deletedFiles.has(normalizedKey);
}
/**
* Check if a file exists in virtual files
*/
protected hasVirtualFile(filePath: string): boolean {
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.resolve(this.baseDir, filePath);
return this.virtualFiles.has(absolutePath);
const normalizedKey = this.normalizePathForKey(filePath);
return this.virtualFiles.has(normalizedKey);
}
/**
* Get virtual file content
*/
protected getVirtualFileContent(filePath: string): string | undefined {
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.resolve(this.baseDir, filePath);
return this.virtualFiles.get(absolutePath);
const normalizedKey = this.normalizePathForKey(filePath);
return this.virtualFiles.get(normalizedKey);
}
}