sussudio
Version:
An unofficial VS Code Internal API
441 lines (440 loc) • 21.6 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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 "../../../base/common/hash.mjs";
import { Emitter } from "../../../base/common/event.mjs";
import { Disposable } from "../../../base/common/lifecycle.mjs";
import { basename, joinPath } from "../../../base/common/resources.mjs";
import { URI } from "../../../base/common/uri.mjs";
import { localize } from "../../../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 "../../../base/common/map.mjs";
import { IUriIdentityService } from "../../uriIdentity/common/uriIdentity.mjs";
import { Promises } from "../../../base/common/async.mjs";
import { generateUuid } from "../../../base/common/uuid.mjs";
import { escapeRegExpCharacters } from "../../../base/common/strings.mjs";
import { isString } from "../../../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; }
}