工作区是编辑器的重要部分,它承载着编辑器和本地文件的连接,对文件增、删、改、查。下面我会介绍vscode工作区的创建。同样我们知道vscode软件打开的时候没有默认工作区,这里我对它进行了改造,软件启动时指向默认工作区。
工作区目录创建
我们先看下vscode在本地电脑的数据存储目录。
所有的缓存数据都在这里。我们也可以把项目中需要缓存的数据创建到这里面。
那么如何获取和设置软件数据存储目录呢?electron提供了官方api
app.getPath()
app.setPath()
官方文档地址: https://www.electronjs.org/zh/docs/latest/api/app#appgetpathname
对应setPath方法
我们看看vscode是如何创建软件数据存储目录的
src/main.js
const userDataPath = getUserDataPath(args);
app.setPath('userData', userDataPath);
跳进getUserDataPath方法
function factory(path, os, productName, cwd) {
/**
* @param {NativeParsedArgs} cliArgs
*
* @returns {string}
*/
function getUserDataPath(cliArgs) {
const userDataPath = doGetUserDataPath(cliArgs);
const pathsToResolve = [userDataPath];
if (!path.isAbsolute(userDataPath)) {
pathsToResolve.unshift(cwd);
}
return path.resolve(...pathsToResolve);
}
/**
* @param {NativeParsedArgs} cliArgs
*
* @returns {string}
*/
function doGetUserDataPath(cliArgs) {
// 1. Support portable mode
const portablePath = process.env['VSCODE_PORTABLE'];
if (portablePath) {
return path.join(portablePath, 'user-data');
}
// 2. Support global VSCODE_APPDATA environment variable
let appDataPath = process.env['VSCODE_APPDATA'];
if (appDataPath) {
return path.join(appDataPath, productName);
}
// 3. Support explicit --user-data-dir
const cliPath = cliArgs['user-data-dir'];
if (cliPath) {
return cliPath;
}
// 4. Otherwise check per platform
switch (process.platform) {
case 'win32':
appDataPath = process.env['APPDATA'];
if (!appDataPath) {
const userProfile = process.env['USERPROFILE'];
if (typeof userProfile !== 'string') {
throw new Error('Windows: Unexpected undefined %USERPROFILE% environment variable');
}
appDataPath = path.join(userProfile, 'AppData', 'Roaming');
}
break;
case 'darwin':
appDataPath = path.join(os.homedir(), 'Library', 'Application Support');
break;
case 'linux':
appDataPath = process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
break;
default:
throw new Error('Platform not supported');
}
return path.join(appDataPath, productName);
}
return {
getUserDataPath
};
}
其中os.homedir()
方法是nodejs的os
模块的内置应用程序编程接口,用于获取当前用户的主目录路径。
到此软件缓存数据总目录创建完成。
下面我们在总目录下面创建一个workspace-content
目录作为工作区目录来存储文件。
src/vs/platform/environment/common/environment.ts
在vscode环境service里面添加工作区名称
export interface IEnvironmentService {
...
workspaceContent: URI;
...
}
实现接口
src/vs/platform/environment/common/environmentService.ts
export abstract class AbstractNativeEnvironmentService implements INativeEnvironmentService {
...
@memoize
get workspaceContent(): URI { return URI.file(join(this.userDataPath, 'workspace-content')); }
...
}
默认工作区目录创建
src/vs/code/electron-main/main.ts
Promise.all<string | undefined>([
environmentMainService.extensionsPath,
environmentMainService.codeCachePath,
environmentMainService.logsPath,
environmentMainService.globalStorageHome.fsPath,
environmentMainService.workspaceStorageHome.fsPath,
environmentMainService.localHistoryHome.fsPath,
environmentMainService.backupHome,
environmentMainService.workspaceTemplate.fsPath,
].map(path => path ? FSPromises.mkdir(path, { recursive: true }) : undefined)),
在这里通过调用mkdir
方法创建了各个数据存储目录。
我们知道vscode软件打开的时候没有默认工作区,这里我对它进行了改造,软件启动时指向默认工作区。
src/vs/platform/windows/electron-main/windowsMainService.ts
const workspaceContentUri = this.environmentService.workspaceContent;
const workspacePath: IPathToOpen[] = [
{
workspace: {
id: '1',
uri: workspaceContentUri
},
type: 2, // 2 File is a directory.
exists: true
}
];
// Identify things to open from open config
// const pathsToOpen = this.getPathsToOpen(openConfig); //愿逻辑
const pathsToOpen = workspacePath;
...
这里把加载工作区入口写死了
文件信息创建
vscode是如何读取文件工作区的uri创建工作区列表的呢?我们接着看
src/vs/workbench/services/configuration/browser/configurationService.ts
private async toValidWorkspaceFolders(workspaceFolders: WorkspaceFolder[]): Promise<WorkspaceFolder[]> {
const validWorkspaceFolders: WorkspaceFolder[] = [];
for (const workspaceFolder of workspaceFolders) {
try {
const result = await this.fileService.stat(workspaceFolder.uri);
if (!result.isDirectory) {
continue;
}
} catch (e) {
this.logService.warn(`Ignoring the error while validating workspace folder ${workspaceFolder.uri.toString()} - ${toErrorMessage(e)}`);
}
validWorkspaceFolders.push(workspaceFolder);
}
return validWorkspaceFolders;
}
这里读取了工作区的文件信息,这里的stat
是对node fs模块通过stat获取文件信息进行了二次封装的,我们可以根据需求修改里面的数据。我们看下定义文件数据的地方
src/vs/platform/files/common/fileService.ts
async stat(resource: URI): Promise<IFileStatWithPartialMetadata> {
const provider = await this.withProvider(resource);
const stat = await provider.stat(resource);
return this.toFileStat(provider, resource, stat, undefined, true, () => false /* Do not resolve any children */);
}
在往里跟
private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial<IStat>, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStat> {
const { providerExtUri } = this.getExtUri(provider);
let isAudioFolderFlag = await this.exists(providerExtUri.joinPath(resource, 'main.audio'));
// convert to file stat
const fileStat: IFileStat = {
resource,
name: providerExtUri.basename(resource),
isFile: (stat.type & FileType.File) !== 0,
isDirectory: (stat.type & FileType.Directory) !== 0,
isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0,
mtime: stat.mtime,
ctime: stat.ctime,
size: stat.size,
readonly: Boolean((stat.permissions ?? 0) & FilePermission.Readonly) || Boolean(provider.capabilities & FileSystemProviderCapabilities.Readonly),
etag: etag({ mtime: stat.mtime, size: stat.size }),
children: undefined,
isAudioFolder: isAudioFolderFlag
};
// check to recurse for directories
if (fileStat.isDirectory && recurse(fileStat, siblings)) {
try {
const entries = await provider.readdir(resource);
// console.log('entries---', entries);
const resolvedEntries = await Promises.settled(entries.map(async ([name, type]) => {
try {
const childResource = providerExtUri.joinPath(resource, name);
// const childStat = resolveMetadata ? await provider.stat(childResource) : { type };
/**
* @todo 获取子文件数据,愿逻辑由resolveMetadata开关控制
*/
const childStat = await provider.stat(childResource)
return await this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse);
} catch (error) {
this.logService.trace(error);
return null; // can happen e.g. due to permission errors
}
}));
// make sure to get rid of null values that signal a failure to resolve a particular entry
fileStat.children = coalesce(resolvedEntries);
} catch (error) {
this.logService.trace(error);
fileStat.children = []; // gracefully handle errors, we may not have permissions to read
}
return fileStat;
}
return fileStat;
}
可以看出这里生成文件信息
src/vs/workbench/contrib/files/common/explorerModel.ts
这个文件是Model层生成文件数据模型。后面用于渲染页面的。
static create(fileService: IFileService, configService: IConfigurationService, raw: IFileStat, parent: ExplorerItem | undefined, resolveTo?: readonly URI[]): ExplorerItem {
let stat: ExplorerItem;
if (raw.isAudioFolder) {
stat = new ExplorerItem(raw.resource, fileService, configService, parent, false, raw.isSymbolicLink, raw.readonly, raw.name, raw.mtime, false, raw.ctime);
} else {
stat = new ExplorerItem(raw.resource, fileService, configService, parent, raw.isDirectory, raw.isSymbolicLink, raw.readonly, raw.name, raw.mtime, !raw.isFile && !raw.isDirectory, raw.ctime);
}
stat.isAudioFolder = (raw.isAudioFolder as boolean);
if (stat.isDirectory &&!stat.isAudioFolder) {
stat._isDirectoryResolved = !!raw.children || (!!resolveTo && resolveTo.some((r) => {
return isEqualOrParent(r, stat.resource);
}));
if (raw.children) {
for (let i = 0, len = raw.children.length; i < len; i++) {
const child = ExplorerItem.create(fileService, configService, raw.children[i], stat, resolveTo);
stat.addChild(child);
}
}
}
// 总 stat
return stat;
}
绘制文件列表
src/vs/workbench/contrib/files/browser/views/explorerView.ts
const promise = this.tree.setInput(input, viewState).then(async () => {
...
});
input是总工作区的文件信息
对文件排序功能
src/vs/workbench/contrib/files/common/files.ts
接口
export const enum SortOrder {
Default = 'default',
Mixed = 'mixed',
FilesFirst = 'filesFirst',
Type = 'type',
ModifiedLower = 'modifiedLower',
ModifiedUp = 'modifiedUp',
NameLower = 'nameLower',
NameUp = 'nameUp',
createTimeLower = 'createTimeLower',
createTimeUp = 'createTimeUp',
FoldersNestsFiles = 'foldersNestsFiles',
}
settings文件里面的配置
src/vs/workbench/contrib/files/browser/files.contribution.ts
'explorer.sortOrder': {
'type': 'string',
'enum': [SortOrder.Default, SortOrder.Mixed, SortOrder.FilesFirst, SortOrder.Type, SortOrder.ModifiedLower, SortOrder.ModifiedUp, SortOrder.NameLower, SortOrder.NameUp, SortOrder.createTimeLower, SortOrder.createTimeUp, SortOrder.FoldersNestsFiles],
'default': SortOrder.Default,
'enumDescriptions': [
nls.localize('sortOrder.default', 'Files and folders are sorted by their names. Folders are displayed before files.'),
nls.localize('sortOrder.mixed', 'Files and folders are sorted by their names. Files are interwoven with folders.'),
nls.localize('sortOrder.filesFirst', 'Files and folders are sorted by their names. Files are displayed before folders.'),
nls.localize('sortOrder.type', 'Files and folders are grouped by extension type then sorted by their names. Folders are displayed before files.'),
nls.localize('sortOrder.ModifiedLower', 'Files and folders are sorted by last modified date in descending order. Folders are displayed before files.'),
nls.localize('sortOrder.ModifiedUp', 'Files and folders are sorted by latest modified date in descending order. Folders are displayed before files.'),
nls.localize('sortOrder.nameLower', 'Files and folders are sorted by name'),
nls.localize('sortOrder.nameUp', 'Files and folders are sorted by name'),
nls.localize('sortOrder.createTimeLower', 'Files and folders are sorted by create time'),
nls.localize('sortOrder.createTimeUp', 'Files and folders are sorted by create time'),
nls.localize('sortOrder.foldersNestsFiles', 'Files and folders are sorted by their names. Folders are displayed before files. Files with nested children are displayed before other files.')
],
'markdownDescription': nls.localize('sortOrder', "Controls the property-based sorting of files and folders in the explorer. When `#explorer.experimental.fileNesting.enabled#` is enabled, also controls sorting of nested files.")
},
实现
export class FileSorter implements ITreeSorter<ExplorerItem> {
constructor(
@IExplorerService private readonly explorerService: IExplorerService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
) { }
compare(statA: ExplorerItem, statB: ExplorerItem): number {
// Do not sort roots
if (statA.isRoot) {
if (statB.isRoot) {
const workspaceA = this.contextService.getWorkspaceFolder(statA.resource);
const workspaceB = this.contextService.getWorkspaceFolder(statB.resource);
return workspaceA && workspaceB ? (workspaceA.index - workspaceB.index) : -1;
}
return -1;
}
if (statB.isRoot) {
return 1;
}
const sortOrder = this.explorerService.sortOrderConfiguration.sortOrder;
const lexicographicOptions = this.explorerService.sortOrderConfiguration.lexicographicOptions;
let compareFileNames;
let compareFileExtensions;
switch (lexicographicOptions) {
case 'upper':
compareFileNames = compareFileNamesUpper;
compareFileExtensions = compareFileExtensionsUpper;
break;
case 'lower':
compareFileNames = compareFileNamesLower;
compareFileExtensions = compareFileExtensionsLower;
break;
case 'unicode':
compareFileNames = compareFileNamesUnicode;
compareFileExtensions = compareFileExtensionsUnicode;
break;
default:
// 'default'
compareFileNames = compareFileNamesDefault;
compareFileExtensions = compareFileExtensionsDefault;
}
// Sort Directories
switch (sortOrder) {
case 'type':
if (statA.isDirectory && !statB.isDirectory) {
return -1;
}
if (statB.isDirectory && !statA.isDirectory) {
return 1;
}
if (statA.isDirectory && statB.isDirectory) {
return compareFileNames(statA.name, statB.name);
}
break;
case 'filesFirst':
if (statA.isDirectory && !statB.isDirectory) {
return 1;
}
if (statB.isDirectory && !statA.isDirectory) {
return -1;
}
break;
case 'foldersNestsFiles':
if (statA.isDirectory && !statB.isDirectory) {
return -1;
}
if (statB.isDirectory && !statA.isDirectory) {
return 1;
}
if (statA.hasNests && !statB.hasNests) {
return -1;
}
if (statB.hasNests && !statA.hasNests) {
return 1;
}
break;
case 'mixed':
break; // not sorting when "mixed" is on
default: /* 'default', 'modified' */
if (statA.isDirectory && !statB.isDirectory) {
return -1;
}
if (statB.isDirectory && !statA.isDirectory) {
return 1;
}
break;
}
// Sort Files
switch (sortOrder) {
case 'createTimeLower':
return (statA.mcime && statB.mcime && statA.mcime < statB.mcime) ? -1 : 1;
case 'createTimeUp':
return (statA.mcime && statB.mcime && statA.mcime < statB.mcime) ? 1 : -1;
case 'nameUp':
return statB.name.localeCompare(statA.name);
case 'nameLower':
return statA.name.localeCompare(statB.name);
case 'type':
return compareFileExtensions(statA.name, statB.name);
case 'modifiedLower':
if (statA.mtime !== statB.mtime) {
return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1;
}
return compareFileNames(statA.name, statB.name);
case 'modifiedUp':
if (statA.mtime !== statB.mtime) {
return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? -1 : 1;
}
return compareFileNames(statA.name, statB.name);
default: /* 'default', 'mixed', 'filesFirst' */
return compareFileNames(statA.name, statB.name);
}
}
}
注册菜单调用
// 按创建日期排序
// 一级菜单
const explorerSortByCreateTimeSubMenu = new MenuId('sortOrderByCreateTime');
MenuRegistry.appendMenuItem(MenuId.ViewTitle, <ISubmenuItem>{
submenu: explorerSortByCreateTimeSubMenu,
title: nls.localize('sortOrderByCreateTime', "按创建日期排序"),
when: ContextKeyExpr.equals('view', VIEW_ID),
group: 'group',
order: 3,
});
// 二级菜单
registerAction2(class extends Action2 {
constructor() {
super({
id: 'sortOrder.create.time.lower',
title: nls.localize('sortOrder.create.time.lower', "最新至最旧"),
menu: {
id: explorerSortByCreateTimeSubMenu,
order: 1,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const config = accessor.get(IConfigurationService);
const paneCompositeService = accessor.get(IPaneCompositePartService);
const explorerService = accessor.get(IExplorerService);
config.updateValue('explorer.sortOrder', SortOrder.createTimeLower);
await paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar);
await explorerService.refresh();
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'sortOrder.create.time.up',
title: nls.localize('sortOrder.create.time.up', "最旧至最新"),
menu: {
id: explorerSortByCreateTimeSubMenu,
order: 2,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const config = accessor.get(IConfigurationService);
const paneCompositeService = accessor.get(IPaneCompositePartService);
const explorerService = accessor.get(IExplorerService);
config.updateValue('explorer.sortOrder', SortOrder.createTimeUp);
await paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar);
await explorerService.refresh();
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'sortOrder.create.time.default',
title: nls.localize('sortOrder.create.time.default', "默认排序"),
menu: {
id: explorerSortByCreateTimeSubMenu,
order: 3,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const config = accessor.get(IConfigurationService);
const paneCompositeService = accessor.get(IPaneCompositePartService);
const explorerService = accessor.get(IExplorerService);
config.updateValue('explorer.sortOrder', SortOrder.Default);
await paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar);
await explorerService.refresh();
}
});
vscode网页版工作区创建机制
https://insiders.vscode.dev/
网页端使用的是window.showOpenFilePicker
浏览器API.
官方文档地址:https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API
src/vs/workbench/services/dialogs/browser/fileDialogService.ts
...
try {
([fileHandle] = await window.showOpenFilePicker({ multiple: false }));
} catch (error) {
return; // `showOpenFilePicker` will throw an error when the user cancels
}
...
其余流程一样