diff --git a/.test-profiles/simple-profile/data/User/settings.json b/.test-profiles/simple-profile/data/User/settings.json index f1766e0..693f064 100755 --- a/.test-profiles/simple-profile/data/User/settings.json +++ b/.test-profiles/simple-profile/data/User/settings.json @@ -1,3 +1,123 @@ { - "editor.fontSize": 24 + "editor.fontSize": 24, + "profileSwitcher.profiles": ["Default"], + "profileSwitcher.storage": { + "Default": { + "sync.gist": "40c333588d64df34c6f36f196010e5ee", + "sync.lastUpload": "", + "sync.autoDownload": false, + "sync.autoUpload": false, + "sync.lastDownload": "2017-11-03T04:41:33.893Z", + "sync.forceDownload": false, + "sync.anonymousGist": false, + "sync.host": "", + "sync.pathPrefix": "", + "sync.quietSync": false, + "sync.askGistName": false, + "workbench.colorTheme": "1984 - Light", + "files.autoSave": "afterDelay", + "editor.renderWhitespace": "all", + "editor.fontFamily": "Fira Code", + "editor.fontLigatures": true, + "workbench.startupEditor": "newUntitledFile", + "files.autoSaveDelay": 5000, + "sync.removeExtensions": true, + "sync.syncExtensions": true, + "team.showWelcomeMessage": false, + "terminal.integrated.rendererType": "auto", + "explorer.confirmDelete": false, + "window.zoomLevel": 0, + "azureStorage.preview.staticWebsites": true, + "breadcrumbs.enabled": true, + "powershell.powerShellExePath": "C:\\WINDOWS\\SysWow64\\WindowsPowerShell\\v1.0\\powershell.exe", + "FSharp.enableReferenceCodeLens": true, + "FSharp.fsacRuntime": "netcore", + "workbench.iconTheme": "vscode-icons", + "peacock.favoriteColors": [ + { + "name": "Angular Red", + "value": "#b52e31" + }, + { + "name": "Auth0 Orange", + "value": "#eb5424" + }, + { + "name": "Azure Blue", + "value": "#007fff" + }, + { + "name": "C# Purple", + "value": "#68217A" + }, + { + "name": "Gatsby Purple", + "value": "#639" + }, + { + "name": "Go Cyan", + "value": "#5dc9e2" + }, + { + "name": "Java Blue-Gray", + "value": "#557c9b" + }, + { + "name": "JavaScript Yellow", + "value": "#f9e64f" + }, + { + "name": "Mandalorian Blue", + "value": "#1857a4" + }, + { + "name": "Node Green", + "value": "#215732" + }, + { + "name": "React Blue", + "value": "#00b3e6" + }, + { + "name": "Something Different", + "value": "#832561" + }, + { + "name": "Vue Green", + "value": "#42b883" + } + ], + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.fontSize": 14, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.formatOnSave": true, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "profileSwitcher.profiles": ["test1"], + "profileSwitcher.storage": { + "test1": { + "foo": "bar" + } + } + } + }, + "profileSwitcher.extensions": { + "Default": [ + { + "id": "96fa4707-6983-4489-b7c5-d5ffdfdcce90", + "publisherId": "esbenp.prettier-vscode", + "publisherName": "esbenp", + "version": "1.9.0", + "name": "prettier-vscode" + } + ] + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index c9e49c4..77232c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to the "vscode-profile-switcher" extension will be documente Check [Keep a Changelog](http://keepachan2gelog.com/) for recommendations on how to structure this file. +## 0.3.0 - Unreleased + +- Working on support for enable/disable extensions as you change profiles (Issue [#2](https://github.com/aaronpowell/vscode-profile-switcher/issues/2)) + ## 0.2.0 - 2019-06-28 - Rewriting internals diff --git a/package.json b/package.json index 067a590..5aff379 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-profile-switcher", "displayName": "Profile Switcher", "description": "Allows you to switch between different profiles you have created", - "version": "0.2.0", + "version": "0.3.0", "preview": true, "license": "SEE LICENSE IN LICENSE.md", "publisher": "aaronpowell", @@ -67,12 +67,31 @@ }, "profileSwitcher.storage": { "type": "object", - "description": "These are the details for each profile that has been saved. Probably don't hand-edit this", + "description": "These are the settings for each profile that has been saved. Probably don't hand-edit this", "patternProperties": { ".*": { "type": "object" } } + }, + "profileSwitcher.extensions": { + "type": "object", + "description": "These are the extensions for each profile that has been saved. Probably don't hand-edit this", + "patternProperties": { + ".*": { + "type": "array" + } + } + }, + "profileSwitcher.extensionsIgnore": { + "type": "array", + "description": "Extensions that will not be removed when profiles are switches. Enter the profile ID like 'ms-vsliveshare.vsliveshare'.", + "default": [ + "shan.code-settings-sync", + "ms-vsliveshare.vsliveshare", + "ms-vsliveshare.vsliveshare-audio", + "ms-vsliveshare.vsliveshare-pack" + ] } } } diff --git a/src/constants.ts b/src/constants.ts index 56839e4..a6cb74d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,3 +5,5 @@ export const ExtensionId = `${ExtensionPublisher}.${ExtensionName}`; export const ConfigKey = "profileSwitcher"; export const ConfigProfilesKey = "profiles"; export const ConfigStorageKey = "storage"; +export const ConfigExtensionsKey = "extensions"; +export const ConfigExtensionsIgnoreKey = "extensionsIgnore"; diff --git a/src/extension.ts b/src/extension.ts index f1d7830..eac8181 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,8 +2,13 @@ import * as vscode from "vscode"; import SettingsHelper from "./settingsHelper"; import ContributedCommands from "./commands"; import Config from "./services/config"; +import ExtensionHelper from "./services/extensions"; -function selectProfile(config: Config, settingsHelper: SettingsHelper) { +function selectProfile( + config: Config, + settingsHelper: SettingsHelper, + extensionsHelper: ExtensionHelper +) { return async () => { let profiles = config.getProfiles(); @@ -22,13 +27,33 @@ function selectProfile(config: Config, settingsHelper: SettingsHelper) { return; } - let profileSettings = config.getProfileSettings(profile); + let msg = vscode.window.setStatusBarMessage("Switching profiles."); + let profileSettings = config.getProfileSettings(profile); await settingsHelper.updateUserSettings(profileSettings); + + let extensions = config.getProfileExtensions(profile); + await extensionsHelper.installExtensions(extensions); + await extensionsHelper.removeExtensions(extensions); + + msg.dispose(); + + const message = await vscode.window.showInformationMessage( + "Do you want to reload and activate the extensions?", + "Yes" + ); + + if (message === "Yes") { + vscode.commands.executeCommand("workbench.action.reloadWindow"); + } }; } -function saveProfile(config: Config, settingsHelper: SettingsHelper) { +function saveProfile( + config: Config, + settingsHelper: SettingsHelper, + extensionsHelper: ExtensionHelper +) { return async () => { let profiles = config.getProfiles(); @@ -52,7 +77,11 @@ function saveProfile(config: Config, settingsHelper: SettingsHelper) { } let userSettings = await settingsHelper.getUserSettings(); + + let extensions = extensionsHelper.getInstalled(); + await config.addProfileSettings(profile, userSettings); + await config.addExtensions(profile, extensions); vscode.window.showInformationMessage(`Profile ${profile} has been saved.`); }; @@ -79,6 +108,7 @@ function deleteProfile(config: Config) { await config.removeProfile(profile); await config.removeProfileSettings(profile); + await config.removeProfileExtensions(profile); await vscode.window.showInformationMessage( `Profile ${profile} has been deleted.` @@ -87,20 +117,21 @@ function deleteProfile(config: Config) { } export async function activate(context: vscode.ExtensionContext) { - let settingsHelper = new SettingsHelper(context); let config = new Config(); + let settingsHelper = new SettingsHelper(context); + let extensionsHelper = new ExtensionHelper(context, settingsHelper, config); context.subscriptions.push( vscode.commands.registerCommand( ContributedCommands.SelectProfile, - selectProfile(config, settingsHelper) + selectProfile(config, settingsHelper, extensionsHelper) ) ); context.subscriptions.push( vscode.commands.registerCommand( ContributedCommands.SaveProfile, - saveProfile(config, settingsHelper) + saveProfile(config, settingsHelper, extensionsHelper) ) ); diff --git a/src/services/config.ts b/src/services/config.ts index 1e5c772..c535974 100755 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -1,5 +1,12 @@ import * as vscode from "vscode"; -import { ConfigKey, ConfigProfilesKey, ConfigStorageKey } from "../constants"; +import { + ConfigKey, + ConfigProfilesKey, + ConfigStorageKey, + ConfigExtensionsKey, + ConfigExtensionsIgnoreKey +} from "../constants"; +import { ExtensionInfo } from "./extensions"; export interface Settings { [key: string]: number | string | boolean | object; @@ -9,6 +16,10 @@ export interface Storage { [key: string]: Settings; } +interface ExtensionStorage { + [key: string]: ExtensionInfo[]; +} + class Config { private getConfig() { return vscode.workspace.getConfiguration(ConfigKey); @@ -24,6 +35,10 @@ class Config { return this.getStorage()[profile]; } + public getProfileExtensions(profile: string) { + return this.getExtensions()[profile] || []; + } + private getStorage() { let config = this.getConfig(); @@ -87,6 +102,40 @@ class Config { vscode.ConfigurationTarget.Global ); } + + public addExtensions(profile: string, extensions: ExtensionInfo[]) { + let storage = this.getExtensions(); + storage[profile] = extensions; + return this.updateExtensions(storage); + } + + private getExtensions() { + let config = this.getConfig(); + + return config.get(ConfigExtensionsKey, {}); + } + + private updateExtensions(storage: ExtensionStorage) { + let config = this.getConfig(); + + return config.update( + ConfigExtensionsKey, + storage, + vscode.ConfigurationTarget.Global + ); + } + + public removeProfileExtensions(profile: string) { + let storage = this.getExtensions(); + delete storage[profile]; + return this.updateExtensions(storage); + } + + public getIgnoredExtensions() { + let config = this.getConfig(); + + return config.get(ConfigExtensionsIgnoreKey, []); + } } export default Config; diff --git a/src/services/extensions.ts b/src/services/extensions.ts new file mode 100755 index 0000000..6c05391 --- /dev/null +++ b/src/services/extensions.ts @@ -0,0 +1,143 @@ +import * as vscode from "vscode"; +import { ExtensionId } from "../constants"; +import * as fs from "fs-extra"; +import { join } from "path"; +import SettingsHelper from "../settingsHelper"; +import Config from "./config"; + +export class ExtensionInfo { + public constructor( + public id: string, + public publisherId: string, + public publisherName: string, + public version: string, + public name: string + ) {} +} + +class ExtensionHelper { + public constructor( + private context: vscode.ExtensionContext, + private settings: SettingsHelper, + private config: Config + ) {} + + public getInstalled() { + let ignoredExtensions = this.config.getIgnoredExtensions(); + + return ( + vscode.extensions.all + .filter(ext => !ext.packageJSON.isBuiltin) + // ignore the current extension + .filter(ext => ext.id.toLowerCase() !== ExtensionId.toLowerCase()) + .filter( + ext => + ignoredExtensions.filter( + iext => iext.toLowerCase() === ext.id.toLowerCase() + ).length === 0 + ) + .map( + ext => + new ExtensionInfo( + ext.packageJSON.uuid, + ext.packageJSON.id, + ext.packageJSON.publisher, + ext.packageJSON.version, + ext.packageJSON.name + ) + ) + ); + } + + private async removeExtension(ext: ExtensionInfo) { + const name = `${ext.publisherName}.${ext.name}-${ext.version}`; + let extPath = join(this.settings.ExtensionFolder, name); + + if (!(await fs.pathExists(extPath))) { + console.log( + `Profile Switcher: Extension ${name} didn't exist at path ${extPath}. Skipping removal.` + ); + return; + } + + try { + let backupPath = join(this.context.globalStoragePath, name); + if (!(await fs.pathExists(join(this.context.globalStoragePath, name)))) { + await fs.copy(extPath, backupPath); + } + } catch (e) { + console.log(`Profile Switcher: Error backing up exstension ${name}`); + console.log(e); + } + + try { + await fs.remove(extPath); + } catch (e) { + console.log(`Profile Switcher: Error removing exstension ${name}`); + console.log(e); + } + } + + public async removeExtensions(extensions: ExtensionInfo[]) { + let installedExtensions = this.getInstalled(); + + let extensionsToRemove = installedExtensions.filter( + ext => extensions.filter(e => e.id === ext.id).length === 0 + ); + + if (!(await fs.pathExists(this.context.globalStoragePath))) { + await fs.mkdir(this.context.globalStoragePath); + } + + let removes = extensionsToRemove.map(ext => this.removeExtension(ext)); + + await Promise.all(removes); + } + + private async installExtension(ext: ExtensionInfo) { + const name = `${ext.publisherName}.${ext.name}-${ext.version}`; + + const backupPath = join(this.context.globalStoragePath, name); + let installed = false; + if (await fs.pathExists(backupPath)) { + try { + await fs.copy(backupPath, join(this.settings.ExtensionFolder, name)); + installed = true; + } catch (e) { + console.log( + `Profile Switcher: Error copying ${name} from backup path, will try to force install.` + ); + console.log(e); + } + } + + if (!installed) { + try { + // todo: install via the VS Code CLI + } catch (e) { + console.log( + `Profile Switcher: Error installing ${name} from marketplace.` + ); + console.log(e); + } + } + } + + public async installExtensions(extensions: ExtensionInfo[]) { + let installedExtensions = this.getInstalled(); + + let newExtensions = extensions.filter( + ext => installedExtensions.filter(e => e.id === ext.id).length === 0 + ); + + if (!(await fs.pathExists(this.context.globalStoragePath))) { + await fs.mkdir(this.context.globalStoragePath); + } + + let installs = newExtensions.map(ext => this.installExtension(ext)); + + await Promise.all(installs); + } +} + +export default ExtensionHelper; diff --git a/src/settingsHelper.ts b/src/settingsHelper.ts index a931bdc..ac81702 100755 --- a/src/settingsHelper.ts +++ b/src/settingsHelper.ts @@ -14,11 +14,12 @@ export default class SettingsHelper { public isInsiders: boolean = false; public isOss: boolean = false; public isPortable: boolean = false; - public homeDir: string | undefined; + public homeDir: string; public USER_FOLDER: string; public PATH: string = ""; public OsType: OsType = OsType.Windows; + public ExtensionFolder: string; public constructor(private context: vscode.ExtensionContext) { this.isInsiders = /insiders/.test(this.context.asAbsolutePath("")); @@ -30,8 +31,12 @@ export default class SettingsHelper { process.platform === "linux" && !!process.env.XDG_DATA_HOME; this.homeDir = isXdg - ? process.env.XDG_DATA_HOME - : process.env[process.platform === "win32" ? "USERPROFILE" : "HOME"]; + ? process.env.XDG_DATA_HOME || "" + : process.env[process.platform === "win32" ? "USERPROFILE" : "HOME"] || + ""; + const configSuffix = `${isXdg ? "" : "."}vscode${ + this.isInsiders ? "-insiders" : this.isOss ? "-oss" : "" + }`; if (!this.isPortable) { if (process.platform === "darwin") { @@ -85,9 +90,15 @@ export default class SettingsHelper { console.error(e); } } + this.ExtensionFolder = path.join( + this.homeDir, + configSuffix, + "extensions" + ); this.USER_FOLDER = this.PATH.concat("/User/"); } else { this.USER_FOLDER = this.PATH.concat("/user-data/User/"); + this.ExtensionFolder = this.PATH.concat("/extensions/"); } }