Skip to content

Expose the API via exports instead of editor commands #2855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 66 additions & 66 deletions src/features/ExternalApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
*--------------------------------------------------------*/
import * as vscode from "vscode";
import { v4 as uuidv4 } from 'uuid';
import { LanguageClient } from "vscode-languageclient";
import { LanguageClientConsumer } from "../languageClientConsumer";
import { Logger } from "../logging";
import { SessionManager } from "../session";
Expand All @@ -15,71 +14,44 @@ export interface IExternalPowerShellDetails {
architecture: string;
}

export class ExternalApiFeature extends LanguageClientConsumer {
private commands: vscode.Disposable[];
export interface IPowerShellExtensionClient {
registerExternalExtension(id: string, apiVersion?: string): string;
unregisterExternalExtension(uuid: string): boolean;
getPowerShellVersionDetails(uuid: string): Promise<IExternalPowerShellDetails>;
}

/*
In order to use this in a Visual Studio Code extension, you can do the following:

const powershellExtension = vscode.extensions.getExtension<IPowerShellExtensionClient>("ms-vscode.PowerShell-Preview");
const powerShellExtensionClient = powershellExtension!.exports as IPowerShellExtensionClient;

NOTE: At some point, we should release a helper npm package that wraps the API and does:
* Discovery of what extension they have installed: PowerShell or PowerShell Preview
* Manages session id for you

*/
export class ExternalApiFeature extends LanguageClientConsumer implements IPowerShellExtensionClient {
private static readonly registeredExternalExtension: Map<string, IExternalExtension> = new Map<string, IExternalExtension>();

constructor(private sessionManager: SessionManager, private log: Logger) {
super();
this.commands = [
/*
DESCRIPTION:
Registers your extension to allow usage of the external API. The returns
a session UUID that will need to be passed in to subsequent API calls.

USAGE:
vscode.commands.executeCommand(
"PowerShell.RegisterExternalExtension",
"ms-vscode.PesterTestExplorer" // the name of the extension using us
"v1"); // API Version.

RETURNS:
string session uuid
*/
vscode.commands.registerCommand("PowerShell.RegisterExternalExtension", (id: string, apiVersion: string = 'v1'): string =>
this.registerExternalExtension(id, apiVersion)),

/*
DESCRIPTION:
Unregisters a session that an extension has. This returns
true if it succeeds or throws if it fails.

USAGE:
vscode.commands.executeCommand(
"PowerShell.UnregisterExternalExtension",
"uuid"); // the uuid from above for tracking purposes

RETURNS:
true if it worked, otherwise throws an error.
*/
vscode.commands.registerCommand("PowerShell.UnregisterExternalExtension", (uuid: string = ""): boolean =>
this.unregisterExternalExtension(uuid)),

/*
DESCRIPTION:
This will fetch the version details of the PowerShell used to start
PowerShell Editor Services in the PowerShell extension.

USAGE:
vscode.commands.executeCommand(
"PowerShell.GetPowerShellVersionDetails",
"uuid"); // the uuid from above for tracking purposes

RETURNS:
An IPowerShellVersionDetails which consists of:
{
version: string;
displayVersion: string;
edition: string;
architecture: string;
}
*/
vscode.commands.registerCommand("PowerShell.GetPowerShellVersionDetails", (uuid: string = ""): Promise<IExternalPowerShellDetails> =>
this.getPowerShellVersionDetails(uuid)),
]
}

private registerExternalExtension(id: string, apiVersion: string = 'v1'): string {
/*
DESCRIPTION:
Registers your extension to allow usage of the external API. The returns
a session UUID that will need to be passed in to subsequent API calls.

USAGE:
powerShellExtensionClient.registerExternalExtension(
"ms-vscode.PesterTestExplorer" // the name of the extension using us
"v1"); // API Version.

RETURNS:
string session uuid
*/
public registerExternalExtension(id: string, apiVersion: string = 'v1'): string {
this.log.writeDiagnostic(`Registering extension '${id}' for use with API version '${apiVersion}'.`);

for (const [_, externalExtension] of ExternalApiFeature.registeredExternalExtension) {
Expand Down Expand Up @@ -107,18 +79,48 @@ export class ExternalApiFeature extends LanguageClientConsumer {
return uuid;
}

private unregisterExternalExtension(uuid: string = ""): boolean {
/*
DESCRIPTION:
Unregisters a session that an extension has. This returns
true if it succeeds or throws if it fails.

USAGE:
powerShellExtensionClient.unregisterExternalExtension(
"uuid"); // the uuid from above for tracking purposes

RETURNS:
true if it worked, otherwise throws an error.
*/
public unregisterExternalExtension(uuid: string = ""): boolean {
this.log.writeDiagnostic(`Unregistering extension with session UUID: ${uuid}`);
if (!ExternalApiFeature.registeredExternalExtension.delete(uuid)) {
throw new Error(`No extension registered with session UUID: ${uuid}`);
}
return true;
}

private async getPowerShellVersionDetails(uuid: string = ""): Promise<IExternalPowerShellDetails> {
/*
DESCRIPTION:
This will fetch the version details of the PowerShell used to start
PowerShell Editor Services in the PowerShell extension.

USAGE:
powerShellExtensionClient.getPowerShellVersionDetails(
"uuid"); // the uuid from above for tracking purposes

RETURNS:
An IPowerShellVersionDetails which consists of:
{
version: string;
displayVersion: string;
edition: string;
architecture: string;
}
*/
public async getPowerShellVersionDetails(uuid: string = ""): Promise<IExternalPowerShellDetails> {
if (!ExternalApiFeature.registeredExternalExtension.has(uuid)) {
throw new Error(
"UUID provided was invalid, make sure you execute the 'PowerShell.GetPowerShellVersionDetails' command and pass in the UUID that it returns to subsequent command executions.");
"UUID provided was invalid, make sure you ran the 'powershellExtensionClient.registerExternalExtension(extensionId)' method and pass in the UUID that it returns to subsequent methods.");
}

// TODO: When we have more than one API version, make sure to include a check here.
Expand All @@ -137,9 +139,7 @@ export class ExternalApiFeature extends LanguageClientConsumer {
}

public dispose() {
for (const command of this.commands) {
command.dispose();
}
// Nothing to dispose.
}
}

Expand Down
14 changes: 11 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { DebugSessionFeature } from "./features/DebugSession";
import { ExamplesFeature } from "./features/Examples";
import { ExpandAliasFeature } from "./features/ExpandAlias";
import { ExtensionCommandsFeature } from "./features/ExtensionCommands";
import { ExternalApiFeature } from "./features/ExternalApi";
import { ExternalApiFeature, IPowerShellExtensionClient } from "./features/ExternalApi";
import { FindModuleFeature } from "./features/FindModule";
import { GenerateBugReportFeature } from "./features/GenerateBugReport";
import { GetCommandsFeature } from "./features/GetCommands";
Expand Down Expand Up @@ -54,7 +54,7 @@ const documentSelector: DocumentSelector = [
{ language: "powershell", scheme: "untitled" },
];

export function activate(context: vscode.ExtensionContext): void {
export function activate(context: vscode.ExtensionContext): IPowerShellExtensionClient {
// create telemetry reporter on extension activation
telemetryReporter = new TelemetryReporter(PackageJSON.name, PackageJSON.version, AI_KEY);

Expand Down Expand Up @@ -147,6 +147,8 @@ export function activate(context: vscode.ExtensionContext): void {
new SpecifyScriptArgsFeature(context),
]

const externalApi = new ExternalApiFeature(sessionManager, logger);

// Features and command registrations that require language client
languageClientConsumers = [
new ConsoleFeature(logger),
Expand All @@ -162,7 +164,7 @@ export function activate(context: vscode.ExtensionContext): void {
new HelpCompletionFeature(logger),
new CustomViewsFeature(),
new PickRunspaceFeature(),
new ExternalApiFeature(sessionManager, logger)
externalApi
];

// Notebook UI is only supported in VS Code Insiders.
Expand All @@ -188,6 +190,12 @@ export function activate(context: vscode.ExtensionContext): void {
if (extensionSettings.startAutomatically) {
sessionManager.start();
}

return {
registerExternalExtension: (id: string, apiVersion: string = 'v1') => externalApi.registerExternalExtension(id, apiVersion),
unregisterExternalExtension: uuid => externalApi.unregisterExternalExtension(uuid),
getPowerShellVersionDetails: uuid => externalApi.getPowerShellVersionDetails(uuid),
};
}

function checkForUpdatedVersion(context: vscode.ExtensionContext, version: string) {
Expand Down
72 changes: 48 additions & 24 deletions test/features/ExternalApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,37 @@
*--------------------------------------------------------*/
import * as assert from "assert";
import * as vscode from "vscode";
import { beforeEach, afterEach } from "mocha";
import { IExternalPowerShellDetails } from "../../src/features/ExternalApi";
import { before, beforeEach, afterEach } from "mocha";
import { IExternalPowerShellDetails, IPowerShellExtensionClient } from "../../src/features/ExternalApi";

const testExtensionId = "ms-vscode.powershell-preview";

suite("ExternalApi feature - Registration API", () => {
test("It can register and unregister an extension", async () => {
const sessionId: string = await vscode.commands.executeCommand("PowerShell.RegisterExternalExtension", testExtensionId);
let powerShellExtensionClient: IPowerShellExtensionClient;
before(async () => {
const powershellExtension = vscode.extensions.getExtension<IPowerShellExtensionClient>(testExtensionId);
if (!powershellExtension.isActive) {
powerShellExtensionClient = await powershellExtension.activate();
return;
}
powerShellExtensionClient = powershellExtension!.exports as IPowerShellExtensionClient;
});

test("It can register and unregister an extension", () => {
const sessionId: string = powerShellExtensionClient.registerExternalExtension(testExtensionId);
assert.notStrictEqual(sessionId , "");
assert.notStrictEqual(sessionId , null);
assert.strictEqual(
await vscode.commands.executeCommand("PowerShell.UnregisterExternalExtension", sessionId),
powerShellExtensionClient.unregisterExternalExtension(sessionId),
true);
});

test("It can register and unregister an extension with a version", async () => {
const sessionId: string = await vscode.commands.executeCommand("PowerShell.RegisterExternalExtension", "ms-vscode.powershell-preview", "v2");
test("It can register and unregister an extension with a version", () => {
const sessionId: string = powerShellExtensionClient.registerExternalExtension(testExtensionId, "v2");
assert.notStrictEqual(sessionId , "");
assert.notStrictEqual(sessionId , null);
assert.strictEqual(
await vscode.commands.executeCommand("PowerShell.UnregisterExternalExtension", sessionId),
powerShellExtensionClient.unregisterExternalExtension(sessionId),
true);
});

Expand All @@ -32,41 +42,55 @@ suite("ExternalApi feature - Registration API", () => {
*/
test("API fails if not registered", async () => {
assert.rejects(
async () => await vscode.commands.executeCommand("PowerShell.GetPowerShellVersionDetails"),
"UUID provided was invalid, make sure you execute the 'PowerShell.RegisterExternalExtension' command and pass in the UUID that it returns to subsequent command executions.");
async () => await powerShellExtensionClient.getPowerShellVersionDetails(""),
"UUID provided was invalid, make sure you ran the 'powershellExtensionClient.registerExternalExtension(extensionId)' method and pass in the UUID that it returns to subsequent methods.");
});

test("It can't register the same extension twice", async () => {
const sessionId: string = await vscode.commands.executeCommand("PowerShell.RegisterExternalExtension", testExtensionId);
const sessionId: string = powerShellExtensionClient.registerExternalExtension(testExtensionId);
try {
assert.rejects(
async () => await vscode.commands.executeCommand("PowerShell.RegisterExternalExtension", testExtensionId),
`The extension '${testExtensionId}' is already registered.`);
assert.throws(
() => powerShellExtensionClient.registerExternalExtension(testExtensionId),
{
message: `The extension '${testExtensionId}' is already registered.`
});
} finally {
await vscode.commands.executeCommand("PowerShell.UnregisterExternalExtension", sessionId);
powerShellExtensionClient.unregisterExternalExtension(sessionId);
}
});

test("It can't unregister an extension that isn't registered", async () => {
assert.rejects(
async () => await vscode.commands.executeCommand("PowerShell.RegisterExternalExtension", "not-real"),
`No extension registered with session UUID: not-real`);
});
assert.throws(
() => powerShellExtensionClient.unregisterExternalExtension("not-real"),
{
message: `No extension registered with session UUID: not-real`
});
});
});

suite("ExternalApi feature - Other APIs", () => {
let sessionId: string;
let powerShellExtensionClient: IPowerShellExtensionClient;

before(async () => {
const powershellExtension = vscode.extensions.getExtension<IPowerShellExtensionClient>(testExtensionId);
if (!powershellExtension.isActive) {
powerShellExtensionClient = await powershellExtension.activate();
return;
}
powerShellExtensionClient = powershellExtension!.exports as IPowerShellExtensionClient;
});

beforeEach(async () => {
sessionId = await vscode.commands.executeCommand("PowerShell.RegisterExternalExtension", "ms-vscode.powershell-preview");
beforeEach(() => {
sessionId = powerShellExtensionClient.registerExternalExtension("ms-vscode.powershell-preview");
});

afterEach(async () => {
await vscode.commands.executeCommand("PowerShell.UnregisterExternalExtension", sessionId);
afterEach(() => {
powerShellExtensionClient.unregisterExternalExtension(sessionId);
});

test("It can get PowerShell version details", async () => {
const versionDetails: IExternalPowerShellDetails = await vscode.commands.executeCommand("PowerShell.GetPowerShellVersionDetails", sessionId);
const versionDetails: IExternalPowerShellDetails = await powerShellExtensionClient.getPowerShellVersionDetails(sessionId);

assert.notStrictEqual(versionDetails.architecture, "");
assert.notStrictEqual(versionDetails.architecture, null);
Expand Down
4 changes: 2 additions & 2 deletions test/features/ISECompatibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ suite("ISECompatibility feature", () => {
const currently = vscode.workspace.getConfiguration(iseSetting.path).get(iseSetting.name);
assert.notEqual(currently, iseSetting.value);
}
});
}).timeout(10000);
test("It leaves Theme after being changed after enabling ISE Mode", async () => {
await vscode.commands.executeCommand("PowerShell.EnableISEMode");
assert.equal(vscode.workspace.getConfiguration("workbench").get("colorTheme"), "PowerShell ISE");
Expand All @@ -35,5 +35,5 @@ suite("ISECompatibility feature", () => {
assert.notEqual(currently, iseSetting.value);
}
assert.equal(vscode.workspace.getConfiguration("workbench").get("colorTheme"), "Dark+");
});
}).timeout(10000);
});