Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
e3ea765d
Unverified
提交
e3ea765d
authored
7月 03, 2025
作者:
Will Chen
提交者:
GitHub
7月 03, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Normalize vfs (#558)
上级
8260aa86
显示空白字符变更
内嵌
并排
正在显示
2 个修改的文件
包含
79 行增加
和
90 行删除
+79
-90
problems.spec.ts
e2e-tests/problems.spec.ts
+6
-12
VirtualFilesystem.ts
src/utils/VirtualFilesystem.ts
+73
-78
没有找到文件。
e2e-tests/problems.spec.ts
浏览文件 @
e3ea765d
import
{
test
,
testSkipIfWindows
}
from
"./helpers/test_helper"
;
import
{
test
}
from
"./helpers/test_helper"
;
import
{
expect
}
from
"@playwright/test"
;
import
fs
from
"fs"
;
import
path
from
"path"
;
const
MINIMAL_APP
=
"minimal-with-ai-rules"
;
test
SkipIfWindows
(
"problems auto-fix - enabled"
,
async
({
po
})
=>
{
test
(
"problems auto-fix - enabled"
,
async
({
po
})
=>
{
await
po
.
setUp
();
await
po
.
importApp
(
MINIMAL_APP
);
await
po
.
expectPreviewIframeIsVisible
();
...
...
@@ -18,9 +18,7 @@ testSkipIfWindows("problems auto-fix - enabled", async ({ po }) => {
await
po
.
snapshotMessages
({
replaceDumpPath
:
true
});
});
testSkipIfWindows
(
"problems auto-fix - gives up after 2 attempts"
,
async
({
po
})
=>
{
test
(
"problems auto-fix - gives up after 2 attempts"
,
async
({
po
})
=>
{
await
po
.
setUp
();
await
po
.
importApp
(
MINIMAL_APP
);
await
po
.
expectPreviewIframeIsVisible
();
...
...
@@ -35,12 +33,9 @@ testSkipIfWindows(
po
.
page
.
getByTestId
(
"problem-summary"
).
last
(),
).
toMatchAriaSnapshot
();
await
po
.
snapshotMessages
({
replaceDumpPath
:
true
});
},
);
});
testSkipIfWindows
(
"problems auto-fix - complex delete-rename-write"
,
async
({
po
})
=>
{
test
(
"problems auto-fix - complex delete-rename-write"
,
async
({
po
})
=>
{
await
po
.
setUp
();
await
po
.
importApp
(
MINIMAL_APP
);
await
po
.
expectPreviewIframeIsVisible
();
...
...
@@ -51,8 +46,7 @@ testSkipIfWindows(
await
po
.
snapshotServerDump
(
"all-messages"
,
{
dumpIndex
:
-
1
});
await
po
.
snapshotMessages
({
replaceDumpPath
:
true
});
},
);
});
test
(
"problems auto-fix - disabled"
,
async
({
po
})
=>
{
await
po
.
setUp
({
disableAutoFixProblems
:
true
});
...
...
src/utils/VirtualFilesystem.ts
浏览文件 @
e3ea765d
...
...
@@ -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
*/
p
ublic
writeFile
(
relativePath
:
string
,
content
:
string
):
void
{
p
rotected
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
*/
p
ublic
deleteFile
(
relativePath
:
string
):
void
{
p
rotected
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
*/
p
ublic
renameFile
(
fromPath
:
string
,
toPath
:
string
):
void
{
p
rotected
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
(
from
Absolute
);
this.deletedFiles.add(from
Normalized
);
// If the source file exists in virtual files, move its content
if
(
this
.
virtualFiles
.
has
(
from
Absolute
))
{
const
content
=
this
.
virtualFiles
.
get
(
from
Absolute
)
!
;
this
.
virtualFiles
.
delete
(
from
Absolute
);
this
.
virtualFiles
.
set
(
to
Absolute
,
content
);
if (this.virtualFiles.has(from
Normalized
)) {
const content = this.virtualFiles.get(from
Normalized
)!;
this.virtualFiles.delete(from
Normalized
);
this.virtualFiles.set(to
Normalized
, content);
} else {
// Try to read from actual filesystem
try {
const content = fs.readFileSync(fromAbsolute, "
utf8
");
this
.
virtualFiles
.
set
(
to
Absolute
,
content
);
this.virtualFiles.set(to
Normalized
, 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
(
to
Absolute
);
this.deletedFiles.delete(to
Normalized
);
}
/**
...
...
@@ -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
),
([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);
}
}
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论