vscode的代码工作区实现机制

工作区是编辑器的重要部分,它承载着编辑器和本地文件的连接,对文件增、删、改、查。下面我会介绍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
}
...

其余流程一样

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值