El espacio de trabajo es una parte importante del editor, lleva la conexión entre el editor y los archivos locales, y agrega, elimina, modifica y verifica archivos. A continuación, presentaré la creación del espacio de trabajo de vscode. Del mismo modo, sabemos que cuando se abre el software vscode, no hay un espacio de trabajo predeterminado. Aquí lo he modificado para que apunte al espacio de trabajo predeterminado cuando se inicia el software.
Creación del directorio del espacio de trabajo
Primero veamos el directorio de almacenamiento de datos de vscode en la computadora local.
Todos los datos almacenados en caché están aquí. También podemos crear los datos que deben almacenarse en caché en el proyecto.
Entonces, ¿cómo obtener y configurar el directorio de almacenamiento de datos del software? Electron proporciona api oficial
app.getPath()
app.setPath()
Dirección del documento oficial: https://www.electronjs.org/zh/docs/latest/api/app#appgetpathname
En correspondencia con el método setPath,
veamos cómo vscode crea
el src/main.js del directorio de almacenamiento de datos del software
const userDataPath = getUserDataPath(args);
app.setPath('userData', userDataPath);
Saltar al método 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
};
}
El método es la interfaz de programación de aplicaciones integrada del módulo os.homedir()
nodejs , que se utiliza para obtener la ruta del directorio de inicio del usuario actual .os
En este punto, se ha creado el directorio general de datos de caché de software.
A continuación creamos un directorio bajo el directorio general workspace-content
como directorio del espacio de trabajo para almacenar archivos.
src/vs/platform/environment/common/environment.ts
agrega el nombre del espacio de trabajo en el servicio de entorno vscode
export interface IEnvironmentService {
...
workspaceContent: URI;
...
}
Implemente la interfaz
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')); }
...
}
El directorio del espacio de trabajo predeterminado crea
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
Cada directorio de almacenamiento de datos se crea llamando al método aquí .
Sabemos que no hay un espacio de trabajo predeterminado cuando se abre el software vscode, aquí lo he modificado para que apunte al espacio de trabajo predeterminado cuando se inicia el software.
src/vs/plataforma/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;
...
Aquí la entrada del espacio de trabajo de carga está escrita hasta la saciedad.
creación de información de archivos
¿Cómo lee vscode el uri del espacio de trabajo del archivo para crear una lista de espacios de trabajo? Veamos
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
La información del archivo en el espacio de trabajo se lee aquí, y la información del archivo obtenida por el módulo node fs a través de stat se vuelve a encapsular aquí , y podemos modificar los datos en él según los requisitos. Veamos el lugar donde se definen los datos del archivo.
src/vs/plataforma/archivos/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 */);
}
siguiendo en
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;
}
Se puede ver que la información del archivo generada aquí
src/vs/workbench/contrib/files/common/explorerModel.ts
es el modelo de datos del archivo generado por la capa Modelo. más tarde se utiliza para representar la página.
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;
}
dibujar lista de archivos
src/vs/workbench/contrib/files/browser/views/explorerView.ts
const promise = this.tree.setInput(input, viewState).then(async () => {
...
});
la entrada es la información del archivo del espacio de trabajo total
Función de clasificación de archivos
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',
}
La configuración en el archivo de configuración
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.")
},
lograr
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);
}
}
}
Registrar llamada de menú
// 按创建日期排序
// 一级菜单
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();
}
});
Mecanismo de creación del espacio de trabajo de la versión web de Vscode
https://insiders.vscode.dev/La
página web utiliza window.showOpenFilePicker
la API del navegador
Dirección del documento oficial: 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
}
...
El resto del proceso es el mismo.