diff --git a/.vscode/launch.json b/.vscode/launch.json index 6018202..d60f8b3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,33 +3,29 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { - "version": "0.2.0", - "configurations": [{ - "name": "Run Extension", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "npm: watch" - }, - { - "name": "Extension Tests", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test" - ], - "outFiles": [ - "${workspaceFolder}/out/test/**/*.js" - ], - "preLaunchTask": "npm: watch" - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "npm: watch" + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test", + "--user-data-dir=${workspaceFolder}/.vscode-test/user" + ], + "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "preLaunchTask": "npm: watch" + } + ] } diff --git a/package-lock.json b/package-lock.json index 2e7461d..f626cc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vscode-profile-switcher", - "version": "0.0.1", + "version": "0.1.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -24,6 +24,12 @@ "js-tokens": "^4.0.0" } }, + "@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, "@types/fs-extra": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-7.0.0.tgz", @@ -99,6 +105,12 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -166,6 +178,20 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -177,6 +203,12 @@ "supports-color": "^5.3.0" } }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -237,6 +269,15 @@ "ms": "2.0.0" } }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -349,6 +390,12 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -663,6 +710,12 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -871,6 +924,12 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "dev": true }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "typescript": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", diff --git a/package.json b/package.json index d2eacad..f54bad5 100644 --- a/package.json +++ b/package.json @@ -84,12 +84,15 @@ "postinstall": "node ./node_modules/vscode/bin/install", "test": "npm run compile && node ./node_modules/vscode/bin/test", "package": "npx vsce package", - "publish": "npx vsce publish" + "publish": "npx vsce publish", + "lint": "tslint --project tsconfig.json -e src/*.d.ts -t verbose" }, "devDependencies": { + "@types/chai": "^4.1.7", "@types/fs-extra": "^7.0.0", "@types/mocha": "^2.2.42", "@types/node": "^11.13.0", + "chai": "^4.2.0", "tslint": "^5.12.1", "typescript": "^3.3.1", "vscode": "^1.1.28" diff --git a/src/commands.ts b/src/commands.ts new file mode 100755 index 0000000..30e3aaa --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,7 @@ +enum Commands { + SelectProfile = "extension.selectProfile", + SaveProfile = "extension.saveProfile", + DeleteProfile = "extension.deleteProfile" +} + +export default Commands; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..56839e4 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,7 @@ +export const ExtensionName = "vscode-profile-switcher"; +export const ExtensionPublisher = "aaronpowell"; +export const ExtensionId = `${ExtensionPublisher}.${ExtensionName}`; + +export const ConfigKey = "profileSwitcher"; +export const ConfigProfilesKey = "profiles"; +export const ConfigStorageKey = "storage"; diff --git a/src/extension.ts b/src/extension.ts index dfc1f15..d0099b4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,19 +1,15 @@ import * as vscode from "vscode"; import SettingsHelper from "./settingsHelper"; - -function getProfiles(profileSwitcherSettings: vscode.WorkspaceConfiguration) { - return profileSwitcherSettings.get("profiles", []).sort(); -} +import Commands from "./commands"; +import Config from "./services/config"; export async function activate(context: vscode.ExtensionContext) { - let config = new SettingsHelper(context); + let settingsHelper = new SettingsHelper(context); + let config = new Config(); context.subscriptions.push( - vscode.commands.registerCommand("extension.selectProfile", async () => { - let profileSwitcherSettings = vscode.workspace.getConfiguration( - "profileSwitcher" - ); - let profiles = getProfiles(profileSwitcherSettings); + vscode.commands.registerCommand(Commands.SelectProfile, async () => { + let profiles = config.getProfiles(); if (!profiles.length) { await vscode.window.showInformationMessage( @@ -30,21 +26,15 @@ export async function activate(context: vscode.ExtensionContext) { return; } - let storage = profileSwitcherSettings.get<{ [key: string]: any }>( - "storage", - {} - ); + let profileSettings = config.getProfileSettings(profile); - await config.updateUserSettings(storage[profile]); + await settingsHelper.updateUserSettings(profileSettings); }) ); context.subscriptions.push( - vscode.commands.registerCommand("extension.saveProfile", async () => { - let profileSwitcherSettings = vscode.workspace.getConfiguration( - "profileSwitcher" - ); - let profiles = getProfiles(profileSwitcherSettings); + vscode.commands.registerCommand(Commands.SaveProfile, async () => { + let profiles = config.getProfiles(); let profile = await vscode.window.showQuickPick( [...profiles, "New profile"], @@ -62,36 +52,11 @@ export async function activate(context: vscode.ExtensionContext) { return; } - profiles.push(profile); - } - - await profileSwitcherSettings.update( - "profiles", - profiles, - vscode.ConfigurationTarget.Global - ); - - let userSettings = await config.getUserSettings(); - - // We don't want to save profile info in the profile storage - if (userSettings["profileSwitcher.profiles"]) { - delete userSettings["profileSwitcher.profiles"]; - } - - if (userSettings["profileSwitcher.storage"]) { - delete userSettings["profileSwitcher.storage"]; + await config.addProfile(profile); } - let storage = profileSwitcherSettings.get<{ [key: string]: any }>( - "storage", - {} - ); - storage[profile] = userSettings; - await profileSwitcherSettings.update( - "storage", - storage, - vscode.ConfigurationTarget.Global - ); + let userSettings = await settingsHelper.getUserSettings(); + await config.addProfileSettings(profile, userSettings); vscode.window.showInformationMessage( `Profile ${profile} has been saved.` @@ -100,11 +65,8 @@ export async function activate(context: vscode.ExtensionContext) { ); context.subscriptions.push( - vscode.commands.registerCommand("extension.deleteProfile", async () => { - let profileSwitcherSettings = vscode.workspace.getConfiguration( - "profileSwitcher" - ); - let profiles = getProfiles(profileSwitcherSettings); + vscode.commands.registerCommand(Commands.DeleteProfile, async () => { + let profiles = config.getProfiles(); if (!profiles.length) { await vscode.window.showInformationMessage( @@ -121,28 +83,8 @@ export async function activate(context: vscode.ExtensionContext) { return; } - let newProfiles = profiles - .slice(0, profiles.indexOf(profile)) - .concat(profiles.slice(profiles.indexOf(profile) + 1, profiles.length)); - - await profileSwitcherSettings.update( - "profiles", - newProfiles, - vscode.ConfigurationTarget.Global - ); - - let storage = profileSwitcherSettings.get<{ [key: string]: any }>( - "storage", - {} - ); - - delete storage[profile]; - - await profileSwitcherSettings.update( - "storage", - storage, - vscode.ConfigurationTarget.Global - ); + await config.removeProfile(profile); + await config.removeProfileSettings(profile); await vscode.window.showInformationMessage( `Profile ${profile} has been deleted.` diff --git a/src/services/config.ts b/src/services/config.ts new file mode 100755 index 0000000..eb27b46 --- /dev/null +++ b/src/services/config.ts @@ -0,0 +1,86 @@ +import * as vscode from "vscode"; +import { ConfigKey, ConfigProfilesKey, ConfigStorageKey } from "../constants"; + +type Storage = { [key: string]: any }; + +class Config { + private getConfig() { + return vscode.workspace.getConfiguration(ConfigKey); + } + + public getProfiles() { + let config = this.getConfig(); + + return config.get(ConfigProfilesKey, []).sort(); + } + + public getProfileSettings(profile: string) { + return this.getStorage()[profile]; + } + + private getStorage() { + let config = this.getConfig(); + + return config.get(ConfigStorageKey, {}); + } + + public addProfile(profile: string) { + let config = this.getConfig(); + + let existingProfiles = this.getProfiles(); + + return config.update( + ConfigProfilesKey, + [...existingProfiles, profile], + vscode.ConfigurationTarget.Global + ); + } + + public removeProfile(profile: string) { + let profiles = this.getProfiles(); + let newProfiles = profiles + .slice(0, profiles.indexOf(profile)) + .concat(profiles.slice(profiles.indexOf(profile) + 1, profiles.length)); + + let config = this.getConfig(); + + return config.update( + ConfigProfilesKey, + newProfiles, + vscode.ConfigurationTarget.Global + ); + } + + public addProfileSettings(profile: string, settings: any) { + // We don't want to save profile info in the profile storage + if (settings["profileSwitcher.profiles"]) { + delete settings["profileSwitcher.profiles"]; + } + + if (settings["profileSwitcher.storage"]) { + delete settings["profileSwitcher.storage"]; + } + + let storage = this.getStorage(); + storage[profile] = settings; + return this.updateStorage(storage); + } + + public removeProfileSettings(profile: string) { + let storage = this.getStorage(); + delete storage[profile]; + return this.updateStorage(storage); + } + + private updateStorage(storage: Storage) { + let config = this.getConfig(); + + return config.update( + ConfigStorageKey, + storage, + vscode.ConfigurationTarget.Global + ); + } +} + +export default Config; diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts new file mode 100644 index 0000000..33369f6 --- /dev/null +++ b/src/test/extension.test.ts @@ -0,0 +1,256 @@ +import { assert } from "chai"; +import * as vscode from "vscode"; +import { + ExtensionId, + ConfigKey, + ConfigProfilesKey, + ConfigStorageKey +} from "../constants"; +import Config from "../services/config"; +import SettingsHelper from "../settingsHelper"; + +suite("basic extension tests", () => { + test("extension is registered", () => { + const extension = vscode.extensions.getExtension(ExtensionId); + assert.isDefined(extension); + }); + + test("extension can activate", done => { + const extension = >( + vscode.extensions.getExtension(ExtensionId) + ); + + setTimeout(() => { + assert.isTrue(extension.isActive); + done(); + }, 200); + }); +}); + +suite("select profile", () => { + const expectedProfileName = "test1"; + const expectedProfileSettings = { foo: "bar" }; + + setup(async () => { + let config = vscode.workspace.getConfiguration(ConfigKey); + + await config.update( + ConfigProfilesKey, + [expectedProfileName], + vscode.ConfigurationTarget.Global + ); + await config.update( + ConfigStorageKey, + { + [expectedProfileName]: expectedProfileSettings + }, + vscode.ConfigurationTarget.Global + ); + }); + + teardown(async () => { + let config = vscode.workspace.getConfiguration(ConfigKey); + + await config.update( + ConfigProfilesKey, + undefined, + vscode.ConfigurationTarget.Global + ); + await config.update( + ConfigStorageKey, + undefined, + vscode.ConfigurationTarget.Global + ); + }); + + test("list of profiles will contain the expected one", () => { + let config = new Config(); + + let profiles = config.getProfiles(); + + assert.include(profiles, expectedProfileName); + }); + + test("storage contains the expected profile", () => { + let config = new Config(); + + let settings = config.getProfileSettings(expectedProfileName); + + assert.deepEqual(settings, expectedProfileSettings); + }); +}); + +suite("save profile", () => { + const expectedProfileName = "test1"; + const expectedProfileSettings = { foo: "bar" }; + const expectedUpdatedProfileSettings = { foo: "baz", a: "b" }; + + teardown(async () => { + let config = vscode.workspace.getConfiguration(ConfigKey); + + await config.update( + ConfigProfilesKey, + undefined, + vscode.ConfigurationTarget.Global + ); + await config.update( + ConfigStorageKey, + undefined, + vscode.ConfigurationTarget.Global + ); + }); + + test("can save a profile name", async () => { + var config = new Config(); + + await config.addProfile(expectedProfileName); + + let profiles = config.getProfiles(); + assert.include(profiles, expectedProfileName); + }); + + test("can save profile settings", async () => { + var config = new Config(); + + await config.addProfileSettings( + expectedProfileName, + expectedProfileSettings + ); + + let settings = config.getProfileSettings(expectedProfileName); + assert.deepEqual(settings, expectedProfileSettings); + }); + + test("can update existing profile", async () => { + var config = new Config(); + + await config.addProfileSettings( + expectedProfileName, + expectedProfileSettings + ); + await config.addProfileSettings( + expectedProfileName, + expectedUpdatedProfileSettings + ); + + let settings = config.getProfileSettings(expectedProfileName); + assert.deepEqual(settings, expectedUpdatedProfileSettings); + }); +}); + +suite("remove profile", () => { + const expectedProfileName = "test1"; + const expectedProfileSettings = { foo: "bar" }; + + setup(async () => { + let config = vscode.workspace.getConfiguration(ConfigKey); + + await config.update( + ConfigProfilesKey, + [expectedProfileName], + vscode.ConfigurationTarget.Global + ); + await config.update( + ConfigStorageKey, + { + [expectedProfileName]: expectedProfileSettings + }, + vscode.ConfigurationTarget.Global + ); + }); + + test("can remove profile name", async () => { + let config = new Config(); + + await config.removeProfile(expectedProfileName); + + let profiles = config.getProfiles(); + + assert.notInclude(profiles, expectedProfileName); + }); + + test("can remove profile settings", async () => { + let config = new Config(); + + await config.removeProfileSettings(expectedProfileName); + + let settings = config.getProfileSettings(expectedProfileName); + + assert.isUndefined(settings); + }); +}); + +suite("end to end testing", () => { + const profileName = "end-to-end-test"; + const profileSettings = { + "editor.fontSize": 24, + "workbench.colorTheme": "Default Light+" + }; + + class MockMemento implements vscode.Memento { + get(key: string): T | undefined; + get(key: string, defaultValue: T): T; + get(key: any, defaultValue?: any) { + throw new Error("Method not implemented."); + } + update(key: string, value: any): Thenable { + throw new Error("Method not implemented."); + } + } + + class MockContext implements vscode.ExtensionContext { + subscriptions: { dispose(): any }[]; + workspaceState: vscode.Memento; + globalState: vscode.Memento; + extensionPath: string; + asAbsolutePath(relativePath: string): string { + return process.execPath; + } + storagePath: string | undefined; + globalStoragePath: string; + logPath: string; + + constructor() { + this.subscriptions = []; + this.extensionPath = ""; + this.globalStoragePath = ""; + this.logPath = ""; + + this.workspaceState = new MockMemento(); + this.globalState = new MockMemento(); + } + } + + let mockContext = new MockContext(); + + setup(async function() { + this.settingsHelper = new SettingsHelper(mockContext); + this.defaultSettings = await this.settingsHelper.getUserSettings(); + this.config = new Config(); + }); + + teardown(async function() { + await this.settingsHelper.updateUserSettings(this.defaultSettings); + await this.config.removeProfile(profileName); + await this.config.removeProfileSettings(profileName); + }); + + test("can change the vscode layout based on profile", async function() { + await this.config.addProfile(profileName); + await this.config.addProfileSettings(profileName, profileSettings); + + let updateSettings = this.config.getProfileSettings(profileName); + + await this.settingsHelper.updateUserSettings(updateSettings); + + let currentSettings = await this.settingsHelper.getUserSettings(); + + assert.equal( + currentSettings["editor.fontSize"], + profileSettings["editor.fontSize"] + ); + + // uncomment for local testing if you want to view the changes applied to vscode + // await new Promise(resolve => setTimeout(resolve, 1000)); + }); +}); diff --git a/src/test/index.ts b/src/test/index.ts new file mode 100644 index 0000000..b7b1655 --- /dev/null +++ b/src/test/index.ts @@ -0,0 +1,24 @@ +// +// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING +// +// This file is providing the test runner to use when running extension tests. +// By default the test runner in use is Mocha based. +// +// You can provide your own test runner if you want to override it by exporting +// a function run(testsRoot: string, clb: (error: Error, failures?: number) => void): void +// that the extension host can call to run the tests. The test runner is expected to use console.log +// to report the results back to the caller. When the tests are finished, return +// a possible error to the callback or null if none. + +import * as testRunner from "vscode/lib/testrunner"; + +// You can directly control Mocha options by configuring the test runner below +// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options +// for more info +testRunner.configure({ + ui: "tdd", // the TDD UI is being used in extension.test.ts (suite, test, etc.) + useColors: true, // colored output from test results, + timeout: 7500 +}); + +module.exports = testRunner;