UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

518 lines (517 loc) 19.1 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); }; }; import { hash } from '@sussudio/base/common/hash.mjs'; import { Emitter } from '@sussudio/base/common/event.mjs'; import { Disposable } from '@sussudio/base/common/lifecycle.mjs'; import { basename, joinPath } from '@sussudio/base/common/resources.mjs'; import { URI } from '@sussudio/base/common/uri.mjs'; import { localize } from 'vscode-nls.mjs'; import { IEnvironmentService } from '../../environment/common/environment.mjs'; import { IFileService } from '../../files/common/files.mjs'; import { createDecorator } from '../../instantiation/common/instantiation.mjs'; import { ILogService } from '../../log/common/log.mjs'; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from '../../workspace/common/workspace.mjs'; import { ResourceMap } from '@sussudio/base/common/map.mjs'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.mjs'; import { Promises } from '@sussudio/base/common/async.mjs'; import { generateUuid } from '@sussudio/base/common/uuid.mjs'; import { escapeRegExpCharacters } from '@sussudio/base/common/strings.mjs'; import { isString } from '@sussudio/base/common/types.mjs'; export function isUserDataProfile(thing) { const candidate = thing; return !!( candidate && typeof candidate === 'object' && typeof candidate.id === 'string' && typeof candidate.isDefault === 'boolean' && typeof candidate.name === 'string' && URI.isUri(candidate.location) && URI.isUri(candidate.globalStorageHome) && URI.isUri(candidate.settingsResource) && URI.isUri(candidate.keybindingsResource) && URI.isUri(candidate.tasksResource) && URI.isUri(candidate.snippetsHome) && URI.isUri(candidate.extensionsResource) ); } export const PROFILES_ENABLEMENT_CONFIG = 'workbench.experimental.settingsProfiles.enabled'; export const IUserDataProfilesService = createDecorator('IUserDataProfilesService'); export function reviveProfile(profile, scheme) { return { id: profile.id, isDefault: profile.isDefault, name: profile.name, shortName: profile.shortName, location: URI.revive(profile.location).with({ scheme }), globalStorageHome: URI.revive(profile.globalStorageHome).with({ scheme }), settingsResource: URI.revive(profile.settingsResource).with({ scheme }), keybindingsResource: URI.revive(profile.keybindingsResource).with({ scheme }), tasksResource: URI.revive(profile.tasksResource).with({ scheme }), snippetsHome: URI.revive(profile.snippetsHome).with({ scheme }), extensionsResource: URI.revive(profile.extensionsResource)?.with({ scheme }), useDefaultFlags: profile.useDefaultFlags, isTransient: profile.isTransient, }; } export function toUserDataProfile(id, name, location, options) { return { id, name, location, isDefault: false, shortName: options?.shortName, globalStorageHome: joinPath(location, 'globalStorage'), settingsResource: joinPath(location, 'settings.json'), keybindingsResource: joinPath(location, 'keybindings.json'), tasksResource: joinPath(location, 'tasks.json'), snippetsHome: joinPath(location, 'snippets'), extensionsResource: joinPath(location, 'extensions.json'), useDefaultFlags: options?.useDefaultFlags, isTransient: options?.transient, }; } let UserDataProfilesService = class UserDataProfilesService extends Disposable { environmentService; fileService; uriIdentityService; logService; static PROFILES_KEY = 'userDataProfiles'; static PROFILE_ASSOCIATIONS_KEY = 'profileAssociations'; _serviceBrand; enabled = false; profilesHome; get defaultProfile() { return this.profiles[0]; } get profiles() { return [...this.profilesObject.profiles, ...this.transientProfilesObject.profiles]; } _onDidChangeProfiles = this._register(new Emitter()); onDidChangeProfiles = this._onDidChangeProfiles.event; _onWillCreateProfile = this._register(new Emitter()); onWillCreateProfile = this._onWillCreateProfile.event; _onWillRemoveProfile = this._register(new Emitter()); onWillRemoveProfile = this._onWillRemoveProfile.event; _onDidResetWorkspaces = this._register(new Emitter()); onDidResetWorkspaces = this._onDidResetWorkspaces.event; profileCreationPromises = new Map(); transientProfilesObject = { profiles: [], workspaces: new ResourceMap(), emptyWindows: new Map(), }; constructor(environmentService, fileService, uriIdentityService, logService) { super(); this.environmentService = environmentService; this.fileService = fileService; this.uriIdentityService = uriIdentityService; this.logService = logService; this.profilesHome = joinPath(this.environmentService.userRoamingDataHome, 'profiles'); } setEnablement(enabled) { if (this.enabled !== enabled) { this._profilesObject = undefined; this.enabled = enabled; } } isEnabled() { return this.enabled; } _profilesObject; get profilesObject() { if (!this._profilesObject) { const profiles = []; if (this.enabled) { for (const storedProfile of this.getStoredProfiles()) { if (!storedProfile.name || !isString(storedProfile.name) || !storedProfile.location) { this.logService.warn('Skipping the invalid stored profile', storedProfile.location || storedProfile.name); continue; } profiles.push( toUserDataProfile(basename(storedProfile.location), storedProfile.name, storedProfile.location, { shortName: storedProfile.shortName, useDefaultFlags: storedProfile.useDefaultFlags, }), ); } } const workspaces = new ResourceMap(); const emptyWindows = new Map(); const defaultProfile = toUserDataProfile( hash(this.environmentService.userRoamingDataHome.path).toString(16), localize('defaultProfile', 'Default'), this.environmentService.userRoamingDataHome, ); profiles.unshift({ ...defaultProfile, extensionsResource: this.getDefaultProfileExtensionsLocation() ?? defaultProfile.extensionsResource, isDefault: true, }); if (profiles.length) { const profileAssociaitions = this.getStoredProfileAssociations(); if (profileAssociaitions.workspaces) { for (const [workspacePath, profilePath] of Object.entries(profileAssociaitions.workspaces)) { const workspace = URI.parse(workspacePath); const profileLocation = URI.parse(profilePath); const profile = profiles.find((p) => this.uriIdentityService.extUri.isEqual(p.location, profileLocation)); if (profile) { workspaces.set(workspace, profile); } } } if (profileAssociaitions.emptyWindows) { for (const [windowId, profilePath] of Object.entries(profileAssociaitions.emptyWindows)) { const profileLocation = URI.parse(profilePath); const profile = profiles.find((p) => this.uriIdentityService.extUri.isEqual(p.location, profileLocation)); if (profile) { emptyWindows.set(windowId, profile); } } } } this._profilesObject = { profiles, workspaces, emptyWindows }; } return this._profilesObject; } async createTransientProfile(workspaceIdentifier) { const namePrefix = `Temp`; const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s(\\d+)`); let nameIndex = 0; for (const profile of this.profiles) { const matches = nameRegEx.exec(profile.name); const index = matches ? parseInt(matches[1]) : 0; nameIndex = index > nameIndex ? index : nameIndex; } const name = `${namePrefix} ${nameIndex + 1}`; return this.createProfile(hash(generateUuid()).toString(16), name, { transient: true }, workspaceIdentifier); } async createNamedProfile(name, options, workspaceIdentifier) { return this.createProfile(hash(generateUuid()).toString(16), name, options, workspaceIdentifier); } async createProfile(id, name, options, workspaceIdentifier) { if (!this.enabled) { throw new Error(`Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`); } const profile = await this.doCreateProfile(id, name, options); if (workspaceIdentifier) { await this.setProfileForWorkspace(workspaceIdentifier, profile); } return profile; } async doCreateProfile(id, name, options) { if (!isString(name) || !name) { throw new Error('Name of the profile is mandatory and must be of type `string`'); } let profileCreationPromise = this.profileCreationPromises.get(name); if (!profileCreationPromise) { profileCreationPromise = (async () => { try { const existing = this.profiles.find((p) => p.name === name || p.id === id); if (existing) { return existing; } const profile = toUserDataProfile(id, name, joinPath(this.profilesHome, id), options); await this.fileService.createFolder(profile.location); const joiners = []; this._onWillCreateProfile.fire({ profile, join(promise) { joiners.push(promise); }, }); await Promises.settled(joiners); this.updateProfiles([profile], [], []); return profile; } finally { this.profileCreationPromises.delete(name); } })(); this.profileCreationPromises.set(name, profileCreationPromise); } return profileCreationPromise; } async updateProfile(profileToUpdate, options) { if (!this.enabled) { throw new Error(`Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`); } let profile = this.profiles.find((p) => p.id === profileToUpdate.id); if (!profile) { throw new Error(`Profile '${profileToUpdate.name}' does not exist`); } profile = toUserDataProfile(profile.id, options.name ?? profile.name, profile.location, { shortName: options.shortName ?? profile.shortName, transient: options.transient ?? profile.isTransient, useDefaultFlags: options.useDefaultFlags ?? profile.useDefaultFlags, }); this.updateProfiles([], [], [profile]); return profile; } async removeProfile(profileToRemove) { if (!this.enabled) { throw new Error(`Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`); } if (profileToRemove.isDefault) { throw new Error('Cannot remove default profile'); } const profile = this.profiles.find((p) => p.id === profileToRemove.id); if (!profile) { throw new Error(`Profile '${profileToRemove.name}' does not exist`); } const joiners = []; this._onWillRemoveProfile.fire({ profile, join(promise) { joiners.push(promise); }, }); try { await Promise.allSettled(joiners); } catch (error) { this.logService.error(error); } for (const windowId of [...this.profilesObject.emptyWindows.keys()]) { if (profile.id === this.profilesObject.emptyWindows.get(windowId)?.id) { this.profilesObject.emptyWindows.delete(windowId); } } for (const workspace of [...this.profilesObject.workspaces.keys()]) { if (profile.id === this.profilesObject.workspaces.get(workspace)?.id) { this.profilesObject.workspaces.delete(workspace); } } this.updateStoredProfileAssociations(); this.updateProfiles([], [profile], []); try { if (this.profiles.length === 1) { await this.fileService.del(this.profilesHome, { recursive: true }); } else { await this.fileService.del(profile.location, { recursive: true }); } } catch (error) { this.logService.error(error); } } async setProfileForWorkspace(workspaceIdentifier, profileToSet) { if (!this.enabled) { throw new Error(`Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`); } const profile = this.profiles.find((p) => p.id === profileToSet.id); if (!profile) { throw new Error(`Profile '${profileToSet.name}' does not exist`); } this.updateWorkspaceAssociation(workspaceIdentifier, profile); } unsetWorkspace(workspaceIdentifier, transient) { if (!this.enabled) { throw new Error(`Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`); } this.updateWorkspaceAssociation(workspaceIdentifier, undefined, transient); } async resetWorkspaces() { this.transientProfilesObject.workspaces.clear(); this.transientProfilesObject.emptyWindows.clear(); this.profilesObject.workspaces.clear(); this.profilesObject.emptyWindows.clear(); this.updateStoredProfileAssociations(); this._onDidResetWorkspaces.fire(); } async cleanUp() { if (!this.enabled) { return; } if (await this.fileService.exists(this.profilesHome)) { const stat = await this.fileService.resolve(this.profilesHome); await Promise.all( (stat.children || []) .filter( (child) => child.isDirectory && this.profiles.every((p) => !this.uriIdentityService.extUri.isEqual(p.location, child.resource)), ) .map((child) => this.fileService.del(child.resource, { recursive: true })), ); } } async cleanUpTransientProfiles() { if (!this.enabled) { return; } const unAssociatedTransientProfiles = this.transientProfilesObject.profiles.filter( (p) => !this.isProfileAssociatedToWorkspace(p), ); await Promise.allSettled(unAssociatedTransientProfiles.map((p) => this.removeProfile(p))); } getProfileForWorkspace(workspaceIdentifier) { const workspace = this.getWorkspace(workspaceIdentifier); return URI.isUri(workspace) ? this.transientProfilesObject.workspaces.get(workspace) ?? this.profilesObject.workspaces.get(workspace) : this.transientProfilesObject.emptyWindows.get(workspace) ?? this.profilesObject.emptyWindows.get(workspace); } getWorkspace(workspaceIdentifier) { if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { return workspaceIdentifier.uri; } if (isWorkspaceIdentifier(workspaceIdentifier)) { return workspaceIdentifier.configPath; } return workspaceIdentifier.id; } isProfileAssociatedToWorkspace(profile) { if ( [...this.transientProfilesObject.emptyWindows.values()].some((windowProfile) => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location), ) ) { return true; } if ( [...this.transientProfilesObject.workspaces.values()].some((workspaceProfile) => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location), ) ) { return true; } if ( [...this.profilesObject.emptyWindows.values()].some((windowProfile) => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location), ) ) { return true; } if ( [...this.profilesObject.workspaces.values()].some((workspaceProfile) => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location), ) ) { return true; } return false; } updateProfiles(added, removed, updated) { const allProfiles = [...this.profiles, ...added]; const storedProfiles = []; this.transientProfilesObject.profiles = []; for (let profile of allProfiles) { if (profile.isDefault) { continue; } if (removed.some((p) => profile.id === p.id)) { continue; } profile = updated.find((p) => profile.id === p.id) ?? profile; if (profile.isTransient) { this.transientProfilesObject.profiles.push(profile); } else { storedProfiles.push({ location: profile.location, name: profile.name, shortName: profile.shortName, useDefaultFlags: profile.useDefaultFlags, }); } } this.saveStoredProfiles(storedProfiles); this._profilesObject = undefined; this.triggerProfilesChanges(added, removed, updated); } triggerProfilesChanges(added, removed, updated) { this._onDidChangeProfiles.fire({ added, removed, updated, all: this.profiles }); } updateWorkspaceAssociation(workspaceIdentifier, newProfile, transient) { // Force transient if the new profile to associate is transient transient = newProfile?.isTransient ? true : transient; if (!transient) { // Unset the transiet workspace association if any this.updateWorkspaceAssociation(workspaceIdentifier, undefined, true); } const workspace = this.getWorkspace(workspaceIdentifier); const profilesObject = transient ? this.transientProfilesObject : this.profilesObject; // Folder or Multiroot workspace if (URI.isUri(workspace)) { profilesObject.workspaces.delete(workspace); if (newProfile) { profilesObject.workspaces.set(workspace, newProfile); } } // Empty Window else { profilesObject.emptyWindows.delete(workspace); if (newProfile) { profilesObject.emptyWindows.set(workspace, newProfile); } } if (!transient) { this.updateStoredProfileAssociations(); } } updateStoredProfileAssociations() { const workspaces = {}; for (const [workspace, profile] of this.profilesObject.workspaces.entries()) { workspaces[workspace.toString()] = profile.location.toString(); } const emptyWindows = {}; for (const [windowId, profile] of this.profilesObject.emptyWindows.entries()) { emptyWindows[windowId.toString()] = profile.location.toString(); } this.saveStoredProfileAssociations({ workspaces, emptyWindows }); this._profilesObject = undefined; } getStoredProfiles() { return []; } saveStoredProfiles(storedProfiles) { throw new Error('not implemented'); } getStoredProfileAssociations() { return {}; } saveStoredProfileAssociations(storedProfileAssociations) { throw new Error('not implemented'); } getDefaultProfileExtensionsLocation() { return undefined; } }; UserDataProfilesService = __decorate( [__param(0, IEnvironmentService), __param(1, IFileService), __param(2, IUriIdentityService), __param(3, ILogService)], UserDataProfilesService, ); export { UserDataProfilesService }; export class InMemoryUserDataProfilesService extends UserDataProfilesService { storedProfiles = []; getStoredProfiles() { return this.storedProfiles; } saveStoredProfiles(storedProfiles) { this.storedProfiles = storedProfiles; } storedProfileAssociations = {}; getStoredProfileAssociations() { return this.storedProfileAssociations; } saveStoredProfileAssociations(storedProfileAssociations) { this.storedProfileAssociations = storedProfileAssociations; } }