文件file模块分析
# 简介
这个模块主要是包括file-tree-next
和file-service
两大子模块;还关联到工作空间workspace
模块;
@EffectDomain(pkgJson.name)
@ModuleDependencies([WorkspaceModule])
export class FileTreeNextModule extends BrowserModule {}
2
3
# 初始化
core/packages/file-service/src/browser/file-service-contribution.ts
@Domain(ClientAppContribution)
export class FileServiceContribution implements ClientAppContribution {
@Autowired(IFileServiceClient)
protected readonly fileSystem: FileServiceClient;
@Autowired(IDiskFileProvider)
private diskFileServiceProvider: IDiskFileProvider;
@Autowired(FsProviderContribution)
contributionProvider: ContributionProvider<FsProviderContribution>;
constructor() {
// 初始化资源读取逻辑,需要在最早初始化时注册
// 否则后续注册的 debug\user_stroage 等将无法正常使用
this.fileSystem.registerProvider(Schemes.file, this.diskFileServiceProvider);
}
async initialize() {
const fsProviderContributions = this.contributionProvider.getContributions();
for (const contribution of fsProviderContributions) {
contribution.registerProvider && (await contribution.registerProvider(this.fileSystem));
}
for (const contribution of fsProviderContributions) {
contribution.onFileServiceReady && (await contribution.onFileServiceReady());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 文件树显示
# collectViewComponent
core/packages/file-tree-next/src/browser/file-tree-contribution.ts
async onStart() {
this.viewsRegistry.registerViewWelcomeContent(RESOURCE_VIEW_ID, {
content: formatLocalize('welcome-view.noFolderHelp', FILE_COMMANDS.OPEN_FOLDER.id),
group: ViewContentGroups.Open,
order: 1,
});
this.mainLayoutService.collectViewComponent(
{
id: RESOURCE_VIEW_ID,
name: this.getWorkspaceTitle(),
weight: 3,
priority: 9,
collapsed: false,
component: FileTree,
},
EXPLORER_CONTAINER_ID,
);
// 监听工作区变化更新标题
this.workspaceService.onWorkspaceLocationChanged(() => {
const handler = this.mainLayoutService.getTabbarHandler(EXPLORER_CONTAINER_ID);
if (handler) {
handler.updateViewTitle(RESOURCE_VIEW_ID, this.getWorkspaceTitle());
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 树数据初始化【核心】
core/packages/file-tree-next/src/browser/file-tree-contribution.ts
initialize() {
// 等待排除配置初始化结束后再初始化文件树
this.workspaceService.initFileServiceExclude().then(async () => {
await this.fileTreeService.init();
this.fileTreeModelService.initTreeModel();
});s
}
2
3
4
5
6
7
# initFileServiceExclude
core/packages/workspace/src/browser/workspace-service.ts
public async initFileServiceExclude() {
await this.setFileServiceExcludes();
}
protected async setFileServiceExcludes() {
const watchExcludeName = 'files.watcherExclude';
const filesExcludeName = 'files.exclude';
await this.preferenceService.ready;
await this.fileServiceClient.setWatchFileExcludes(this.getFlattenExcludes(watchExcludeName));
await this.fileServiceClient.setFilesExcludes(
this.getFlattenExcludes(filesExcludeName),
this._roots.map((stat) => stat.uri),
);
this.onWorkspaceFileExcludeChangeEmitter.fire();
}
protected getFlattenExcludes(name: string): string[] {
const excludes: string[] = [];
const fileExcludes = this.preferenceService.get<any>(name);
if (fileExcludes) {
for (const key of Object.keys(fileExcludes)) {
if (fileExcludes[key]) {
excludes.push(key);
}
}
}
return excludes;
}
protected readonly onWorkspaceFileExcludeChangeEmitter = new Emitter<void>();
get onWorkspaceFileExcludeChanged(): Event<void> {
return this.onWorkspaceFileExcludeChangeEmitter.event;
}
this.toDispose.push(
this.workspaceService.onWorkspaceFileExcludeChanged(() => {
this.refresh();
}),
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# fileTreeService.init
core/packages/file-tree-next/src/browser/file-tree.service.ts
async init() {
this._roots = await this.workspaceService.roots;
await this.preferenceService.ready;
this._baseIndent = this.preferenceService.get('explorer.fileTree.baseIndent') || 8;
this._indent = this.preferenceService.get('explorer.fileTree.indent') || 8;
this._isCompactMode = this.preferenceService.get('explorer.compactFolders') as boolean;
this.toDispose.push(
this.workspaceService.onWorkspaceChanged((roots) => {
this._roots = roots;
// 切换工作区时更新文件树
const newRootUri = new URI(roots[0].uri);
const newRoot = new Directory(
this,
undefined,
newRootUri,
newRootUri.displayName,
roots[0],
this.fileTreeAPI.getReadableTooltip(newRootUri),
);
this._root = newRoot;
this.onWorkspaceChangeEmitter.fire(newRoot);
this.refresh();
}),
);
this.toDispose.push(
this.workspaceService.onWorkspaceFileExcludeChanged(() => {
this.refresh();
}),
);
this.toDispose.push(
Disposable.create(() => {
this._roots = null;
}),
);
this.toDispose.push(
this.corePreferences.onPreferenceChanged((change) => {
if (change.preferenceName === 'explorer.fileTree.baseIndent') {
this._baseIndent = (change.newValue as number) || 8;
this.onTreeIndentChangeEmitter.fire({
indent: this.indent,
baseIndent: this.baseIndent,
});
} else if (change.preferenceName === 'explorer.fileTree.indent') {
this._indent = (change.newValue as number) || 8;
this.onTreeIndentChangeEmitter.fire({
indent: this.indent,
baseIndent: this.baseIndent,
});
} else if (change.preferenceName === 'explorer.compactFolders') {
this._isCompactMode = change.newValue as boolean;
this.refresh();
}
}),
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# fileTreeModelService.initTreeModel
core/packages/file-tree-next/src/browser/services/file-tree-model.service.ts
async initTreeModel() {
this._initTreeModelReady = false;
// 根据是否为多工作区创建不同根节点
const root = (await this.fileTreeService.resolveChildren())[0];
if (!root) {
this._whenReady.resolve();
return;
}
this._treeModel = this.injector.get<any>(FileTreeModel, [root]);
this.initDecorations(root);
// _dndService依赖装饰器逻辑加载
this._dndService = this.injector.get<any>(DragAndDropService, [this]);
// 确保文件树响应刷新操作时无正在操作的 CollapsedAll 和 Location
this.disposableCollection.push(
this.fileTreeService.requestFlushEventSignalEvent(async () => await this.canHandleRefreshEvent()),
);
// 等待初次加载完成后再初始化当前的 treeStateWatcher, 只加载可见的节点
this.treeStateWatcher = this._treeModel.getTreeStateWatcher(true);
this.disposableCollection.push(
this.fileTreeService.onNodeRefreshed(() => {
if (!this.initTreeModelReady) {
return;
}
if (!this.refreshedActionDelayer.isTriggered) {
this.refreshedActionDelayer.cancel();
}
this.refreshedActionDelayer.trigger(async () => {
// 当无选中节点时,选中编辑器中激活的节点
if (this.selectedFiles.length === 0) {
const currentEditor = this.editorService.currentEditor;
if (currentEditor && currentEditor.currentUri) {
this.location(currentEditor.currentUri);
}
}
if (!this.fileTreeService.isCompactMode) {
this._activeUri = null;t
}
});
}),
);
//***************
this._explorerStorage = await this.storageProvider(STORAGE_NAMESPACE.EXPLORER);
// 获取上次文件树的状态
const snapshot = this.explorerStorage.get<ISerializableState>(FileTreeModelService.FILE_TREE_SNAPSHOT_KEY);
if (snapshot) {
// 初始化时。以右侧编辑器打开的文件进行定位
await this.loadFileTreeSnapshot(snapshot);
}
// 完成首次文件树快照恢复后再进行 Tree 状态变化的更新
this.disposableCollection.push(
this.treeStateWatcher.onDidChange(() => {
if (!this._initTreeModelReady) {
return;
}
const snapshot = this.explorerStorage.get<any>(FileTreeModelService.FILE_TREE_SNAPSHOT_KEY);
const currentTreeSnapshot = this.treeStateWatcher.snapshot();
this.explorerStorage.set(FileTreeModelService.FILE_TREE_SNAPSHOT_KEY, {
...snapshot,
...currentTreeSnapshot,
});
}),
);
//这两个方法要点;一个启动数据监听;
await this.fileTreeService.startWatchFileEvent();
//一个获取到数据后回显示到树组件上;
this.onFileTreeModelChangeEmitter.fire(this._treeModel);
this._whenReady.resolve();
this._initTreeModelReady = true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# fileTreeService.resolveChildren
core/packages/file-tree-next/src/browser/file-tree.service.ts
async resolveChildren(parent?: Directory) {
let children: (File | Directory)[] = [];
if (!parent) {
// 加载根目录
if (!this._roots) {
this._roots = await this.workspaceService.roots;
}
if (this.isMultipleWorkspace) {
const rootUri = new URI(this.workspaceService.workspace?.uri);
let rootName = rootUri.displayName;
rootName = rootName.slice(0, rootName.lastIndexOf('.'));
const fileStat = {
...this.workspaceService.workspace,
isDirectory: true,
} as FileStat;
const root = new Directory(
this,
undefined,
rootUri,
rootName,
fileStat,
this.fileTreeAPI.getReadableTooltip(rootUri),
);
// 创建Root节点并引入root文件目录
this.root = root;
return [root];
} else {
if (this._roots.length > 0) {
children = await (await this.fileTreeAPI.resolveChildren(this, this._roots[0])).children;
children.forEach((child) => {
// 根据workspace更新Root名称
const rootName = this.workspaceService.getWorkspaceName(child.uri);
if (rootName && rootName !== child.name) {
(child as Directory).updateMetaData({
name: rootName,
});
}
});
this.watchFilesChange(new URI(this._roots[0].uri));
this.root = children[0] as Directory;
return children;
}
}
} else{
.....
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# fileTreeAPI.resolveChildren
core/packages/file-tree-next/src/browser/services/file-tree-api.service.ts
async resolveChildren(tree: IFileTreeService, path: string | FileStat, parent?: Directory, compact?: boolean) {
let file: FileStat | undefined;
if (!this.userhomePath) {
const userhome = await this.fileServiceClient.getCurrentUserHome();
if (userhome) {
this.userhomePath = new URI(userhome.uri);
}
}
if (typeof path === 'string') {
file = await this.fileServiceClient.getFileStat(path);
} else {
file = await this.fileServiceClient.getFileStat(path.uri);
}
if (file) {
if (file.children?.length === 1 && file.children[0].isDirectory && compact) {
const parentURI = new URI(file.children[0].uri);
if (!!parent && parent.parent) {
const parentName = (parent.parent as Directory).uri.relative(parentURI)?.toString();
if (parentName && parentName !== parent.name) {
parent.updateMetaData({
name: parentName,
uri: parentURI,
fileStat: file.children[0],
tooltip: this.getReadableTooltip(parentURI),
});
}
}
return await this.resolveChildren(tree, file.children[0].uri, parent, compact);
} else {
// 为文件树节点新增isInSymbolicDirectory属性,用于探测节点是否处于软链接文件中
const filestat = {
...file,
isInSymbolicDirectory: parent?.filestat.isSymbolicLink || parent?.filestat.isInSymbolicDirectory,
};
return {
children: this.toNodes(tree, filestat, parent),
filestat,
};
}
} else {
return {
children: [],
filestat: null,
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# getFileStat【最终文件处理】
core/packages/file-service/src/browser/file-service-client.ts
/**
* 提供一个方法让集成方对该方法进行复写。
* 如双容器架构中对 IDE 容器读取不到的研发容器目录进行 scheme 替换,让插件提供提供的 fs-provider 去读取
*/
protected convertUri(uri: string | Uri): URI {
const _uri = new URI(uri);
if (!_uri.scheme) {
throw new Error(`没有设置 scheme: ${uri}`);
}
return _uri;
}
private async getProvider<T extends string>(
scheme: T,
): Promise<T extends 'file' ? IDiskFileProvider : FileSystemProvider>;
private async getProvider(scheme: string): Promise<IDiskFileProvider | FileSystemProvider> {
if (this._providerChanged.has(scheme)) {
// 让相关插件启动完成 (3秒超时), 此处防止每次都发,仅在相关scheme被影响时才尝试激活插件
await this.eventBus.fireAndAwait(new ExtensionActivateEvent({ topic: 'onFileSystem', data: scheme }), {
timeout: 3000,
});
this._providerChanged.delete(scheme);
}
const provider = this.fsProviders.get(scheme);
if (!provider) {
throw new Error(`Not find ${scheme} provider.`);
}
return provider;
}
async getFileStat(uri: string, withChildren = true) {
const _uri = this.convertUri(uri);
const provider = await this.getProvider(_uri.scheme);
try {
const stat = await provider.stat(_uri.codeUri);
if (!stat) {
throw FileSystemError.FileNotFound(_uri.codeUri.toString(), 'File not found.');
}
return this.filterStat(stat, withChildren);
} catch (err) {
if (FileSystemError.FileNotFound.is(err)) {
return undefined;
}
}
}
private filterStat(stat?: FileStat, withChildren = true) {
if (!stat) {
return;
}
if (this.isExclude(stat.uri)) {
return;
}
// 这里传了 false 就走不到后面递归逻辑了
if (stat.children && withChildren) {
stat.children = this.filterStatChildren(stat.children);
}
return stat;
}
private filterStatChildren(children: FileStat[]) {
const list: FileStat[] = [];
children.forEach((child) => {
if (this.isExclude(child.uri)) {
return false;
}
const state = this.filterStat(child);
if (state) {
list.push(state);
}
});
return list;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 监听/树查找
core/packages/file-tree-next/src/browser/file-tree.service.ts
if (this._roots.length > 0) {
children = await (await this.fileTreeAPI.resolveChildren(this, this._roots[0])).children;
children.forEach((child) => {
// 根据workspace更新Root名称
const rootName = this.workspaceService.getWorkspaceName(child.uri);
if (rootName && rootName !== child.name) {
(child as Directory).updateMetaData({
name: rootName,
});
}
});
this.watchFilesChange(new URI(this._roots[0].uri));
this.root = children[0] as Directory;
return children;
}
public startWatchFileEvent() {
this._readyToWatch = true;
return Promise.all(
this._watchRootsQueue.map(async (uri) => {
await this.watchFilesChange(uri);
}),
);
}
private isFileContentChanged(change: FileChange): boolean {
return change.type === FileChangeType.UPDATED && this.isContentFile(this.getNodeByPathOrUri(change.uri));
}
private getAffectedChanges(changes: FileChange[]): FileChange[] {
const affectChange: FileChange[] = [];
for (const change of changes) {
const isFile = this.isFileContentChanged(change);
if (!isFile) {
affectChange.push(change);
}
}
return affectChange;
}
private async onFilesChanged(changes: FileChange[]) {
const nodes = await this.getAffectedNodes(this.getAffectedChanges(changes));
if (nodes.length > 0) {
this.effectedNodes = this.effectedNodes.concat(nodes);
} else if (!(nodes.length > 0) && this.isRootAffected(changes)) {
this.effectedNodes.push(this.root as Directory);
}
// 文件事件引起的刷新进行队列化处理,每 200 ms 处理一次刷新任务
return this.refreshThrottler.queue(this.doDelayRefresh.bind(this));
}
async watchFilesChange(uri: URI) {
if (!this._readyToWatch) {
this._watchRootsQueue.push(uri);
return;
}
const watcher = await this.fileServiceClient.watchFileChanges(uri);
this.toDispose.push(watcher);
this.toDispose.push(
watcher.onFilesChanged((changes: FileChange[]) => {
this.onFilesChanged(changes);
}),
);
this._fileServiceWatchers.set(uri.toString(), watcher);
}
public startWatchFileEvent() {
this._readyToWatch = true;
return Promise.all(
this._watchRootsQueue.map(async (uri) => {
await this.watchFilesChange(uri);
}),
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# onFileTreeModelChange
get onFileTreeModelChange(): Event<TreeModel> {
return this.onFileTreeModelChangeEmitter.event;
}
await this.fileTreeService.startWatchFileEvent();
this.onFileTreeModelChangeEmitter.fire(this._treeModel);
2
3
4
5
6
# 树页面回显数据
通过回显到树节点页面;core/packages/file-tree-next/src/browser/file-tree.tsx
useEffect(() => {
if (isReady) {
// 首次初始化完成时,监听后续变化,适配工作区变化事件
// 监听工作区变化
fileTreeModelService.onFileTreeModelChange(async (treeModel) => {
setIsLoading(true);
if (treeModel) {
// 确保数据初始化完毕,减少初始化数据过程中多次刷新视图
await treeModel.root.ensureLoaded();
}
setModel(treeModel);
setIsLoading(false);
});
}
}, [isReady]);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ensureLoaded 父节点加载孩子
core/packages/components/src/recycle-tree/tree/TreeNode.ts
/**
* 确保此“目录”的子级已加载(不影响“展开”状态)
* 如果子级已经加载,则返回的Promise将立即解决
* 否则,将发出重新加载请求并返回Promise
* 一旦返回的Promise.resolve,“CompositeTreeNode#children” 便可以访问到对于节点
*/
public async ensureLoaded(token?: CancellationToken) {
if (this._children) {
return;
}
return await this.hardReloadChildren(token);
}
/**
* 加载子节点信息
* 当返回值为 true 时,正常加载完子节点并同步到数据结构中
* 返回值为 false 时,加载节点的过程被中断
*
* @memberof CompositeTreeNode
*/
public async hardReloadChildren(token?: CancellationToken) {
let rawItems;
try {
rawItems = (await this._tree.resolveChildren(this)) || [];
} catch (e) {
rawItems = [];
}
// 当获取到新的子节点时,如果当前节点正处于非展开状态时,忽略后续裁切逻辑
// 后续的 expandBranch 也不应该被响应
if (!this.expanded || token?.isCancellationRequested) {
return false;
}
const expandedChilds: CompositeTreeNode[] = [];
const flatTree = new Array(rawItems.length);
const tempChildren = new Array(rawItems.length);
for (let i = 0; i < rawItems.length; i++) {
const child = rawItems[i];
// 如果存在上一次缓存的节点,则使用缓存节点的 ID
(child as TreeNode).id = TreeNode.getIdByPath(child.path) || (child as TreeNode).id;
tempChildren[i] = child;
TreeNode.setIdByPath(child.path, child.id);
if (CompositeTreeNode.is(child) && child.expanded) {
if (!(child as CompositeTreeNode).children) {
await (child as CompositeTreeNode).resolveChildrens(token);
}
if (token?.isCancellationRequested) {
return false;
}
expandedChilds.push(child as CompositeTreeNode);
}
}
tempChildren.sort(this._tree.sortComparator || CompositeTreeNode.defaultSortComparator);
for (let i = 0; i < rawItems.length; i++) {
flatTree[i] = tempChildren[i].id;
}
if (this.children) {
// 重置节点分支
this.shrinkBranch(this);
}
if (this.children) {
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
(child as any).dispose();
}
}
for (let i = 0; i < tempChildren.length; i++) {
this.updateTreeNodeCache(tempChildren[i]);
}
this._children = tempChildren;
this._branchSize = flatTree.length;
this.setFlattenedBranch(flatTree);
for (let i = 0; i < expandedChilds.length; i++) {
const child = expandedChilds[i];
(child as CompositeTreeNode).expandBranch(child, true);
}
// 清理上一次监听函数
if (typeof this.watchTerminator === 'function') {
this.watchTerminator(this.path);
}
this.watchTerminator = this.watcher.onWatchEvent(this.path, this.handleWatchEvent);
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# 相关使用规范
准备
- 框架中服务分为运行在浏览器环境的 前端服务(frontService) 与运行在 node 环境的 后端服务(backService),服务在两端的实现方式是一致的
- 目前在
tools/dev-tool
中的启动逻辑中完成了服务的注册和获取逻辑,在具体功能模块中无需关心具体的通信注册获取逻辑
后端服务(backService) 后端服务(backService) 即在 Web Server 暴露的能力,类似 web 应用框架中 controller 提供的请求响应逻辑
- 注册服务
packages/file-service/src/node/index.ts
import { FileSystemNodeOptions, FileService } from './file-service';
import { servicePath } from '../common/index';
export class FileServiceModule extends NodeModule {
providers = [{ token: 'FileServiceOptions', useValue: FileSystemNodeOptions.DEFAULT }];
backServices = [
{
servicePath,
token: FileService,
},
];
}
2
3
4
5
6
7
8
9
10
11
12
13
14
例如在 file-service 模块中,通过定义 backServices
数组,传递模块提供的后端服务,servicePath
为前端模块引用的服务地址,以及对应服务的注入 token
- 服务调用
packages/file-tree/src/browser/index.ts
import { servicePath as FileServicePath } from '@opensumi/ide-file-service';
@Injectable()
export class FileTreeModule extends BrowserModule {
providers: Provider[] = [createFileTreeAPIProvider(FileTreeAPIImpl)];
backServices = [
{
servicePath: FileServicePath,
},
];
}
2
3
4
5
6
7
8
9
10
11
12
例如在 file-tree 模块中,首先在模块入口位置声明需要用到的 backServices
,传入引用的服务 servicePath
,与服务注册时的 servicePath
一致
packages/file-tree/src/browser/file-tree.service.ts
import {servicePath as FileServicePath} from '@opensumi/ide-file-service';
@Injectable()
export default class FileTreeService extends Disposable {
@observable.shallow
files: CloudFile[] = [];
@Autowired()
private fileAPI: FileTreeAPI;
@Autowired(CommandService)
private commandService: CommandService;
constructor(@Inject(FileServicePath) protected readonly fileService) {
super();
this.getFiles();
}
createFile = async () => {
const {content} = await this.fileService.resolveContent('/Users/franklife/work/ide/ac/ide-framework/tsconfig.json');
const file = await this.fileAPI.createFile({
name: 'name' + Date.now() + '\n' + content,
path: 'path' + Date.now(),
});
// 只会执行注册在 Module 里声明的 Contribution
this.commandService.executeCommand('file.tree.console');
if (this.files) {
this.files.push(file);
} else {
this.files = [file];
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
在 file-tree.service.ts 中通过 servicePath
进行注入,并直接调用在服务类上的方法
constructor(@Inject(FileServicePath) protected readonly fileService) {
super();
this.getFiles();
}
2
3
4
5
方法调用会转换成一个远程调用进行响应,返回结果
const { content } = await this.fileService.resolveContent('/Users/franklife/work/ide/ac/ide-framework/tsconfig.json');
前端服务(frontService) 后端服务(backService) 即在 Browser 环境下运行的代码暴露的能力
- 注册服务
packages/file-ree/src/browser/index.ts
@Injectable()
export class FileTreeModule extends BrowserModule {
providers: Provider[] = [createFileTreeAPIProvider(FileTreeAPIImpl)];
backServices = [
{
servicePath: FileServicePath,
},
];
frontServices = [
{
servicePath: FileTreeServicePath,
token: FileTreeService,
},
];
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
与后端服务注册类似,例如在 file-tree 模块中声明 frontServices
字段,传入对应的服务地址 servicePath
和对应服务的注入 token
- 服务使用
packages/file-service/src/node/index.ts
import { servicePath as FileTreeServicePath } from '@opensumi/ide-file-tree';
@Injectable()
export class FileServiceModule extends NodeModule {
providers = [{ token: 'FileServiceOptions', useValue: FileSystemNodeOptions.DEFAULT }];
backServices = [
{
servicePath,
token: FileService,
},
];
frontServices = [
{
servicePath: FileTreeServicePath,
},
];
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
与使用后端服务一致,在模块定义中声明需要使用的前端服务 frontServices
,传入前端服务注册时用的 servicePath
一致
packages/file-service/src/node/file-service.ts
@Injectable()
export class FileService implements IFileService {
constructor(
@Inject('FileServiceOptions') protected readonly options: FileSystemNodeOptions,
@Inject(FileTreeServicePath) protected readonly fileTreeService
) { }
async resolveContent(uri: string, options?: { encoding?: string }): Promise<{ stat: FileStat, content: string }> {
const fileTree = await this.fileTreeService
fileTree.fileName(uri.substr(-5))
...
return { stat, content };
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
与使用后端服务使用方式一致,在 file-service.ts 中通过 servicePath
进行注入,通过调用注入服务的对应方法
constructor(
@Inject('FileServiceOptions') protected readonly options: FileSystemNodeOptions,
@Inject(FileTreeServicePath) protected readonly fileTreeService
) { }
2
3
4
方法调用会转换成一个远程调用进行响应,返回结果
const fileTree = await this.fileTreeService;
fileTree.fileName(uri.substr(-5));
2
与后端服务调用区别的是,目前因前端代码后置执行,所以首先需要获取服务 await this.fileTreeService
后进行调用
# 树点击显示
# 页面点击
return (
<div
className={cls(styles.file_tree, outerDragOver && styles.outer_drag_over, outerActive && styles.outer_active)}
tabIndex={-1}
ref={wrapperRef}
onClick={handleOuterClick}
onDoubleClick={handleOuterDblClick}
onFocus={handleFocus}
onBlur={handleBlur}
onContextMenu={handleOuterContextMenu}
draggable={true}
onDragStart={handleOuterDragStart}
onDragLeave={handleOuterDragLeave}
onDragOver={handleOuterDragOver}
onDrop={handleOuterDrop}
>
<FileTreeView
isLoading={isLoading}
isReady={isReady}
height={height}
model={model}
iconTheme={iconTheme}
treeIndent={treeIndent}
filterMode={filterMode}
locationToCurrentFile={locationToCurrentFile}
onTreeReady={handleTreeReady}
onContextMenu={handleContextMenu}
onItemClick={handleItemClicked}
onItemDoubleClick={handleItemDoubleClicked}
onTwistierClick={handleTwistierClick}
/>
</div>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 事件处理
const handleItemClicked = useCallback(
(event: MouseEvent, item: File | Directory, type: TreeNodeType, activeUri?: URI) => {
// 阻止点击事件冒泡
event.stopPropagation();
const { handleItemClick, handleItemToggleClick, handleItemRangeClick } = fileTreeModelService;
if (!item) {
return;
}
const shiftMask = hasShiftMask(event);
const ctrlCmdMask = hasCtrlCmdMask(event);
if (shiftMask) {
handleItemRangeClick(item, type);
} else if (ctrlCmdMask) {
handleItemToggleClick(item, type);
} else {
handleItemClick(item, type, activeUri);
}
},
[],
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# handleItemClick
core/packages/file-tree-next/src/browser/services/file-tree-model.service.ts
/**
* 当传入的 `item` 为 `undefined` 时,默认为目录类型的选择
* 工作区模式下 `type` 为 `TreeNodeType.TreeNode`
* 目录模式下 `type` 为 `TreeNodeType.CompositeTreeNode`
*
* @param item 节点
* @param type 节点类型
* @param activeUri 焦点路径
*/
handleItemClick = (
item?: File | Directory,
type: TreeNodeType = this.fileTreeService.isMultipleWorkspace
? TreeNodeType.TreeNode
: TreeNodeType.CompositeTreeNode,
activeUri?: URI,
) => {
if (!this.treeModel) {
return;
}
if (!item) {
item = this.treeModel.root as Directory | File;
}
// 更新压缩节点对应的Contextkey
this.updateExplorerCompressedContextKey(item, activeUri);
this._isMultiSelected = false;
if (this.fileTreeService.isCompactMode && activeUri) {
this._activeUri = activeUri;
// 存在 activeUri 的情况默认 explorerResourceIsFolder 的值都为 true
this.contextKey?.explorerResourceIsFolder.set(true);
} else if (!activeUri) {
this._activeUri = null;
// 单选操作默认先更新选中状态
if (type === TreeNodeType.CompositeTreeNode || type === TreeNodeType.TreeNode) {
this.activeFileDecoration(item);
}
this.contextKey?.explorerResourceIsFolder.set(type === TreeNodeType.CompositeTreeNode);
}
// 如果为文件夹需展开
// 如果为文件,则需要打开文件
if (this.corePreferences['workbench.list.openMode'] === 'singleClick') {
if (type === TreeNodeType.CompositeTreeNode) {
this.contextKey?.explorerResourceIsFolder.set(true);
if (item === this.treeModel.root) {
// 根节点情况下忽略后续操作
return;
}
this.toggleDirectory(item as Directory);
} else if (type === TreeNodeType.TreeNode) {
this.contextKey?.explorerResourceIsFolder.set(false);
if (item === this.treeModel.root) {
// 根节点情况下忽略后续操作
return;
}
// 对于文件的单击事件,走 openFile 去执行 editor.previewMode 配置项
this.fileTreeService.openFile(item.uri);
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# openFile打开编辑文件
core/packages/file-tree-next/src/browser/file-tree.service.ts
/**
* 打开文件
* @param uri
*/
public openFile(uri: URI) {
// 当打开模式为双击同时预览模式生效时,默认单击为预览文件
const preview = this.preferenceService.get<boolean>('editor.previewMode');
this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, uri, { disableNavigate: true, preview });
}
2
3
4
5
6
7
8
9
EDITOR_COMMANDS.OPEN_RESOURCE 命令处理;
详见 【registerCommand【核心】】
# 打开编辑器文件
# 多个入口位置统计
左边树列表;
public openFile(uri: URI) { // 当打开模式为双击同时预览模式生效时,默认单击为预览文件 const preview = this.preferenceService.get<boolean>('editor.previewMode'); this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, uri, { disableNavigate: true, preview }); }
1
2
3
4
5左边树搜索过滤后列表;
async run(item: QuickOpenItem): Promise<void> { await this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, item.getUri(), { preview: false, split: EditorGroupSplitAction.Right, range: getRangeByInput(this.injector.get(FileSearchQuickCommandHandler).currentLookFor), focus: true, }); }
1
2
3
4
5
6
7
8面包屑下拉点击;
res.onClick = () => { this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, uri); };
1
2
3welcome快速进入;
比较页面;
export class CompareService implements ICompareService { public readonly comparing = new Map<string, Deferred<CompareResult>>(); @Autowired(CommandService) private commandService: CommandService; compare(original: URI, modified: URI, name: string): Promise<CompareResult> { const compareUri = URI.from({ scheme: 'diff', query: URI.stringifyQuery({ name, original, modified, comparing: true, }), }); if (!this.comparing.has(compareUri.toString())) { const deferred = new Deferred<CompareResult>(); this.comparing.set(compareUri.toString(), deferred); deferred.promise.then(() => { this.comparing.delete(compareUri.toString()); this.commandService.executeCommand(EDITOR_COMMANDS.CLOSE_ALL.id, compareUri); }); } this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, compareUri); return this.comparing.get(compareUri.toString())!.promise; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 测试模拟
core/packages/file-tree-next/__tests__/browser/file-tree.service.test.ts
it('Commands should be work', () => {
const testUri = new URI('file://userhome/test.js');
// openAndFixedFile
const mockOpenResource = jest.fn();
injector.mockCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, mockOpenResource);
fileTreeService.openAndFixedFile(testUri);
expect(mockOpenResource).toBeCalledWith(testUri, { disableNavigate: true, preview: false, focus: true });
// OpenToTheSide
fileTreeService.openToTheSide(testUri);
expect(mockOpenResource).toBeCalledWith(testUri, { disableNavigate: true, split: 4 });
// compare
const mockCompare = jest.fn();
injector.mockCommand(EDITOR_COMMANDS.COMPARE.id, mockCompare);
fileTreeService.compare(testUri, testUri);
expect(mockCompare).toBeCalledWith({
original: testUri,
modified: testUri,
});
// toggleFilterMode
const mockLocation = jest.fn();
injector.mockCommand(FILE_COMMANDS.LOCATION.id, mockLocation);
fileTreeService.toggleFilterMode();
// set filterMode to true
fileTreeService.toggleFilterMode();
expect(mockLocation).toBeCalledTimes(1);
// enableFilterMode
fileTreeService.toggleFilterMode();
expect(fileTreeService.filterMode).toBeTruthy();
// locationToCurrentFile
fileTreeService.locationToCurrentFile();
expect(mockLocation).toBeCalledTimes(2);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 点击树Item入口
core/packages/file-tree-next/src/browser/file-tree.tsx
const handleItemClicked = useCallback(
(event: MouseEvent, item: File | Directory, type: TreeNodeType, activeUri?: URI) => {
// 阻止点击事件冒泡
event.stopPropagation();
const { handleItemClick, handleItemToggleClick, handleItemRangeClick } = fileTreeModelService;
if (!item) {
return;
}
const shiftMask = hasShiftMask(event);
const ctrlCmdMask = hasCtrlCmdMask(event);
if (shiftMask) {
handleItemRangeClick(item, type);
} else if (ctrlCmdMask) {
handleItemToggleClick(item, type);
} else {
handleItemClick(item, type, activeUri);
}
},
[],
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# handleItemClick
最终还是调用 this.fileTreeService.openFile(item.uri);
core/packages/file-tree-next/src/browser/services/file-tree-model.service.ts
/**
* 当传入的 `item` 为 `undefined` 时,默认为目录类型的选择
* 工作区模式下 `type` 为 `TreeNodeType.TreeNode`
* 目录模式下 `type` 为 `TreeNodeType.CompositeTreeNode`
* @param item 节点
* @param type 节点类型
* @param activeUri 焦点路径
*/
handleItemClick = (
item?: File | Directory,
type: TreeNodeType = this.fileTreeService.isMultipleWorkspace
? TreeNodeType.TreeNode
: TreeNodeType.CompositeTreeNode,
activeUri?: URI,
) => {
if (!this.treeModel) {
return;
}
if (!item) {
item = this.treeModel.root as Directory | File;
}
// 更新压缩节点对应的Contextkey
this.updateExplorerCompressedContextKey(item, activeUri);
this._isMultiSelected = false;
if (this.fileTreeService.isCompactMode && activeUri) {
this._activeUri = activeUri;
// 存在 activeUri 的情况默认 explorerResourceIsFolder 的值都为 true
this.contextKey?.explorerResourceIsFolder.set(true);
} else if (!activeUri) {
this._activeUri = null;
// 单选操作默认先更新选中状态
if (type === TreeNodeType.CompositeTreeNode || type === TreeNodeType.TreeNode) {
this.activeFileDecoration(item);
}
this.contextKey?.explorerResourceIsFolder.set(type === TreeNodeType.CompositeTreeNode);
}
// 如果为文件夹需展开
// 如果为文件,则需要打开文件
if (this.corePreferences['workbench.list.openMode'] === 'singleClick') {
if (type === TreeNodeType.CompositeTreeNode) {
this.contextKey?.explorerResourceIsFolder.set(true);
if (item === this.treeModel.root) {
// 根节点情况下忽略后续操作
return;
}
this.toggleDirectory(item as Directory);
} else if (type === TreeNodeType.TreeNode) {
this.contextKey?.explorerResourceIsFolder.set(false);
if (item === this.treeModel.root) {
// 根节点情况下忽略后续操作
return;
}
// 对于文件的单击事件,走 openFile 去执行 editor.previewMode 配置项
this.fileTreeService.openFile(item.uri);
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# openFile打开编辑器编辑
EDITOR_COMMANDS.OPEN_RESOURCE
/**
* 打开文件
* @param uri
*/
public openFile(uri: URI) {
// 当打开模式为双击同时预览模式生效时,默认单击为预览文件
const preview = this.preferenceService.get<boolean>('editor.previewMode');
this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, uri, {
disableNavigate: true,
preview,
});
}
2
3
4
5
6
7
8
9
10
11
12
# opened-editor页面入口
opened-editor # 「打开的编辑器」功能实现
# 注册树组件
core/packages/opened-editor/src/browser/opened-editor.contribution.ts
async onStart() {
this.mainLayoutService.collectViewComponent(
{
id: ExplorerOpenedEditorViewId,
name: localize('opened.editors.title'),
weight: 1,
priority: 10,
collapsed: true,
component: ExplorerOpenEditorPanel,
},
EXPLORER_CONTAINER_ID,
);
}
2
3
4
5
6
7
8
9
10
11
12
13
# ExplorerOpenEditorPanel
core/packages/opened-editor/src/browser/opened-editor.tsx
const renderTreeNode = React.useCallback(
(props: INodeRendererWrapProps) => (
<EditorTreeNode
item={props.item}
itemType={props.itemType}
decorationService={decorationService}
labelService={labelService}
commandService={commandService}
decorations={openedEditorModelService.decorations.getDecorations(props.item as any)}
onClick={handleItemClicked}
onContextMenu={handlerContextMenu}
defaultLeftPadding={22}
leftPadding={0}
/>
),
[openedEditorModelService.treeModel],
);
const renderContent = () => {
if (!isReady) {
return <span className={styles.opened_editor_empty_text}>{localize('opened.editors.empty')}</span>;
} else {
if (isLoading) {
return <ProgressBar loading />;
} else if (model) {
return (
<RecycleTree
height={height}
width={width}
itemHeight={OPEN_EDITOR_NODE_HEIGHT}
onReady={handleTreeReady}
model={model}
placeholder={() => (
<span className={styles.opened_editor_empty_text}>{localize('opened.editors.empty')}</span>
)}
>
{renderTreeNode}
</RecycleTree>
);
} else {
return <span className={styles.opened_editor_empty_text}>{localize('opened.editors.empty')}</span>;
}
}
};
return (
<div
className={styles.opened_editor_container}
tabIndex={-1}
ref={wrapperRef}
onContextMenu={handleOuterContextMenu}
onClick={handleOuterClick}
>
{renderContent()}
</div>
);
const handleItemClicked = (ev: React.MouseEvent, item: EditorFile | EditorFileGroup, type: TreeNodeType) => {
// 阻止点击事件冒泡
ev.stopPropagation();
const { handleItemClick } = openedEditorModelService;
if (!item) {
return;
}
handleItemClick(item, type);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
core/packages/opened-editor/src/browser/services/opened-editor-model.service.ts
handleItemClick = (item: EditorFileGroup | EditorFile, type: TreeNodeType) => {
// 单选操作默认先更新选中状态
this.activeFileDecoration(item);
if (type === TreeNodeType.TreeNode) {
this.openFile(item as EditorFile);
}
};
2
3
4
5
6
7
8
# openFile/openToTheSide
最终:EDITOR_COMMANDS.OPEN_RESOURCE
core/packages/file-tree-next/src/browser/file-tree.service.ts
/**
* 打开文件
* @param uri
*/
public openFile(uri: URI) {
// 当打开模式为双击同时预览模式生效时,默认单击为预览文件
const preview = this.preferenceService.get<boolean>('editor.previewMode');
this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, uri, { disableNavigate: true, preview });
}
/**
* 在侧边栏打开文件
* @param {URI} uri
* @memberof FileTreeService
*/
public openToTheSide(uri: URI) {
this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, uri, {
disableNavigate: true,
split: 4 /** right */,
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# registerCommand【核心】
core/packages/editor/src/browser/editor.contribution.ts
commands.registerCommand(EDITOR_COMMANDS.OPEN_RESOURCE, {
execute: async (uri: URI, options?: IResourceOpenOptions) => {
const openResult = await this.workbenchEditorService.open(uri, options);
if (openResult) {
return {
groupId: openResult?.group.name,
};
}
},
});
2
3
4
5
6
7
8
9
10
# open
core/packages/editor/src/browser/workbench-editor.service.ts
async open(uri: URI, options?: IResourceOpenOptions) {
await this.initialize();
let group = this.currentEditorGroup;
let groupIndex: number | undefined;
if (options && typeof options.groupIndex !== 'undefined') {
groupIndex = options.groupIndex;
} else if (options && options.relativeGroupIndex) {
groupIndex = this.currentEditorGroup.index + options.relativeGroupIndex;
}
if (typeof groupIndex === 'number' && groupIndex >= 0) {
if (groupIndex >= this.editorGroups.length) {
return group.open(uri, Object.assign({}, options, { split: EditorGroupSplitAction.Right }));
} else {
group = this.sortedEditorGroups[groupIndex] || this.currentEditorGroup;
}
}
return group.open(uri, options);
}
public initialize() {
if (!this.initializing) {
this.initializing = this.doInitialize();
}
return this.initializing;
}
private async doInitialize() {
this.openedResourceState = await this.initializeState();
await this.contributionsReady.promise;
await this.restoreState();
this._currentEditorGroup = this.editorGroups[0];
}
async open(uri: URI, options: IResourceOpenOptions = {}): Promise<IOpenResourceResult> {
if (uri.scheme === Schemes.file) {
// 只记录 file 类型的
this.recentFilesManager.setMostRecentlyOpenedFile!(uri.withoutFragment().toString());
}
if (options && options.split) {
return this.split(options.split, uri, Object.assign({}, options, { split: undefined, preview: false }));
}
if (!this.openingPromise.has(uri.toString())) {
const promise = this.doOpen(uri, options);
this.openingPromise.set(uri.toString(), promise);
promise.then(
() => {
this.openingPromise.delete(uri.toString());
},
() => {
this.openingPromise.delete(uri.toString());
},
);
}
const previewMode =
this.preferenceService.get('editor.previewMode') && (isUndefinedOrNull(options.preview) ? true : options.preview);
if (!previewMode) {
this.openingPromise.get(uri.toString())!.then(() => {
this.pinPreviewed(uri);
});
}
return this.openingPromise.get(uri.toString())!;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# doOpen
this._currentResource = resource;
记录相关;
async doOpen(
uri: URI,
options: IResourceOpenOptions = {},
): Promise<{ group: IEditorGroup; resource: IResource } | false> {
if (!this.resourceService.handlesUri(uri)) {
this.openerService.open(uri);
return false;
}
let resourceReady: Deferred<void> | undefined;
try {
const previewMode =
this.preferenceService.get('editor.previewMode') &&
(isUndefinedOrNull(options.preview) ? true : options.preview);
if (this.currentResource && this.currentResource.uri.isEqual(uri)) {
// 就是当前打开的resource
if (options.focus && this.currentEditor) {
this._domNode?.focus();
this.currentEditor.monacoEditor.focus();
}
if (options.range && this.currentEditor) {
this.currentEditor.monacoEditor.setSelection(options.range as monaco.IRange);
this.currentEditor.monacoEditor.revealRangeInCenterIfOutsideViewport(options.range as monaco.IRange, 0);
}
if ((options && options.disableNavigate) || (options && options.backend)) {
// no-op
} else {
this.locateInFileTree(uri);
}
this.notifyTabChanged();
return {
group: this,
resource: this.currentResource,
};
} else {
const oldOpenType = this._currentOpenType;
const oldResource = this._currentResource;
let resource: IResource | null | undefined = this.resources.find((r) => r.uri.toString() === uri.toString());
if (!resource) {
// open new resource
resource = await this.resourceService.getResource(uri);
if (!resource) {
throw new Error('This uri cannot be opened!: ' + uri);
}
if (resource.deleted) {
if (options.deletedPolicy === 'fail') {
throw new Error('resource deleted ' + uri);
} else if (options.deletedPolicy === 'skip') {
return false;
}
}
if (options && options.label) {
resource.name = options.label;
}
let replaceResource: IResource | null = null;
if (options && options.index !== undefined && options.index < this.resources.length) {
replaceResource = this.resources[options.index];
this.resources.splice(options.index, 0, resource);
} else {
if (this.currentResource) {
const currentIndex = this.resources.indexOf(this.currentResource);
this.resources.splice(currentIndex + 1, 0, resource);
replaceResource = this.currentResource;
} else {
this.resources.push(resource);
}
}
if (previewMode) {
if (this.previewURI) {
await this.close(this.previewURI, { treatAsNotCurrent: true, force: options.forceClose });
}
this.previewURI = resource.uri;
}
if (options.replace && replaceResource) {
await this.close(replaceResource.uri, { treatAsNotCurrent: true, force: options.forceClose });
}
}
if (options.backend) {
this.notifyTabChanged();
return false;
}
if (oldResource && this.resourceOpenHistory[this.resourceOpenHistory.length - 1] !== oldResource.uri) {
this.resourceOpenHistory.push(oldResource.uri);
const oldResourceSelections = this.currentCodeEditor?.getSelections();
if (oldResourceSelections && oldResourceSelections.length > 0) {
this.recentFilesManager.updateMostRecentlyOpenedFile(oldResource.uri.toString(), {
lineNumber: oldResourceSelections[0].selectionStartLineNumber,
column: oldResourceSelections[0].selectionStartColumn,
});
}
}
this._currentResource = resource;
this.notifyTabChanged();
this._currentOpenType = null;
this.notifyBodyChanged();
// 只有真正打开的文件才会走到这里,backend模式的只更新了tab,文件内容并未加载
const reportTimer = this.reporterService.time(REPORT_NAME.EDITOR_REACTIVE);
resourceReady = new Deferred<void>();
this.resourceStatus.set(resource, resourceReady.promise);
// 超过60ms loading时间的才展示加载
const delayTimer = setTimeout(() => {
this.notifyTabLoading(resource!);
}, 60);
await this.displayResourceComponent(resource, options);
clearTimeout(delayTimer);
resourceReady.resolve();
reportTimer.timeEnd(resource.uri.toString());
this._currentOrPreviousFocusedEditor = this.currentEditor;
this._onDidEditorFocusChange.fire();
this.setContextKeys();
this.eventBus.fire(
new EditorGroupOpenEvent({
group: this,
resource,
}),
);
if ((options && options.disableNavigate) || (options && options.backend)) {
// no-op
} else {
this.locateInFileTree(uri);
}
this.eventBus.fire(
new EditorGroupChangeEvent({
group: this,
newOpenType: this.currentOpenType,
newResource: this.currentResource,
oldOpenType,
oldResource,
}),
);
return {
group: this,
resource,
};
}
} catch (e) {
getDebugLogger().error(e);
resourceReady && resourceReady.reject();
if (!isEditorError(e, EditorTabChangedError)) {
this.messageService.error(formatLocalize('editor.failToOpen', uri.displayName, e.message), [], true);
}
return false;
// todo 给用户显示error
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# 创建文件【核心】
三个入口
registerMenus(menuRegistry: IMenuRegistry): void {
menuRegistry.registerMenuItem(MenuId.ExplorerContext, {
command: {
id: FILE_COMMANDS.NEW_FILE.id,
label: localize('file.new'),
},
order: 1,
group: '0_new',
});
}
registerToolbarItems(registry: ToolbarRegistry) {
registry.registerItem({
id: FILE_COMMANDS.NEW_FILE.id,
command: FILE_COMMANDS.NEW_FILE.id,
label: localize('file.new'),
viewId: RESOURCE_VIEW_ID,
when: `view == '${RESOURCE_VIEW_ID}' && !${FilesExplorerFilteredContext.raw}`,
order: 1,
});
}
//file-tree-model.service.ts
handleDblClick = () => {
this.commandService.executeCommand(FILE_COMMANDS.NEW_FILE.id);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# registerCommand
统一处理创建文件事件,处理上面三入口
commands.registerCommand<ExplorerContextCallback>(FILE_COMMANDS.NEW_FILE, {
execute: async (uri) => {
if (this.fileTreeService.filterMode) {
this.fileTreeService.toggleFilterMode();
}
if (uri) {
this.fileTreeModelService.newFilePrompt(uri);
} else {
if (this.fileTreeService.isCompactMode && this.fileTreeModelService.activeUri) {
this.fileTreeModelService.newFilePrompt(this.fileTreeModelService.activeUri);
}
//双击空目录,处理新建文件的情况
else if (this.fileTreeModelService.selectedFiles && this.fileTreeModelService.selectedFiles.length > 0) {
this.fileTreeModelService.newFilePrompt(this.fileTreeModelService.selectedFiles[0].uri);
} else {
let rootUri: URI;
if (!this.fileTreeService.isMultipleWorkspace) {
rootUri = new URI(this.workspaceService.workspace?.uri);
} else {
rootUri = new URI((await this.workspaceService.roots)[0].uri);
}
this.fileTreeModelService.newFilePrompt(rootUri);
}
}
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# newFilePrompt
async newFilePrompt(uri: URI) {
const targetNode = await this.getPromptTarget(uri, true);
if (targetNode) {
this.proxyPrompt(await this.fileTreeHandle.promptNewTreeNode(targetNode as Directory));
}
}
2
3
4
5
6
# getPromptTarget
组装node
private async getPromptTarget(uri: URI, isCreatingFile?: boolean) {
let targetNode: File | Directory;
// 使用path能更精确的定位新建文件位置,因为软连接情况下可能存在uri一致的情况
if (uri.isEqual((this.treeModel.root as Directory).uri)) {
// 可能为空白区域点击, 即选中的对象为根目录
targetNode = await this.fileTreeService.getNodeByPathOrUri(uri)!;
} else if (this.focusedFile) {
targetNode = this.focusedFile;
} else if (this.contextMenuFile) {
targetNode = this.contextMenuFile;
} else if (this.selectedFiles.length > 0) {
const selectedNode = this.selectedFiles[this.selectedFiles.length - 1];
if (!this.treeModel.root.isItemVisibleAtSurface(selectedNode)) {
const targetNodePath = await this.fileTreeService.getFileTreeNodePathByUri(uri);
targetNode = (await this.treeModel.root.loadTreeNodeByPath(targetNodePath!)) as File;
} else {
targetNode = selectedNode;
}
} else {
targetNode = await this.fileTreeService.getNodeByPathOrUri(uri)!;
}
if (!targetNode) {
targetNode = this.treeModel.root as Directory;
}
const namePieces = Path.splitPath(targetNode.name);
if (Directory.isRoot(targetNode)) {
return targetNode;
} else if (
targetNode.name !== uri.displayName &&
namePieces[namePieces.length - 1] !== uri.displayName &&
isCreatingFile
) {
// 说明当前在压缩节点的非末尾路径上触发的新建事件, 如 a/b 上右键 a 产生的新建事件
const removePathName = uri.relative(targetNode.uri)!.toString();
const relativeName = targetNode.name.replace(`${Path.separator}${removePathName}`, '');
const newTargetUri = (targetNode.parent as Directory).uri.resolve(relativeName);
const tempFileName = removePathName.split(Path.separator)[0];
if (!relativeName) {
return;
}
// 移除目录下的子节点
if ((targetNode as Directory).children) {
for (const node of (targetNode as Directory).children!) {
this.fileTreeService.deleteAffectedNodeByPath(node.path, true);
}
}
// 更新目标节点信息
(targetNode as Directory).updateMetaData({
name: relativeName?.toString(),
uri: newTargetUri,
tooltip: this.fileTreeAPI.getReadableTooltip(newTargetUri),
fileStat: {
...targetNode.filestat,
uri: newTargetUri.toString(),
},
});
this.fileTreeService.addNode(targetNode as Directory, tempFileName, TreeNodeType.CompositeTreeNode);
}
return targetNode;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# getNodeByPathOrUri
空目录双击处理
/**
*
* @param pathOrUri 路径或者URI对象
* @param compactMode 是否开启压缩模式查找
*
*/
getNodeByPathOrUri(pathOrUri: string | URI) {
let path: string | undefined;
let pathURI: URI | undefined;
if (typeof pathOrUri !== 'string') {
pathURI = pathOrUri;
pathOrUri = pathOrUri.toString();
} else if (this.isFileURI(pathOrUri)) {
pathURI = new URI(pathOrUri);
} else if (!this.isFileURI(pathOrUri) && typeof pathOrUri === 'string') {
path = pathOrUri;
}
if (this.isFileURI(pathOrUri) && !!pathURI) {
let rootStr;
if (!this.isMultipleWorkspace) {
rootStr = this.workspaceService.workspace?.uri;
} else if (this._roots) {
rootStr = this._roots.find((root) => new URI(root.uri).isEqualOrParent(pathURI!))?.uri;
}
if (this.root && rootStr) {
const rootUri = new URI(rootStr);
if (rootUri.isEqualOrParent(pathURI)) {
path = new Path(this.root.path).join(rootUri.relative(pathURI)!.toString()).toString();
}
}
}
if (path) {
// 压缩模式下查找不到对应节点时,需要查看是否已有包含的文件夹存在
// 如当收到的变化是 /root/test_folder/test_file,而当前缓存中的路径只有/root/test_folder/test_folder2的情况
// 需要用当前缓存路径校验是否存在包含关系,这里/root/test_folder/test_folder2与/root/test_folder存在路径包含关系
// 此时应该重载/root下的文件,将test_folder目录折叠并清理缓存
if (this.isCompactMode && !this._cacheNodesMap.has(path)) {
const allNearestPath = Array.from(this._cacheNodesMap.keys()).filter((cache) => cache.indexOf(path!) >= 0);
let nearestPath;
for (const nextPath of allNearestPath) {
const depth = Path.pathDepth(nextPath);
if (nearestPath) {
if (depth < nearestPath.depth) {
nearestPath = {
path: nextPath,
depth,
};
}
} else {
nearestPath = {
path: nextPath,
depth,
};
}
}
if (nearestPath) {
return this.root?.getTreeNodeByPath(nearestPath.path) as File | Directory;
}
}
return this.root?.getTreeNodeByPath(path) as File | Directory;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# proxyPrompt
private proxyPrompt = (promptHandle: RenamePromptHandle | NewPromptHandle) => {
let isCommit = false;
const selectNodeIfNodeExist = async (path: string) => {
// 文件树更新后尝试定位文件位置
const node = await this.fileTreeService.getNodeByPathOrUri(path);
if (node && node.path === path) {
this.selectFileDecoration(node);
}
};
const commit = async (newName) => {
this.validateMessage = undefined;
if (promptHandle instanceof RenamePromptHandle) {
const target = promptHandle.target as File | Directory;
const nameFragments = (promptHandle.target as File).displayName.split(Path.separator);
const index = this.activeUri?.displayName ? nameFragments.indexOf(this.activeUri?.displayName) : -1;
const newNameFragments = index === -1 ? [] : nameFragments.slice(0, index).concat(newName);
let from = target.uri;
let to = (target.parent as Directory).uri.resolve(newName);
const isCompactNode = target.name.indexOf(Path.separator) > 0;
// 无变化,直接返回
if ((isCompactNode && this.activeUri?.displayName === newName) || (!isCompactNode && newName === target.name)) {
return true;
}
promptHandle.addAddonAfter('loading_indicator');
if (isCompactNode && newNameFragments.length > 0) {
// 压缩目录情况下,需要计算下标进行重命名路径拼接
from = (target.parent as Directory).uri.resolve(nameFragments.slice(0, index + 1).join(Path.separator));
to = (target.parent as Directory).uri.resolve(newNameFragments.concat().join(Path.separator));
}
// 屏蔽重命名文件事件
const error = await this.fileTreeAPI.mv(from, to, target.type === TreeNodeType.CompositeTreeNode);
if (error) {
this.validateMessage = {
type: PROMPT_VALIDATE_TYPE.ERROR,
message: error,
value: newName,
};
promptHandle.addValidateMessage(this.validateMessage);
return false;
}
if (!isCompactNode && target.parent) {
// 重命名节点的情况,直接刷新一下父节点即可
const node = await this.fileTreeService.moveNodeByPath(
target.parent as Directory,
target.parent as Directory,
target.name,
newName,
target.type,
);
if (node) {
this.selectFileDecoration(node as File, false);
}
} else {
// 更新压缩目录展示名称
// 由于节点移动时默认仅更新节点路径
// 我们需要自己更新额外的参数,如uri, filestat等
(target as Directory).updateMetaData({
name: newNameFragments.concat(nameFragments.slice(index + 1)).join(Path.separator),
uri: to,
fileStat: {
...target.filestat,
uri: to.toString(),
},
tooltip: this.fileTreeAPI.getReadableTooltip(to),
});
this.treeModel.dispatchChange();
if ((target.parent as Directory).children?.find((child) => target.path.indexOf(child.path) >= 0)) {
// 当重命名后的压缩节点在父节点中存在子节点时,刷新父节点
// 如:
// 压缩节点 001/002 修改为 003/002 时
// 同时父节点下存在 003 空节点
await this.fileTreeService.refresh(target.parent as Directory);
} else {
// 压缩节点重命名时,刷新文件夹更新子文件路径
await this.fileTreeService.refresh(target as Directory);
}
}
promptHandle.removeAddonAfter();
} else if (promptHandle instanceof NewPromptHandle) {
const parent = promptHandle.parent as Directory;
const newUri = parent.uri.resolve(newName);
let error;
const isEmptyDirectory = !parent.children || parent.children.length === 0;
promptHandle.addAddonAfter('loading_indicator');
if (promptHandle.type === TreeNodeType.CompositeTreeNode) {
error = await this.fileTreeAPI.createDirectory(newUri);
} else {
error = await this.fileTreeAPI.createFile(newUri);
}
promptHandle.removeAddonAfter();
if (error) {
this.validateMessage = {
type: PROMPT_VALIDATE_TYPE.ERROR,
message: error,
value: newName,
};
promptHandle.addValidateMessage(this.validateMessage);
return false;
}
if (this.fileTreeService.isCompactMode && newName.indexOf(Path.separator) > 0 && !Directory.isRoot(parent)) {
// 压缩模式下,检查是否有同名父目录存在,有则不需要生成临时目录,刷新对应父节点并定位节点
const parentPath = new Path(parent.path).join(Path.splitPath(newName)[0]).toString();
const parentNode = this.fileTreeService.getNodeByPathOrUri(parentPath) as Directory;
if (parentNode) {
if (!parentNode.expanded && !parentNode.children) {
await parentNode.setExpanded(true);
// 使用uri作为定位是不可靠的,需要检查一下该节点是否处于软链接目录内进行对应转换
selectNodeIfNodeExist(new Path(parent.path).join(newName).toString());
} else {
await this.fileTreeService.refresh(parentNode as Directory);
selectNodeIfNodeExist(new Path(parent.path).join(newName).toString());
}
} else {
// 不存在同名目录的情况下
if (promptHandle.type === TreeNodeType.CompositeTreeNode) {
if (isEmptyDirectory) {
const newNodeName = [parent.name].concat(newName).join(Path.separator);
parent.updateMetaData({
name: newNodeName,
uri: parent.uri.resolve(newName),
fileStat: {
...parent.filestat,
uri: parent.uri.resolve(newName).toString(),
},
tooltip: this.fileTreeAPI.getReadableTooltip(parent.uri.resolve(newName)),
});
selectNodeIfNodeExist(parent.path);
} else {
const addNode = await this.fileTreeService.addNode(parent, newName, promptHandle.type);
// 文件夹首次创建需要将焦点设到新建的文件夹上
selectNodeIfNodeExist(addNode.path);
}
} else if (promptHandle.type === TreeNodeType.TreeNode) {
const namePieces = Path.splitPath(newName);
const parentAddonPath = namePieces.slice(0, namePieces.length - 1).join(Path.separator);
const fileName = namePieces.slice(-1)[0];
const parentUri = parent.uri.resolve(parentAddonPath);
const newNodeName = [parent.name].concat(parentAddonPath).join(Path.separator);
parent.updateMetaData({
name: newNodeName,
uri: parentUri,
fileStat: {
...parent.filestat,
uri: parentUri.toString(),
},
tooltip: this.fileTreeAPI.getReadableTooltip(parentUri),
});
const addNode = (await this.fileTreeService.addNode(parent, fileName, TreeNodeType.TreeNode)) as File;
selectNodeIfNodeExist(addNode.path);
}
}
} else {
if (
this.fileTreeService.isCompactMode &&
promptHandle.type === TreeNodeType.CompositeTreeNode &&
isEmptyDirectory &&
!Directory.isRoot(parent)
) {
const parentUri = parent.uri.resolve(newName);
const newNodeName = [parent.name].concat(newName).join(Path.separator);
parent.updateMetaData({
name: newNodeName,
uri: parentUri,
fileStat: {
...parent.filestat,
uri: parentUri.toString(),
},
tooltip: this.fileTreeAPI.getReadableTooltip(parentUri),
});
selectNodeIfNodeExist(parent.path);
} else {
await this.fileTreeService.addNode(parent, newName, promptHandle.type);
selectNodeIfNodeExist(new Path(parent!.path).join(newName).toString());
}
}
}
this.contextKey?.filesExplorerInputFocused.set(false);
return true;
};
const blurCommit = async (newName) => {
if (isCommit) {
return false;
}
if (!!this.validateMessage && this.validateMessage.type === PROMPT_VALIDATE_TYPE.ERROR) {
this.validateMessage = undefined;
return true;
}
if (!newName) {
// 清空节点路径焦点态
this.contextKey?.explorerCompressedFocusContext.set(false);
this.contextKey?.explorerCompressedFirstFocusContext.set(false);
this.contextKey?.explorerCompressedLastFocusContext.set(false);
if (this.fileTreeService.isCompactMode && promptHandle instanceof NewPromptHandle) {
this.fileTreeService.refresh(promptHandle.parent as Directory);
}
return;
}
this.contextKey?.filesExplorerInputFocused.set(false);
await commit(newName);
return true;
};
const enterCommit = async (newName) => {
isCommit = true;
if (!!this.validateMessage && this.validateMessage.type === PROMPT_VALIDATE_TYPE.ERROR) {
return false;
}
if (
newName.trim() === '' ||
(!!this.validateMessage && this.validateMessage.type !== PROMPT_VALIDATE_TYPE.ERROR)
) {
this.validateMessage = undefined;
return true;
}
const success = await commit(newName);
isCommit = false;
if (!success) {
return false;
}
// 返回true时,输入框会隐藏
return true;
};
const handleFocus = async () => {
this.contextKey?.filesExplorerInputFocused.set(true);
};
const handleDestroy = () => {
this.contextKey?.filesExplorerInputFocused.set(false);
if (this.contextMenuFile) {
// 卸载输入框时及时更新选中态
this.selectFileDecoration(this.contextMenuFile, true);
}
};
const handleCancel = () => {
this.contextKey?.filesExplorerInputFocused.set(false);
if (this.fileTreeService.isCompactMode) {
if (promptHandle instanceof NewPromptHandle) {
this.fileTreeService.refresh(promptHandle.parent as Directory);
}
}
};
const handleChange = (currentValue) => {
const validateMessage = this.validateFileName(promptHandle, currentValue);
if (validateMessage) {
this.validateMessage = validateMessage;
promptHandle.addValidateMessage(validateMessage);
} else if (!validateMessage && this.validateMessage && this.validateMessage.value !== currentValue) {
this.validateMessage = undefined;
promptHandle.removeValidateMessage();
}
};
if (!promptHandle.destroyed) {
promptHandle.onChange(handleChange);
promptHandle.onCommit(enterCommit);
promptHandle.onBlur(blurCommit);
promptHandle.onFocus(handleFocus);
promptHandle.onDestroy(handleDestroy);
promptHandle.onCancel(handleCancel);
}
// 文件树刷新操作会让重命名/新建输入框销毁
// 可能存在部分用户疑惑
this.disposableCollection.push(
Event.once(this.fileTreeService.onNodeRefreshed)(() => {
if (promptHandle && !promptHandle.destroyed) {
promptHandle.destroy();
}
}),
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# promptNewTreeNode
public componentDidMount() {
const { model, onReady } = this.props;
this.listRef?.current?.scrollTo(model.state.scrollOffset);
this.disposables.push(model.onChange(this.batchUpdate.bind(this)));
this.disposables.push(
model.state.onDidLoadState(() => {
this.listRef?.current?.scrollTo(model.state.scrollOffset);
}),
);
if (typeof onReady === 'function') {
const api: IRecycleTreeHandle = {
promptNewTreeNode: this.promptNewTreeNode,
promptNewCompositeTreeNode: this.promptNewCompositeTreeNode,
promptRename: this.promptRename,
expandNode: this.expandNode,
collapseNode: this.collapseNode,
ensureVisible: this.ensureVisible,
getModel: () => this.props.model,
layoutItem: this.layoutItem,
getCurrentSize: () => ({
width: this.props.width,
height: this.props.height,
}),
onDidChangeModel: this.onDidModelChangeEmitter.event,
onDidUpdate: this.onDidUpdateEmitter.event,
onOnceDidUpdate: Event.once(this.onDidUpdateEmitter.event),
onError: this.onErrorEmitter.event,
};
onReady(api);
}
}
// 使用箭头函数绑定当前this
private promptNewTreeNode = (pathOrTreeNode: string | CompositeTreeNode): Promise<NewPromptHandle> =>
this.promptNew(pathOrTreeNode);
private async promptNew(
pathOrTreeNode: string | CompositeTreeNode,
type: TreeNodeType = TreeNodeType.TreeNode,
): Promise<NewPromptHandle> {
const { root } = this.props.model;
let node = typeof pathOrTreeNode === 'string' ? await root.getTreeNodeByPath(pathOrTreeNode) : pathOrTreeNode;
if (type !== TreeNodeType.TreeNode && type !== TreeNodeType.CompositeTreeNode) {
throw new TypeError(
`Invalid type supplied. Expected 'TreeNodeType.TreeNode' or 'TreeNodeType.CompositeTreeNode', got ${type}`,
);
}
if (!!node && !CompositeTreeNode.is(node)) {
// 获取的子节点如果不是CompositeTreeNode,尝试获取父级节点
node = node.parent;
if (!CompositeTreeNode.is(node)) {
throw new TypeError(`Cannot create new prompt at object of type ${typeof node}`);
}
}
if (!node) {
throw new Error(`Cannot find node at ${pathOrTreeNode}`);
}
const promptHandle = new NewPromptHandle(type, node as CompositeTreeNode);
this.promptHandle = promptHandle;
this.promptTargetID = node!.id;
if (
node !== root &&
(!(node as CompositeTreeNode).expanded || !root.isItemVisibleAtSurface(node as CompositeTreeNode))
) {
// 调用setExpanded即会在之后调用batchUpdate函数
await (node as CompositeTreeNode).setExpanded(true);
} else {
await this.batchUpdate();
}
if (this.newPromptInsertionIndex >= 0) {
// 说明已在输入框已在可视区域
this.listRef?.current?.scrollToItem(this.newPromptInsertionIndex);
} else {
this.tryScrollIntoViewWhileStable(this.promptHandle as any);
}
return this.promptHandle as NewPromptHandle;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# selectNodeIfNodeExist
const selectNodeIfNodeExist = async (path: string) => {
// 文件树更新后尝试定位文件位置
const node = await this.fileTreeService.getNodeByPathOrUri(path);
if (node && node.path === path) {
this.selectFileDecoration(node);
}
};
2
3
4
5
6
7
# commit
const enterCommit = async (newName) => {
isCommit = true;
if (!!this.validateMessage && this.validateMessage.type === PROMPT_VALIDATE_TYPE.ERROR) {
return false;
}
if (
newName.trim() === '' ||
(!!this.validateMessage && this.validateMessage.type !== PROMPT_VALIDATE_TYPE.ERROR)
) {
this.validateMessage = undefined;
return true;
}
const success = await commit(newName);
isCommit = false;
if (!success) {
return false;
}
// 返回true时,输入框会隐藏
return true;
};
const commit = async (newName) => {
else if (promptHandle instanceof NewPromptHandle) {
const parent = promptHandle.parent as Directory;
const newUri = parent.uri.resolve(newName);
let error;
const isEmptyDirectory = !parent.children || parent.children.length === 0;
promptHandle.addAddonAfter('loading_indicator');
if (promptHandle.type === TreeNodeType.CompositeTreeNode) {
error = await this.fileTreeAPI.createDirectory(newUri);
} else {
error = await this.fileTreeAPI.createFile(newUri);
}
promptHandle.removeAddonAfter();
if (error) {
this.validateMessage = {
type: PROMPT_VALIDATE_TYPE.ERROR,
message: error,
value: newName,
};
promptHandle.addValidateMessage(this.validateMessage);
return false;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# createFile 正式创建文件
core/packages/file-tree-next/src/browser/services/file-tree-api.service.ts
async createFile(uri: URI) {
try {
await this.workspaceEditService.apply({
edits: [
{
newResource: uri,
options: {},
},
],
});
} catch (e) {
return e.message;
}
this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, uri, { disableNavigate: true });
return;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复用上面打开文件功能命令;
# workspaceEditService.apply
core/packages/workspace-edit/src/browser/workspace-edit.service.ts
async apply(edit: IWorkspaceEdit): Promise<void> {
const bulkEdit = new BulkEdit();
edit.edits.forEach((edit) => {
bulkEdit.add(edit);
});
await bulkEdit.apply(this.documentModelService, this.editorService, this.workspaceFileService, this.eventBus);
this.editStack.push(bulkEdit);
}
async apply(
documentModelService: IEditorDocumentModelService,
editorService: WorkbenchEditorService,
workspaceFS: IWorkspaceFileService,
eventBus: IEventBus,
) {
for (const edit of this.edits) {
if (edit instanceof ResourceFileEdit) {
await edit.apply(documentModelService, editorService, workspaceFS, eventBus);
} else {
await edit.apply(documentModelService, editorService);
}
}
}
async apply(
documentModelService: IEditorDocumentModelService,
editorService: WorkbenchEditorService,
workspaceFS: IWorkspaceFileService,
eventBus: IEventBus,
) {
const options = this.options || {};
if (this.newResource && this.oldResource) {
if (options.copy) {
await workspaceFS.copy([{ source: this.oldResource.codeUri, target: this.newResource.codeUri }], options);
} else {
// rename
await workspaceFS.move([{ source: this.oldResource.codeUri, target: this.newResource.codeUri }], options);
await this.notifyEditor(editorService, documentModelService);
// TODO: 文件夹rename应该带传染性, 但是遍历实现比较坑,先不实现
eventBus.fire(new WorkspaceEditDidRenameFileEvent({ oldUri: this.oldResource, newUri: this.newResource }));
}
if (options.showInEditor) {
editorService.open(this.newResource);
}
} else if (!this.newResource && this.oldResource) {
// 删除文件
try {
// electron windows下moveToTrash大量文件会导致IDE卡死,如果检测到这个情况就不使用moveToTrash
await workspaceFS.delete([this.oldResource], {
useTrash: !(isWindows && this.oldResource.path.name === 'node_modules'),
});
// 默认recursive
await editorService.close(this.oldResource, true);
eventBus.fire(new WorkspaceEditDidDeleteFileEvent({ oldUri: this.oldResource }));
} catch (err) {
if (FileSystemError.FileNotFound.is(err) && options.ignoreIfNotExists) {
// 不抛出错误
} else {
throw err;
}
}
} else if (this.newResource && !this.oldResource) {
// 创建文件
try {
if (options.isDirectory) {
await workspaceFS.createFolder(this.newResource);
} else {
await workspaceFS.create(this.newResource, '', { overwrite: options.overwrite });
}
} catch (err) {
if (FileSystemError.FileExists.is(err) && options.ignoreIfExists) {
// 不抛出错误
} else {
throw err;
}
}
if (!options.isDirectory && options.showInEditor) {
editorService.open(this.newResource);
}
}
}
async revert(): Promise<void> {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# workspaceFS创建文件和目录
最终还是走,fileService中的createFile,createFolder方法;
core/packages/workspace-edit/src/browser/workspace-file.service.ts
public create(resource: URI, contents?: string, options?: { overwrite?: boolean }) {
return this.doCreate(resource, true, contents, options);
}
public createFolder(resource: URI) {
return this.doCreate(resource, false);
}
// 创建文件
try {
if (options.isDirectory) {
await workspaceFS.createFolder(this.newResource);
} else {
await workspaceFS.create(this.newResource, '', { overwrite: options.overwrite });
}
} catch (err) {
if (FileSystemError.FileExists.is(err) && private async doCreate(resource: URI, isFile: boolean, content?: string, options?: { overwrite?: boolean }) {
// file operation participant
await this.runOperationParticipant([{ target: resource.codeUri }], FileOperation.CREATE);
// before events
const event = {
correlationId: this.correlationIds++,
operation: FileOperation.CREATE,
files: [{ target: resource.codeUri }],
};
await this._onWillRunWorkspaceFileOperation.fireAsync(event, CancellationToken.None);
// now actually create on disk
let stat: FileStat;
try {
if (isFile) {
stat = await this.fileService.createFile(resource.toString(), { overwrite: options?.overwrite, content });
} else {
stat = await this.fileService.createFolder(resource.toString());
}
} catch (error) {
// error event
await this._onDidFailWorkspaceFileOperation.fireAsync(event, CancellationToken.None);
throw error;
}
// after event
await this._onDidRunWorkspaceFileOperation.fireAsync(event, CancellationToken.None);
return stat;
} options.ignoreIfExists) {
// 不抛出错误
} else {
throw err;
}
}
if (!options.isDirectory && options.showInEditor) {
editorService.open(this.newResource);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# fileService.createFile
core/packages/file-service/src/browser/file-service-client.ts
async createFile(uri: string, options?: FileCreateOptions) {
const _uri = this.convertUri(uri);
const provider = await this.getProvider(_uri.scheme);
const content = BinaryBuffer.fromString(options?.content || '').buffer;
let newStat: any = await provider.writeFile(_uri.codeUri, content, {
create: true,
overwrite: (options && options.overwrite) || false,
encoding: options?.encoding,
});
newStat = newStat || (await provider.stat(_uri.codeUri));
return newStat;
}
2
3
4
5
6
7
8
9
10
11
12
13
# 编辑保存文件
# 当前文件SAVE_CURRENT
# registerKeybindings
core/packages/editor/src/browser/editor.contribution.ts
registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.registerKeybinding({
command: EDITOR_COMMANDS.SAVE_CURRENT.id,
keybinding: 'ctrlcmd+s',
});
}
commands.registerCommand(EDITOR_COMMANDS.SAVE_CURRENT, {
execute: async () => {
//获取当前编辑器组中的group信息
const group = this.workbenchEditorService.currentEditorGroup;
if (group && group.currentResource) {
group.pin(group.currentResource!.uri);
group.saveCurrent();
}
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# pin/pinPreviewed
core/packages/editor/src/browser/workbench-editor.service.ts
/**
* 每个group只能有一个preview
*/
public previewURI: URI | null = null;
async pin(uri: URI) {
return this.pinPreviewed(uri);
}
pinPreviewed(uri?: URI) {
const previous = this.previewURI;
if (uri === undefined) {
this.previewURI = null;
} else if (this.previewURI && this.previewURI.isEqual(uri)) {
this.previewURI = null;
}
if (previous !== this.previewURI) {
this.notifyTabChanged();
}
}
//保存Tab 加载loading
private notifyTabChanged() {
if (this._restoringState) {
return;
}
this._onDidEditorGroupTabChanged.fire();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# saveCurrent
core/packages/editor/src/browser/workbench-editor.service.ts
async saveCurrent(reason: SaveReason = SaveReason.Manual) {
const resource = this.currentResource;
if (!resource) {
return;
}
if (await this.saveByOpenType(resource, reason)) {
return;
}
if (this.currentEditor) {
return this.currentEditor.save();
}
}
async saveByOpenType(resource: IResource, reason: SaveReason): Promise<boolean> {
const openType = this.cachedResourcesActiveOpenTypes.get(resource.uri.toString());
if (openType && openType.saveResource) {
try {
await openType.saveResource(resource, reason);
return true;
} catch (e) {
this.logger.error(e);
}
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# currentDocumentModel.save
core/packages/editor/src/browser/editor-collection.service.ts
public async save(): Promise<void> {
if (this.currentDocumentModel) {
await this.currentDocumentModel.save();
}
}
2
3
4
5
core/packages/editor/src/browser/doc-model/editor-document-model.ts
保存文档, 如果文档不可保存,则不会有任何反应
async save(force = false, reason: SaveReason = SaveReason.Manual): Promise<boolean> {
await this.formatOnSave(reason);
// 发送willSave并等待完成
await this.eventBus.fireAndAwait(
new EditorDocumentModelWillSaveEvent({
uri: this.uri,
reason,
language: this.languageId,
}),
);
if (!this.editorPreferences['editor.askIfDiff']) {
force = true;
}
if (!this.dirty) {
return false;
}
const versionId = this.monacoModel.getVersionId();
const lastSavingTask = this.savingTasks[this.savingTasks.length - 1];
if (lastSavingTask && lastSavingTask.versionId === versionId) {
return false;
}
const task = new SaveTask(this.uri, versionId, this.monacoModel.getAlternativeVersionId(), this.getText(), force);
this.savingTasks.push(task);
if (this.savingTasks.length === 1) {
this.initSave();
}
const res = await task.finished;
if (res.state === 'success') {
return true;
} else if (res.state === 'error') {
this.logger.error(res.errorMessage);
this.messageService.error(localize('doc.saveError.failed') + '\n' + res.errorMessage);
return false;
} else if (res.state === 'diff') {
this.messageService
.error(formatLocalize('doc.saveError.diff', this.uri.toString()), [localize('doc.saveError.diffAndSave')])
.then((res) => {
if (res) {
this.compareAndSave();
}
});
this.logger.error('文件无法保存,版本和磁盘不一致');
return false;
}
return false;
}
protected async formatOnSave(reason: SaveReason) {
const formatOnSave = this.editorPreferences['editor.formatOnSave'];
// 和 vscode 逻辑保持一致,如果是 AfterDelay 则不执行 formatOnSave
if (formatOnSave && reason !== SaveReason.AfterDelay) {
const formatOnSaveTimeout = this.editorPreferences['editor.formatOnSaveTimeout'] || 3000;
const timer = this.reporter.time(REPORT_NAME.FORMAT_ON_SAVE);
try {
await Promise.race([
new Promise((_, reject) => {
setTimeout(() => {
const err = new Error(formatLocalize('preference.editor.formatOnSaveTimeoutError', formatOnSaveTimeout));
err.name = 'FormatOnSaveTimeoutError';
reject(err);
}, formatOnSaveTimeout);
}),
this.commandService.executeCommand('editor.action.formatDocument'),
]);
} catch (err) {
if (err.name === 'FormatOnSaveTimeoutError') {
this.reporter.point(REPORT_NAME.FORMAT_ON_SAVE_TIMEOUT_ERROR, this.uri.toString());
}
// 目前 command 没有读取到 contextkey,在不支持 format 的地方执行 format 命令会报错,先警告下,后续要接入 contextkey 来判断
this.logger.warn(`${EditorDocumentError.FORMAT_ERROR} ${err && err.message}`);
} finally {
timer.timeEnd(this.uri.path.ext);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# initSave
async initSave() {
while (this.savingTasks.length > 0) {
const changes = this.dirtyChanges;
this.dirtyChanges = [];
const res = await this.savingTasks[0].run(this.service, this.baseContent, changes, this.encoding, this.eol);
if (res.state === 'success' && this.savingTasks[0]) {
this.baseContent = this.savingTasks[0].content;
this.eventBus.fire(new EditorDocumentModelSavedEvent(this.uri));
this.setPersist(this.savingTasks[0].alternativeVersionId);
} else {
// 回滚 changes
this.dirtyChanges.unshift(...changes);
}
this.savingTasks.shift();
}
}
setPersist(versionId: number) {
this._persistVersionId = versionId;
this.notifyChangeEvent([], false, false);
}
private async compareAndSave() {
const originalUri = URI.from({
scheme: ORIGINAL_DOC_SCHEME,
query: URI.stringifyQuery({
target: this.uri.toString(),
}),
});
const fileName = this.uri.path.base;
const res = await this.compareService.compare(
originalUri,
this.uri,
formatLocalize('editor.compareAndSave.title', fileName, fileName),
);
if (res === CompareResult.revert) {
this.revert();
} else if (res === CompareResult.accept) {
this.save(true);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# task队列 run
core/packages/editor/src/browser/doc-model/save-task.ts
export class SaveTask {
private deferred: Deferred<IEditorDocumentModelSaveResult> = new Deferred();
public finished: Promise<IEditorDocumentModelSaveResult> = this.deferred.promise;
public started = false;
constructor(
private uri: URI,
public readonly versionId: number,
public readonly alternativeVersionId: number,
public content: string,
private ignoreDiff: boolean,
) {}
async run(
service: IEditorDocumentModelServiceImpl,
baseContent: string,
changes: IEditorDocumentChange[],
encoding?: string,
eol?: EOL,
): Promise<IEditorDocumentModelSaveResult> {
this.started = true;
try {
const res = await service.saveEditorDocumentModel(
this.uri,
this.content,
baseContent,
changes,
encoding,
this.ignoreDiff,
eol,
);
this.deferred.resolve(res);
return res;
} catch (e) {
const res = {
errorMessage: e.message,
state: 'error',
} as any;
this.deferred.resolve(res);
return res;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# saveEditorDocumentModel
core/packages/editor/src/browser/doc-model/editor-document-model-service.ts
async saveEditorDocumentModel(
uri: URI,
content: string,
baseContent: string,
changes: IEditorDocumentChange[],
encoding?: string,
ignoreDiff?: boolean,
eol?: EOL,
): Promise<IEditorDocumentModelSaveResult> {
const provider = await this.contentRegistry.getProvider(uri);
if (!provider) {
throw new Error(`未找到${uri.toString()}的文档提供商`);
}
if (!provider.saveDocumentModel) {
throw new Error(`${uri.toString()}的文档提供商不存在保存方法`);
}
const result = await provider.saveDocumentModel(uri, content, baseContent, changes, encoding, ignoreDiff, eol);
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# saveDocumentModel
入口一:目前是这个入口;
core/packages/file-scheme/src/browser/file-doc.ts
async saveDocumentModel(
uri: URI,
content: string,
baseContent: string,
changes: IEditorDocumentChange[],
encoding: string,
ignoreDiff = false,
eol: EOL = EOL.LF,
): Promise<IEditorDocumentModelSaveResult> {
const baseMd5 = this.hashCalculateService.calculate(baseContent);
if (content.length > FILE_SAVE_BY_CHANGE_THRESHOLD) {
return await this.fileSchemeDocClient.saveByChange(
uri.toString(),
{
baseMd5,
changes,
eol,
},
encoding,
ignoreDiff,
);
} else {
return await this.fileSchemeDocClient.saveByContent(
uri.toString(),
{
baseMd5,
content,
},
encoding,
ignoreDiff,
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# saveByContent
core/packages/file-scheme/src/browser/file-scheme-doc.client.ts
saveByContent(
uri: string,
content: ISavingContent,
encoding?: string | undefined,
force?: boolean | undefined,
): Promise<IEditorDocumentModelSaveResult> {
return this.fileDocBackendService.$saveByContent(uri, content, encoding, force);
}
2
3
4
5
6
7
8
# $saveByContent
core/packages/file-scheme/src/browser/file-scheme-doc.client.ts
@Injectable()
export class FileSchemeDocClientService implements IFileSchemeDocClient {
@Autowired(FileSchemeDocNodeServicePath)
protected readonly fileDocBackendService: IFileSchemeDocNodeService;
saveByChange(
uri: string,
change: IContentChange,
encoding?: string | undefined,
force?: boolean | undefined,
): Promise<IEditorDocumentModelSaveResult> {
return this.fileDocBackendService.$saveByChange(uri, change, encoding, force);
}
saveByContent(
uri: string,
content: ISavingContent,
encoding?: string | undefined,
force?: boolean | undefined,
): Promise<IEditorDocumentModelSaveResult> {
return this.fileDocBackendService.$saveByContent(uri, content, encoding, force);
}
getMd5(uri: string, encoding?: string | undefined): Promise<string | undefined> {
return this.fileDocBackendService.$getMd5(uri, encoding);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
core/packages/file-scheme/src/node/file-scheme-doc.service.ts
async $saveByContent(
uri: string,
content: ISavingContent,
encoding?: string | undefined,
force = false,
): Promise<IEditorDocumentModelSaveResult> {
try {
const stat = await this.fileService.getFileStat(uri);
if (stat) {
if (!force) {
const res = await this.fileService.resolveContent(uri, { encoding });
if (content.baseMd5 !== this.hashCalculateService.calculate(res.content)) {
return {
state: 'diff',
};
}
}
await this.fileService.setContent(stat, content.content, { encoding });
return {
state: 'success',
};
} else {
await this.fileService.createFile(uri, { content: content.content, encoding });
return {
state: 'success',
};
}
} catch (e) {
return {
state: 'error',
errorMessage: e.toString(),
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
入口二:
core/packages/editor/src/browser/fs-resource/fs-editor-doc.ts
async saveDocumentModel(
uri: URI,
content: string,
baseContent: string,
changes: IEditorDocumentChange[],
encoding: string,
ignoreDiff = false,
): Promise<IEditorDocumentModelSaveResult> {
// 默认的文件系统都直接存 content
try {
const fileStat = await this.fileServiceClient.getFileStat(uri.toString());
if (!fileStat) {
await this.fileServiceClient.createFile(uri.toString(), { content, overwrite: true, encoding });
} else {
await this.fileServiceClient.setContent(fileStat, content, { encoding });
}
return {
state: 'success',
};
} catch (e) {
return {
state: 'error',
errorMessage: e.message,
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 功能文件处理方法【createFile】
core/packages/file-service/src/browser/file-service-client.ts
async getFileStat(uri: string, withChildren = true) {
const _uri = this.convertUri(uri);
const provider = await this.getProvider(_uri.scheme);
try {
const stat = await provider.stat(_uri.codeUri);
if (!stat) {
throw FileSystemError.FileNotFound(_uri.codeUri.toString(), 'File not found.');
}
return this.filterStat(stat, withChildren);
} catch (err) {
if (FileSystemError.FileNotFound.is(err)) {
return undefined;
}
}
}
async setContent(file: FileStat, content: string | Uint8Array, options?: FileSetContentOptions) {
const _uri = this.convertUri(file.uri);
const provider = await this.getProvider(_uri.scheme);
const stat = await provider.stat(_uri.codeUri);
if (!stat) {
throw FileSystemError.FileNotFound(file.uri, 'File not found.');
}
if (stat.isDirectory) {
throw FileSystemError.FileIsDirectory(file.uri, 'Cannot set the content.');
}
if (!(await this.isInSync(file, stat))) {
throw this.createOutOfSyncError(file, stat);
}
await provider.writeFile(
_uri.codeUri,
typeof content === 'string' ? BinaryBuffer.fromString(content).buffer : content,
{ create: false, overwrite: true, encoding: options?.encoding },
);
const newStat = await provider.stat(_uri.codeUri);
return newStat;
}
async createFile(uri: string, options?: FileCreateOptions) {
const _uri = this.convertUri(uri);
const provider = await this.getProvider(_uri.scheme);
const content = BinaryBuffer.fromString(options?.content || '').buffer;
let newStat: any = await provider.writeFile(_uri.codeUri, content, {
create: true,
overwrite: (options && options.overwrite) || false,
encoding: options?.encoding,
});
newStat = newStat || (await provider.stat(_uri.codeUri));
return newStat;
}
async createFolder(uri: string): Promise<FileStat> {
const _uri = this.convertUri(uri);
const provider = await this.getProvider(_uri.scheme);
const result = await provider.createDirectory(_uri.codeUri);
if (result) {
return result;
}
const stat = await provider.stat(_uri.codeUri);
return stat as FileStat;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# provider.writeFile
最终调用到node端处理;
core/packages/file-service/src/node/disk-file-system.provider.ts
async writeFile(
uri: UriComponents,
content: Uint8Array,
options: { create: boolean; overwrite: boolean; encoding?: string },
): Promise<void | FileStat> {
const _uri = Uri.revive(uri);
const exists = await this.access(uri);
if (exists && !options.overwrite) {
throw FileSystemError.FileExists(_uri.toString());
} else if (!exists && !options.create) {
throw FileSystemError.FileNotFound(_uri.toString());
}
// fileServiceNode调用不会转换,前传通信会转换
const buffer = content instanceof Buffer ? content : Buffer.from(Uint8Array.from(content));
if (options.create) {
return await this.createFile(uri, { content: buffer });
}
try {
await writeFileAtomic(FileUri.fsPath(new URI(_uri)), buffer);
} catch (e) {
getDebugLogger().warn('writeFileAtomicSync 出错,使用 fs', e);
await fse.writeFile(FileUri.fsPath(new URI(_uri)), buffer);
}
}
// Protected or private
protected async createFile(uri: UriComponents, options: { content: Buffer }): Promise<FileStat> {
const _uri = Uri.revive(uri);
const parentUri = new URI(_uri).parent;
const parentStat = await this.doGetStat(parentUri.codeUri, 0);
if (!parentStat) {
await fse.ensureDir(FileUri.fsPath(parentUri));
}
await fse.writeFile(FileUri.fsPath(_uri.toString()), options.content);
const newStat = await this.doGetStat(_uri, 1);
if (newStat) {
return newStat;
}
throw FileSystemError.FileNotFound(uri.path, 'Error occurred while creating the file.');
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 绑定provider初始化地方
core/packages/file-service/src/browser/file-service-contribution.ts
// 常规文件资源读取
@Domain(ClientAppContribution)
export class FileServiceContribution implements ClientAppContribution {
@Autowired(IFileServiceClient)
protected readonly fileSystem: FileServiceClient;
@Autowired(IDiskFileProvider)
private diskFileServiceProvider: IDiskFileProvider;
@Autowired(FsProviderContribution)
contributionProvider: ContributionProvider<FsProviderContribution>;
constructor() {
// 初始化资源读取逻辑,需要在最早初始化时注册
// 否则后续注册的 debug\user_stroage 等将无法正常使用
this.fileSystem.registerProvider(Schemes.file, this.diskFileServiceProvider);
}
async initialize() {
const fsProviderContributions = this.contributionProvider.getContributions();
for (const contribution of fsProviderContributions) {
contribution.registerProvider && (await contribution.registerProvider(this.fileSystem));
}
for (const contribution of fsProviderContributions) {
contribution.onFileServiceReady && (await contribution.onFileServiceReady());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 代码对比
compareAndSave
core/packages/editor/src/browser/doc-model/editor-document-model.ts
private async compareAndSave() {
const originalUri = URI.from({
scheme: ORIGINAL_DOC_SCHEME,
query: URI.stringifyQuery({
target: this.uri.toString(),
}),
});
const fileName = this.uri.path.base;
const res = await this.compareService.compare(
originalUri,
this.uri,
formatLocalize('editor.compareAndSave.title', fileName, fileName),
);
if (res === CompareResult.revert) {
this.revert();
} else if (res === CompareResult.accept) {
this.save(true);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# compare
core/packages/editor/src/browser/diff/compare.ts
compare(original: URI, modified: URI, name: string): Promise<CompareResult> {
const compareUri = URI.from({
scheme: 'diff',
query: URI.stringifyQuery({
name,
original,
modified,
comparing: true,
}),
});
if (!this.comparing.has(compareUri.toString())) {
const deferred = new Deferred<CompareResult>();
this.comparing.set(compareUri.toString(), deferred);
deferred.promise.then(() => {
this.comparing.delete(compareUri.toString());
this.commandService.executeCommand(EDITOR_COMMANDS.CLOSE_ALL.id, compareUri);
});
}
this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, compareUri);
return this.comparing.get(compareUri.toString())!.promise;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# registerCommand
回到上面的打开命令处理:
commands.registerCommand(EDITOR_COMMANDS.OPEN_RESOURCE, {
execute: async (uri: URI, options?: IResourceOpenOptions) => {
const openResult = await this.workbenchEditorService.open(uri, options);
if (openResult) {
return {
groupId: openResult?.group.name,
};
}
},
});
2
3
4
5
6
7
8
9
10
# revert
async revert(notOnDisk?: boolean) {
if (notOnDisk) {
// FIXME: 暂时就让它不 dirty, 不是真正的 revert
this._persistVersionId = this.monacoModel.getAlternativeVersionId();
} else {
// 利用修改编码的副作用
await this.updateEncoding(this._originalEncoding);
}
}
2
3
4
5
6
7
8
9
10
# updateEncoding
async updateEncoding(encoding: string) {
let shouldFireChange = false;
if (this._encoding !== encoding) {
shouldFireChange = true;
}
this._encoding = encoding;
await this.reload();
if (shouldFireChange) {
this.eventBus.fire(
new EditorDocumentModelOptionChangedEvent({
uri: this.uri,
encoding: this._encoding,
}),
);
this._onDidChangeEncoding.fire();
}
}
async reload() {
try {
const content = await this.contentRegistry.getContentForUri(this.uri, this._encoding);
if (!isUndefinedOrNull(content)) {
this.cleanAndUpdateContent(content);
}
} catch (e) {
this._persistVersionId = this.monacoModel.getAlternativeVersionId();
}
}
cleanAndUpdateContent(content) {
this.monacoModel.setValue(content);
(this.monacoModel as any)._commandManager.clear();
this._persistVersionId = this.monacoModel.getAlternativeVersionId();
this.savingTasks = [];
this.notifyChangeEvent([], false, false);
this.baseContent = content;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 特定路径文件SAVE_URI
core/packages/editor/src/browser/editor.contribution.ts
SAVE_URI 好像系统暂时没有开放
commands.registerCommand(EDITOR_COMMANDS.SAVE_URI, {
execute: async (uri: URI) => {
for (const g of this.workbenchEditorService.editorGroups) {
const r = g.resources.find((r) => r.uri.isEqual(uri));
if (r) {
g.saveResource(r);
}
}
},
});
2
3
4
5
6
7
8
9
10
保存资源
async saveResource(resource: IResource, reason: SaveReason = SaveReason.Manual) {
// 尝试使用 openType 提供的保存方法保存
if (await this.saveByOpenType(resource, reason)) {
return;
}
// 否则使用 document 进行保存 (如果有)
const docRef = this.documentModelManager.getModelReference(resource.uri);
if (docRef) {
if (docRef.instance.dirty) {
await docRef.instance.save(undefined, reason);
}
docRef.dispose();
}
}
async saveByOpenType(resource: IResource, reason: SaveReason): Promise<boolean> {
const openType = this.cachedResourcesActiveOpenTypes.get(resource.uri.toString());
if (openType && openType.saveResource) {
try {
await openType.saveResource(resource, reason);
return true;
} catch (e) {
this.logger.error(e);
}
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 搜索文件并显示
功能主要集中在:core/packages/search
和core/packages/components/src/recycle-tree
两个模块;
# 树组件头部搜索
# Filter字段修改触发
RecycleTreeFilterDecorator
core/packages/components/src/recycle-tree/decorators/Filter/index.tsx
const handleFilterChange = throttle(async (value: string) => {
if (beforeFilterValueChange) {
await beforeFilterValueChange(value);
}
setFilter(value);
}, TREE_FILTER_DELAY);
const handleFilterInputChange = (value: string) => {
setValue(value);
handleFilterChange(value);
};
return (
<>
{filterEnabled && (
<FilterInput
afterClear={filterAfterClear}
placeholder={filterPlaceholder}
value={value}
autoFocus={filterAutoFocus}
onValueChange={handleFilterInputChange}
/>
)}
{React.createElement(recycleTreeComp, {
...recycleTreeProps,
height: height - (filterEnabled ? FILTER_AREA_HEIGHT : 0),
onReady: filterTreeReadyHandle,
filter,
})}
</>
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# UNSAFE_componentWillUpdate
core/packages/components/src/recycle-tree/RecycleTree.tsx
const FilterableRecycleTree = RecycleTreeFilterDecorator(RecycleTree);
public UNSAFE_componentWillUpdate(prevProps: IRecycleTreeProps) {
if (this.props.filter !== prevProps.filter) {
this.filterItems(prevProps.filter!);
}
if (this.props.model !== prevProps.model) {
// model变化时,在渲染前清理缓存
this.idxToRendererPropsCache.clear();
this.idToFilterRendererPropsCache.clear();
this.dynamicSizeMap.clear();
}
}
// 过滤Root节点展示
private filterItems = (filter: string) => {
const {
model: { root },
filterProvider,
} = this.props;
this.filterWatcherDisposeCollection.dispose();
this.idToFilterRendererPropsCache.clear();
if (!filter) {
return;
}
const isPathFilter = /\//.test(filter);
const idSets: Set<number> = new Set();
const idToRenderTemplate: Map<number, any> = new Map();
const nodes: TreeNode[] = [];
for (let idx = 0; idx < root.branchSize; idx++) {
const node = root.getTreeNodeAtIndex(idx)!;
nodes.push(node as TreeNode);
}
if (isPathFilter) {
nodes.forEach((node) => {
if (node && node.path.indexOf(filter) > -1) {
idSets.add(node.id);
let parent = node.parent;
// 不应包含根节点
while (parent && !CompositeTreeNode.isRoot(parent)) {
idSets.add(parent.id);
parent = parent.parent;
}
}
});
} else {
let fuzzyLists: fuzzy.FilterResult<TreeNode>[] = [];
if (filterProvider) {
fuzzyLists = fuzzy.filter(filter, nodes, filterProvider.fuzzyOptions());
} else {
fuzzyLists = fuzzy.filter(filter, nodes, RecycleTree.FILTER_FUZZY_OPTIONS);
}
fuzzyLists.forEach((item) => {
const node = (item as any).original as TreeNode;
idSets.add(node.id);
let parent = node.parent;
idToRenderTemplate.set(node.id, () => (
<div
style={{
flex: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
dangerouslySetInnerHTML={{ __html: item.string || '' }}
></div>
));
// 不应包含根节点
while (parent && !CompositeTreeNode.isRoot(parent)) {
idSets.add(parent.id);
parent = parent.parent;
}
});
}
this.filterFlattenBranch = new Array(idSets.size);
for (let flatTreeIdx = 0, idx = 0; idx < root.branchSize; idx++) {
const node = root.getTreeNodeAtIndex(idx);
if (node && idSets.has(node.id)) {
this.filterFlattenBranch[flatTreeIdx] = node.id;
if (CompositeTreeNode.is(node)) {
this.idToFilterRendererPropsCache.set(node.id, {
item: node as CompositeTreeNode,
itemType: TreeNodeType.CompositeTreeNode,
template: idToRenderTemplate.has(node.id) ? idToRenderTemplate.get(node.id) : undefined,
});
} else {
this.idToFilterRendererPropsCache.set(node.id, {
item: node as TreeNode,
itemType: TreeNodeType.TreeNode,
template: idToRenderTemplate.has(node.id) ? idToRenderTemplate.get(node.id) : undefined,
});
}
flatTreeIdx++;
}
}
// 根据折叠情况变化裁剪filterFlattenBranch
this.filterWatcherDisposeCollection.push(
root.watcher.on(TreeNodeEvent.DidChangeExpansionState, (target, nowExpanded) => {
const expandItemIndex = this.filterFlattenBranch.indexOf(target.id);
if (!nowExpanded) {
const collapesArray: number[] = [];
for (let i = expandItemIndex + 1; i < this.filterFlattenBranch.length; i++) {
const node = root.getTreeNodeById(this.filterFlattenBranch[i]);
if (node && node.depth > target.depth) {
collapesArray.push(node.id);
} else {
break;
}
}
this.filterFlattenBranchChildrenCache.set(target.id, collapesArray);
this.filterFlattenBranch = spliceArray(this.filterFlattenBranch, expandItemIndex + 1, collapesArray.length);
} else {
const spliceUint32Array = this.filterFlattenBranchChildrenCache.get(target.id);
if (spliceUint32Array && spliceUint32Array.length > 0) {
this.filterFlattenBranch = spliceArray(this.filterFlattenBranch, expandItemIndex + 1, 0, spliceUint32Array);
this.filterFlattenBranchChildrenCache.delete(target.id);
}
}
}),
);
this.filterWatcherDisposeCollection.push(
Disposable.create(() => {
this.filterFlattenBranchChildrenCache.clear();
}),
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# 列表打开功能
- 是所有文件都已经存到内存中,从内存搜索的?
- 还是从磁盘一个一个打开搜索的?
- 还是利用磁盘的文件搜索功能?
列表显示,及打开文件功能
core/packages/addons/src/browser/file-search.contribution.ts
protected async getQueryFiles(fileQuery: string, alreadyCollected: Set<string>, token: CancellationToken) {
const roots = await this.workspaceService.roots;
const rootUris: string[] = [];
roots.forEach((stat) => {
const uri = new URI(stat.uri);
if (uri.scheme !== Schemes.file) {
return;
}
this.logger.debug('file-search.contribution rootUri', uri.toString());
return rootUris.push(uri.toString());
});
const files = await this.fileSearchService.find(
fileQuery,
{
rootUris,
fuzzyMatch: true,
limit: DEFAULT_FILE_SEARCH_LIMIT,
useGitIgnore: true,
noIgnoreParent: true,
excludePatterns: ['*.git*', ...this.getPreferenceSearchExcludes()],
},
token,
);
const results = await this.getItems(
files.filter((uri: string) => {
if (alreadyCollected.has(uri) || token.isCancellationRequested) {
return false;
}
alreadyCollected.add(uri);
return true;
}),
{},
);
results.sort(this.compareItems.bind(this));
return results;
}
private async getItems(uriList: string[], options: { [key: string]: any }) {
const items: QuickOpenItem[] = [];
for (const [index, strUri] of uriList.entries()) {
const uri = new URI(strUri);
const icon = `file-icon ${await this.labelService.getIcon(uri.withoutFragment())}`;
const description = await this.workspaceService.asRelativePath(uri.parent.withoutFragment());
const item = new QuickOpenItem({
uri,
label: uri.displayName,
tooltip: strUri,
iconClass: icon,
description,
groupLabel: index === 0 ? options.groupLabel : '',
showBorder: uriList.length > 0 && index === 0 ? options.showBorder : false,
run: (mode: Mode) => {
if (mode === Mode.PREVIEW) {
this.prevSelected = uri;
}
if (mode === Mode.OPEN) {
this.openFile(uri);
return true;
}
return false;
},
});
items.push(item);
}
return items;
}
private openFile(uri: URI) {
const filePath = uri.path.toString();
// 优先从输入上获取 line 和 column
let range = getRangeByInput(this.currentLookFor);
if (!range || (!range.startLineNumber && !range.startColumn)) {
range = getRangeByInput(uri.fragment ? filePath + '#' + uri.fragment : filePath);
}
this.currentLookFor = '';
this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, uri.withoutFragment(), {
preview: false,
range,
focus: true,
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# 高亮实现
# 文件服务
# 设置忽略
# 初始化
core/packages/file-tree-next/src/browser/file-tree-contribution.ts
initialize() {
// 等待排除配置初始化结束后再初始化文件树
this.workspaceService.initFileServiceExclude().then(async () => {
await this.fileTreeService.init();
this.fileTreeModelService.initTreeModel();
});
}
async onStart() {
// 监听工作区变化更新标题
this.workspaceService.onWorkspaceLocationChanged(() => {
const handler = this.mainLayoutService.getTabbarHandler(EXPLORER_CONTAINER_ID);
if (handler) {
handler.updateViewTitle(RESOURCE_VIEW_ID, this.getWorkspaceTitle());
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async init() {
this.toDispose.push(
this.workspaceService.onWorkspaceFileExcludeChanged(() => {
this.refresh();
}),
);
}
/**
* 刷新指定下的所有子节点;默认直接刷新根节点
*/
async refresh(node: Directory = this.root as Directory) {
// 如果正在刷新,就不要创建新的 Defer
// 否则会导致下面的 callback 闭包 resolve 的仍然是之前捕获的旧 defer
if (!this.willRefreshDeferred) {
this.willRefreshDeferred = new Deferred();
}
if (!node) {
return;
}
if (!Directory.is(node) && node.parent) {
node = node.parent as Directory;
}
// 队列化刷新动作减少更新成本
this._changeEventDispatchQueue.add(node.path);
return this.doHandleQueueChange();
}
private doHandleQueueChange = throttle(
async () => {
try {
// 询问是否此时可进行刷新事件
await this.requestFlushEventSignalEmitter.fireAndAwait();
await this.flushEventQueue();
} catch (error) {
this.logger.error('flush file change event queue error:', error);
} finally {
this.onNodeRefreshedEmitter.fire();
this.willRefreshDeferred?.resolve();
this.willRefreshDeferred = null;
}
},
FileTreeService.DEFAULT_REFRESH_DELAY,
{
leading: true,
trailing: true,
},
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
core/packages/workspace/src/browser/workspace-service.ts
roots也相关初始化了;
protected async setFileServiceExcludes() {
const watchExcludeName = 'files.watcherExclude';
const filesExcludeName = 'files.exclude';
await this.preferenceService.ready;
await this.fileServiceClient.setWatchFileExcludes(this.getFlattenExcludes(watchExcludeName));
await this.fileServiceClient.setFilesExcludes(
this.getFlattenExcludes(filesExcludeName),
this._roots.map((stat) => stat.uri),
);
//发送通知上面刷新页面
this.onWorkspaceFileExcludeChangeEmitter.fire();
}
protected readonly onWorkspaceFileExcludeChangeEmitter = new Emitter<void>();
get onWorkspaceFileExcludeChanged(): Event<void> {
return this.onWorkspaceFileExcludeChangeEmitter.event;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
core/packages/file-service/src/browser/file-service-client.ts
async getWatchFileExcludes() {
const provider = await this.getProvider(Schemes.file);
return await provider.getWatchFileExcludes();
}
async setFilesExcludes(excludes: string[], roots?: string[]): Promise<void> {
this.filesExcludes = excludes;
// 先初始化,下面更新时用到;updateExcludeMatcher
this.filesExcludesMatcherList = [];
if (roots) {
this.setWorkspaceRoots(roots);
}
this.updateExcludeMatcher();
}
async setWorkspaceRoots(roots: string[]) {
this.workspaceRoots = roots;
// FIXME:这里重复调用
// this.updateExcludeMatcher();
}
private updateExcludeMatcher() {
this.filesExcludes.forEach((str) => {
//多个工作空间情况下
if (this.workspaceRoots.length > 0) {
this.workspaceRoots.forEach((root: string) => {
const uri = new URI(root);
const pathStrWithExclude = uri.resolve(str).path.toString();
this.filesExcludesMatcherList.push(parseGlob(pathStrWithExclude));
});
} else {
this.filesExcludesMatcherList.push(parseGlob(str));
}
});
}
//在后面的检查查询文件时,用到;
private isExclude(uriString: string) {
const uri = new URI(uriString);
return this.filesExcludesMatcherList.some((matcher) => matcher(uri.path.toString()));
}
private filterStat(stat?: FileStat, withChildren = true) {
if (!stat) {
return;
}
if (this.isExclude(stat.uri)) {
return;
}
// 这里传了 false 就走不到后面递归逻辑了
if (stat.children && withChildren) {
stat.children = this.filterStatChildren(stat.children);
}
return stat;
}
private filterStatChildren(children: FileStat[]) {
const list: FileStat[] = [];
children.forEach((child) => {
if (this.isExclude(child.uri)) {
return false;
}
const state = this.filterStat(child);
if (state) {
list.push(state);
}
});
return list;
}
async getFileStat(uri: string, withChildren = true) {
const _uri = this.convertUri(uri);
const provider = await this.getProvider(_uri.scheme);
try {
const stat = await provider.stat(_uri.codeUri);
if (!stat) {
throw FileSystemError.FileNotFound(_uri.codeUri.toString(), 'File not found.');
}
return this.filterStat(stat, withChildren);
} catch (err) {
if (FileSystemError.FileNotFound.is(err)) {
return undefined;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
回到客户端调用处:
core/packages/file-tree-next/src/browser/services/file-tree-api.service.ts
async resolveChildren(tree: IFileTreeService, path: string | FileStat, parent?: Directory, compact?: boolean) {
let file: FileStat | undefined;
if (!this.userhomePath) {
const userhome = await this.fileServiceClient.getCurrentUserHome();
if (userhome) {
this.userhomePath = new URI(userhome.uri);
}
}
if (typeof path === 'string') {
file = await this.fileServiceClient.getFileStat(path);
} else {
file = await this.fileServiceClient.getFileStat(path.uri);
}
if (file) {
if (file.children?.length === 1 && file.children[0].isDirectory && compact) {
const parentURI = new URI(file.children[0].uri);
if (!!parent && parent.parent) {
const parentName = (parent.parent as Directory).uri.relative(parentURI)?.toString();
if (parentName && parentName !== parent.name) {
parent.updateMetaData({
name: parentName,
uri: parentURI,
fileStat: file.children[0],
tooltip: this.getReadableTooltip(parentURI),
});
}
}
return await this.resolveChildren(tree, file.children[0].uri, parent, compact);
} else {
// 为文件树节点新增isInSymbolicDirectory属性,用于探测节点是否处于软链接文件中
const filestat = {
...file,
isInSymbolicDirectory: parent?.filestat.isSymbolicLink || parent?.filestat.isInSymbolicDirectory,
};
return {
children: this.toNodes(tree, filestat, parent),
filestat,
};
}
} else {
return {
children: [],
filestat: null,
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 监听忽略
core/packages/file-service/src/node/disk-file-system.provider.ts
// 出于通信成本的考虑,排除文件的逻辑必须放在node层(fs provider层,不同的fs实现的exclude应该不一样)
setWatchFileExcludes(excludes: string[]) {
let watcherExcludes = excludes;
// 兼容 Windows 下对 node_modules 默认排除监听的逻辑
// 由于 files.watcherExclude 允许用户手动修改,所以只对默认值做处理
// 在 Windows 下将 **/node_modules/**/* 替换为 **/node_modules/*/**
if (isWindows && excludes.includes(UNIX_DEFAULT_NODE_MODULES_EXCLUDE)) {
const idx = watcherExcludes.findIndex((v) => v === UNIX_DEFAULT_NODE_MODULES_EXCLUDE);
watcherExcludes = watcherExcludes.splice(idx, 1, WINDOWS_DEFAULT_NODE_MODULES_EXCLUDE);
}
getDebugLogger().info('set watch file exclude:', watcherExcludes);
this.watchFileExcludes = watcherExcludes;
this.watchFileExcludesMatcherList = watcherExcludes.map((pattern) => parseGlob(pattern));
}
constructor() {
super();
this.initWatcher();
}
//在构造时,就初始化监听
protected initWatcher() {
this.watcherServer = new NsfwFileSystemWatcherServer({
verbose: true,
});
this.watcherServer.setClient({
onDidFilesChanged: (events: DidFilesChangedParams) => {
const filteredChange = events.changes.filter((file) => {
const uri = new URI(file.uri);
const pathStr = uri.path.toString();
return !this.watchFileExcludesMatcherList.some((match) => match(pathStr));
});
if (filteredChange.length > 0) {
this.fileChangeEmitter.fire(filteredChange);
if (Array.isArray(this.rpcClient)) {
this.rpcClient.forEach((client) => {
client.onDidFilesChanged({
changes: filteredChange,
});
});
}
}
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 监听
初始化
core/packages/file-tree-next/src/browser/services/file-tree-model.service.ts
async initTreeModel() {
this._initTreeModelReady = false;
// 根据是否为多工作区创建不同根节点
const root = (await this.fileTreeService.resolveChildren())[0];
this._explorerStorage = await this.storageProvider(STORAGE_NAMESPACE.EXPLORER);
// 获取上次文件树的状态
const snapshot = this.explorerStorage.get<ISerializableState>(FileTreeModelService.FILE_TREE_SNAPSHOT_KEY);
if (snapshot) {
// 初始化时。以右侧编辑器打开的文件进行定位
await this.loadFileTreeSnapshot(snapshot);
}
// 完成首次文件树快照恢复后再进行 Tree 状态变化的更新
this.disposableCollection.push(
this.treeStateWatcher.onDidChange(() => {
if (!this._initTreeModelReady) {
return;
}
const snapshot = this.explorerStorage.get<any>(FileTreeModelService.FILE_TREE_SNAPSHOT_KEY);
const currentTreeSnapshot = this.treeStateWatcher.snapshot();
this.explorerStorage.set(FileTreeModelService.FILE_TREE_SNAPSHOT_KEY, {
...snapshot,
...currentTreeSnapshot,
});
}),
);
//初始化监听
await this.fileTreeService.startWatchFileEvent();
this.onFileTreeModelChangeEmitter.fire(this._treeModel);
this._whenReady.resolve();
this._initTreeModelReady = true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
core/packages/file-tree-next/src/browser/file-tree.service.ts
两个地方处理监听:
//第一个地方: 初始化后启动监听
public startWatchFileEvent() {
this._readyToWatch = true;
return Promise.all(
this._watchRootsQueue.map(async (uri) => {
await this.watchFilesChange(uri);
}),
);
}
private async onFilesChanged(changes: FileChange[]) {
const nodes = await this.getAffectedNodes(this.getAffectedChanges(changes));
if (nodes.length > 0) {
this.effectedNodes = this.effectedNodes.concat(nodes);
} else if (!(nodes.length > 0) && this.isRootAffected(changes)) {
this.effectedNodes.push(this.root as Directory);
}
// 文件事件引起的刷新进行队列化处理,每 200 ms 处理一次刷新任务
return this.refreshThrottler.queue(this.doDelayRefresh.bind(this));
}
//第二个地方:过滤过去到目录后,添加监听到文件处理;
else {
if (this._roots.length > 0) {
children = await (await this.fileTreeAPI.resolveChildren(this, this._roots[0])).children;
children.forEach((child) => {
// 根据workspace更新Root名称
const rootName = this.workspaceService.getWorkspaceName(child.uri);
if (rootName && rootName !== child.name) {
(child as Directory).updateMetaData({
name: rootName,
});
}
});
this.watchFilesChange(new URI(this._roots[0].uri));
this.root = children[0] as Directory;
return children;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
core/packages/file-tree-next/src/browser/file-tree.service.ts
async watchFilesChange(uri: URI) {
if (!this._readyToWatch) {
this._watchRootsQueue.push(uri);
return;
}
const watcher = await this.fileServiceClient.watchFileChanges(uri);
this.toDispose.push(watcher);
this.toDispose.push(
watcher.onFilesChanged((changes: FileChange[]) => {
this.onFilesChanged(changes);
}),
);
this._fileServiceWatchers.set(uri.toString(), watcher);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
core/packages/file-service/src/browser/file-service-client.ts
// 添加监听文件
async watchFileChanges(uri: URI, excludes?: string[]): Promise<IFileServiceWatcher> {
const _uri = this.convertUri(uri.toString());
if (this.uriWatcherMap.has(_uri.toString())) {
return this.uriWatcherMap.get(_uri.toString())!;
}
const id = this.watcherId++;
const provider = await this.getProvider(_uri.scheme);
const schemaWatchIdList = this.watcherWithSchemaMap.get(_uri.scheme) || [];
const watcherId = await provider.watch(_uri.codeUri, {
recursive: true,
excludes,
});
this.watcherDisposerMap.set(id, {
dispose: () => {
provider.unwatch && provider.unwatch(watcherId);
this.uriWatcherMap.delete(_uri.toString());
},
});
schemaWatchIdList.push(id);
this.watcherWithSchemaMap.set(_uri.scheme, schemaWatchIdList);
const watcher = new FileSystemWatcher({
fileServiceClient: this,
watchId: id,
uri,
});
this.uriWatcherMap.set(_uri.toString(), watcher);
return watcher;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
core/packages/file-service/src/node/disk-file-system.provider.ts
监听/停止监听
/**
* @param {Uri} uri
* @param {{ recursive: boolean; excludes: string[] }} [options] // 还不支持 recursive 参数
* @memberof DiskFileSystemProvider
*/
async watch(uri: UriComponents, options?: { recursive: boolean; excludes?: string[] }): Promise<number> {
const _uri = Uri.revive(uri);
const id = await this.watcherServer.watchFileChanges(_uri.toString(), {
excludes: options?.excludes ?? [],
});
const disposable = {
dispose: () => {
this.watcherServer.unwatchFileChanges(id);
},
};
this.watcherDisposerMap.set(id, disposable);
return id;
}
unwatch(watcherId: number) {
const disposable = this.watcherDisposerMap.get(watcherId);
if (!disposable || !disposable.dispose) {
return;
}
disposable.dispose();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
core/packages/file-service/src/node/file-service-watcher.ts
/**
* 如果监听路径不存在,则会监听父目录
* @param uri 要监听的路径
* @param options
* @returns
*/
async watchFileChanges(uri: string, options?: WatchOptions): Promise<number> {
const basePath = FileUri.fsPath(uri);
let realpath;
if (await fs.pathExists(basePath)) {
realpath = basePath;
}
let watcherId = realpath && this.checkIsAlreadyWatched(realpath);
if (watcherId) {
return watcherId;
}
watcherId = this.watcherSequence++;
this.debug('Starting watching:', basePath, options);
const toDisposeWatcher = new DisposableCollection();
if (await fs.pathExists(basePath)) {
this.watchers.set(watcherId, {
path: realpath,
disposable: toDisposeWatcher,
});
toDisposeWatcher.push(Disposable.create(() => this.watchers.delete(watcherId)));
await this.start(watcherId, basePath, options, toDisposeWatcher);
} else {
const watchPath = await this.lookup(basePath);
if (watchPath) {
const existingWatcher = watchPath && this.checkIsAlreadyWatched(watchPath);
if (existingWatcher) {
return existingWatcher;
}
this.watchers.set(watcherId, {
path: watchPath,
disposable: toDisposeWatcher,
});
toDisposeWatcher.push(Disposable.create(() => this.watchers.delete(watcherId)));
await this.start(watcherId, watchPath, options, toDisposeWatcher, undefined, basePath);
} else {
// 向上查找不到对应文件时,使用定时逻辑定时检索文件,当检测到文件时,启用监听逻辑
const toClearTimer = new DisposableCollection();
const timer = setInterval(async () => {
if (await fs.pathExists(basePath)) {
toClearTimer.dispose();
this.pushAdded(watcherId, basePath);
await this.start(watcherId, basePath, options, toDisposeWatcher);
}
}, NsfwFileSystemWatcherServer.WATCHER_FILE_DETECTED_TIME);
toClearTimer.push(Disposable.create(() => clearInterval(timer)));
toDisposeWatcher.push(toClearTimer);
}
}
this.toDispose.push(toDisposeWatcher);
return watcherId;
}
protected async start(
watcherId: number,
basePath: string,
rawOptions: WatchOptions | undefined,
toDisposeWatcher: DisposableCollection,
rawFile?: string,
_rawDirectory?: string,
): Promise<void> {
const options: WatchOptions = {
excludes: [],
...rawOptions,
};
let watcher: INsfw.NSFW | undefined;
if (!(await fs.pathExists(basePath))) {
return;
}
watcher = await nsfw(
await fs.realpath(basePath),
(events: INsfw.ChangeEvent[]) => {
events = this.trimChangeEvent(events);
for (const event of events) {
if (rawFile && event.file !== rawFile) {
return;
}
if (event.action === INsfw.actions.CREATED) {
this.pushAdded(watcherId, this.resolvePath(event.directory, event.file!));
}
if (event.action === INsfw.actions.DELETED) {
this.pushDeleted(watcherId, this.resolvePath(event.directory, event.file!));
}
if (event.action === INsfw.actions.MODIFIED) {
this.pushUpdated(watcherId, this.resolvePath(event.directory, event.file!));
}
if (event.action === INsfw.actions.RENAMED) {
if (event.newDirectory) {
this.pushDeleted(watcherId, this.resolvePath(event.directory, event.oldFile!));
this.pushAdded(watcherId, this.resolvePath(event.newDirectory, event.newFile!));
} else {
this.pushDeleted(watcherId, this.resolvePath(event.directory, event.oldFile!));
this.pushAdded(watcherId, this.resolvePath(event.directory, event.newFile!));
}
}
}
},
{
errorCallback: (error: any) => {
// see https://github.com/atom/github/issues/342
// eslint-disable-next-line no-console
console.warn(`Failed to watch "${basePath}":`, error);
this.unwatchFileChanges(watcherId);
},
},
);
await watcher!.start();
// this.options.info('Started watching:', basePath);
if (toDisposeWatcher.disposed) {
this.debug('Stopping watching:', basePath);
await watcher!.stop();
// remove a reference to nsfw otherwise GC cannot collect it
watcher = undefined;
this.options.info('Stopped watching:', basePath);
return;
}
toDisposeWatcher.push(
Disposable.create(async () => {
this.watcherOptions.delete(watcherId);
if (watcher) {
this.debug('Stopping watching:', basePath);
await watcher.stop();
// remove a reference to nsfw otherwise GC cannot collect it
watcher = undefined;
this.options.info('Stopped watching:', basePath);
}
}),
);
this.watcherOptions.set(watcherId, {
excludesPattern: options.excludes.map((pattern) => parseGlob(pattern)),
excludes: options.excludes,
});
}
unwatchFileChanges(watcherId: number): Promise<void> {
const watcher = this.watchers.get(watcherId);
if (watcher) {
this.watchers.delete(watcherId);
watcher.disposable.dispose();
}
return Promise.resolve();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# 主动刷新功能
core/packages/file-tree-next/src/browser/file-tree-contribution.ts
commands.registerCommand(FILE_COMMANDS.REFRESH_ALL, {
execute: async () => {
const handler = this.mainLayoutService.getTabbarHandler(EXPLORER_CONTAINER_ID);
if (!handler || !handler.isVisible) {
return;
}
await this.fileTreeService.refresh();
},
});
2
3
4
5
6
7
8
9
core/packages/file-tree-next/src/browser/file-tree.service.ts
/**
* 刷新指定下的所有子节点; 默认直接刷新根节点
*/
async refresh(node: Directory = this.root as Directory) {
// 如果正在刷新,就不要创建新的 Defer
// 否则会导致下面的 callback 闭包 resolve 的仍然是之前捕获的旧 defer
if (!this.willRefreshDeferred) {
this.willRefreshDeferred = new Deferred();
}
if (!node) {
return;
}
if (!Directory.is(node) && node.parent) {
node = node.parent as Directory;
}
// 队列化刷新动作减少更新成本
this._changeEventDispatchQueue.add(node.path);
return this.doHandleQueueChange();
}
private doHandleQueueChange = throttle(
async () => {
try {
// 询问是否此时可进行刷新事件
await this.requestFlushEventSignalEmitter.fireAndAwait();
await this.flushEventQueue();
} catch (error) {
this.logger.error('flush file change event queue error:', error);
} finally {
this.onNodeRefreshedEmitter.fire();
this.willRefreshDeferred?.resolve();
this.willRefreshDeferred = null;
}
},
FileTreeService.DEFAULT_REFRESH_DELAY,
{
leading: true,
trailing: true,
},
);
public flushEventQueue = async () => {
if (!this._changeEventDispatchQueue || this._changeEventDispatchQueue.size === 0) {
return;
}
const queue = Array.from(this._changeEventDispatchQueue);
const effectedRoots = this.sortPaths(queue);
const promise = pSeries(
effectedRoots.map((root) => async () => {
if (Directory.is(root.node)) {
await (root.node as Directory).refresh();
}
}),
);
// 重置更新队列
this._changeEventDispatchQueue.clear();
return await promise;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
core/packages/components/src/recycle-tree/tree/TreeNode.ts
// 当没有传入具体路径时,使用当前展开目录作为刷新路径
public async refresh() {
const state = TreeNode.getGlobalTreeState(this.path);
if (state.isLoadingPath || state.isExpanding) {
return;
}
let token;
if (state.refreshCancelToken.token.isCancellationRequested) {
const refreshCancelToken = new CancellationTokenSource();
TreeNode.setGlobalTreeState(this.path, {
isRefreshing: true,
refreshCancelToken,
});
token = refreshCancelToken.token;
} else {
token = state.refreshCancelToken.token;
}
await this.refreshThrottler.queue(async () => this.doRefresh(token));
TreeNode.setGlobalTreeState(this.path, {
isRefreshing: false,
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22