Skip to content

Commit b04ddbc

Browse files
committed
Add extension API support
This change adds the editor-side implementation of the PowerShell Editor Services' new extensibility API so that PowerShell-authored extensions will operate in Visual Studio Code.
1 parent d3001c7 commit b04ddbc

File tree

4 files changed

+322
-0
lines changed

4 files changed

+322
-0
lines changed

examples/ExtensionExamples.ps1

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Instructions: select the entire file and hit F8 to
2+
# load the extensions. To see the list of registered
3+
# extensions and run them, hit Ctrl+Shift+P, type 'addi'
4+
# and run the "Show additional commands from PowerShell modules"
5+
# command. A quick pick list will appear with all 3
6+
# extensions registered. Selecting one of them will launch it.
7+
8+
function Invoke-MyCommand {
9+
Write-Output "My command's function was executed!"
10+
}
11+
12+
# Registering a command for an existing function
13+
14+
Register-EditorCommand -Verbose `
15+
-Name "MyModule.MyCommandWithFunction" `
16+
-DisplayName "My command with function" `
17+
-Function Invoke-MyCommand
18+
19+
# Registering a command to run a ScriptBlock
20+
21+
Register-EditorCommand -Verbose `
22+
-Name "MyModule.MyCommandWithScriptBlock" `
23+
-DisplayName "My command with script block" `
24+
-ScriptBlock { Write-Output "My command's script block was executed!" }
25+
26+
# A real command example:
27+
28+
function Invoke-MyEdit([Microsoft.PowerShell.EditorServices.Extensions.EditorContext]$context) {
29+
30+
# Insert text at pre-defined position
31+
32+
$context.CurrentFile.InsertText(
33+
"`r`n# I was inserted by PowerShell code!`r`nGet-Process -Name chrome`r`n",
34+
35, 1);
35+
36+
# TRY THIS ALSO, comment out the above 4 lines and uncomment the below
37+
38+
# # Insert text at cursor position
39+
40+
# $context.CurrentFile.InsertText(
41+
# "Get-Process -Name chrome",
42+
# $context.CursorPosition);
43+
}
44+
45+
# After registering this command, you only need to re-evaluate the
46+
# Invoke-MyEdit command when you've made changes to its code. The
47+
# registration of the command persists.
48+
49+
Register-EditorCommand -Verbose `
50+
-Name "MyModule.MyEditCommand" `
51+
-DisplayName "Apply my edit!" `
52+
-Function Invoke-MyEdit `
53+
-SuppressOutput

package.json

+5
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@
9797
"command": "PowerShell.PowerShellFindModule",
9898
"title": "Find/Install PowerShell modules from the gallery",
9999
"category": "PowerShell"
100+
},
101+
{
102+
"command": "PowerShell.ShowAdditionalCommands",
103+
"title": "Show additional commands from PowerShell modules",
104+
"category": "PowerShell"
100105
}
101106
],
102107
"snippets": [

src/features/ExtensionCommands.ts

+262
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import vscode = require('vscode');
2+
import path = require('path');
3+
import { LanguageClient, RequestType, NotificationType, Range, Position } from 'vscode-languageclient';
4+
5+
export interface ExtensionCommand {
6+
name: string;
7+
displayName: string;
8+
}
9+
10+
export interface ExtensionCommandQuickPickItem extends vscode.QuickPickItem {
11+
command: ExtensionCommand;
12+
}
13+
14+
var extensionCommands: ExtensionCommand[] = [];
15+
16+
export namespace InvokeExtensionCommandRequest {
17+
export const type: RequestType<InvokeExtensionCommandRequestArguments, void, void> =
18+
{ get method() { return 'powerShell/invokeExtensionCommand'; } };
19+
}
20+
21+
export interface EditorContext {
22+
currentFilePath: string;
23+
cursorPosition: Position;
24+
selectionRange: Range;
25+
}
26+
27+
export interface InvokeExtensionCommandRequestArguments {
28+
name: string;
29+
context: EditorContext;
30+
}
31+
32+
export namespace ExtensionCommandAddedNotification {
33+
export const type: NotificationType<ExtensionCommandAddedNotificationBody> =
34+
{ get method() { return 'powerShell/extensionCommandAdded'; } };
35+
}
36+
37+
export interface ExtensionCommandAddedNotificationBody {
38+
name: string;
39+
displayName: string;
40+
}
41+
42+
function addExtensionCommand(command: ExtensionCommandAddedNotificationBody) {
43+
44+
extensionCommands.push({
45+
name: command.name,
46+
displayName: command.displayName
47+
});
48+
}
49+
50+
function showExtensionCommands(client: LanguageClient) : Thenable<InvokeExtensionCommandRequestArguments> {
51+
52+
// If no extension commands are available, show a message
53+
if (extensionCommands.length == 0) {
54+
vscode.window.showInformationMessage(
55+
"No extension commands have been loaded into the current session.");
56+
57+
return;
58+
}
59+
60+
var quickPickItems =
61+
extensionCommands.map<ExtensionCommandQuickPickItem>(command => {
62+
return {
63+
label: command.displayName,
64+
description: "",
65+
command: command
66+
}
67+
});
68+
69+
vscode.window
70+
.showQuickPick(
71+
quickPickItems,
72+
{ placeHolder: "Select a command" })
73+
.then(command => onCommandSelected(command, client));
74+
}
75+
76+
function onCommandSelected(
77+
chosenItem: ExtensionCommandQuickPickItem,
78+
client: LanguageClient) {
79+
80+
if (chosenItem !== undefined) {
81+
client.sendRequest(
82+
InvokeExtensionCommandRequest.type,
83+
{ name: chosenItem.command.name,
84+
context: getEditorContext() });
85+
}
86+
}
87+
88+
// ---------- Editor Operations ----------
89+
90+
function asRange(value: vscode.Range): Range {
91+
92+
if (value === undefined) {
93+
return undefined;
94+
} else if (value === null) {
95+
return null;
96+
}
97+
return { start: asPosition(value.start), end: asPosition(value.end) };
98+
}
99+
100+
function asPosition(value: vscode.Position): Position {
101+
102+
if (value === undefined) {
103+
return undefined;
104+
} else if (value === null) {
105+
return null;
106+
}
107+
return { line: value.line, character: value.character };
108+
}
109+
110+
111+
export function asCodeRange(value: Range): vscode.Range {
112+
113+
if (value === undefined) {
114+
return undefined;
115+
} else if (value === null) {
116+
return null;
117+
}
118+
return new vscode.Range(asCodePosition(value.start), asCodePosition(value.end));
119+
}
120+
121+
export function asCodePosition(value: Position): vscode.Position {
122+
123+
if (value === undefined) {
124+
return undefined;
125+
} else if (value === null) {
126+
return null;
127+
}
128+
return new vscode.Position(value.line, value.character);
129+
}
130+
131+
function getEditorContext(): EditorContext {
132+
return {
133+
currentFilePath: vscode.window.activeTextEditor.document.fileName,
134+
cursorPosition: asPosition(vscode.window.activeTextEditor.selection.active),
135+
selectionRange:
136+
asRange(
137+
new vscode.Range(
138+
vscode.window.activeTextEditor.selection.start,
139+
vscode.window.activeTextEditor.selection.end))
140+
}
141+
}
142+
143+
export namespace GetEditorContextRequest {
144+
export const type: RequestType<GetEditorContextRequestArguments, EditorContext, void> =
145+
{ get method() { return 'editor/getEditorContext'; } };
146+
}
147+
148+
export interface GetEditorContextRequestArguments {
149+
}
150+
151+
enum EditorOperationResponse {
152+
Unsupported = 0,
153+
Completed
154+
}
155+
156+
export namespace InsertTextRequest {
157+
export const type: RequestType<InsertTextRequestArguments, EditorOperationResponse, void> =
158+
{ get method() { return 'editor/insertText'; } };
159+
}
160+
161+
export interface InsertTextRequestArguments {
162+
filePath: string;
163+
insertText: string;
164+
insertRange: Range
165+
}
166+
167+
function insertText(details: InsertTextRequestArguments): EditorOperationResponse {
168+
var edit = new vscode.WorkspaceEdit();
169+
170+
edit.set(
171+
vscode.Uri.parse(details.filePath),
172+
[
173+
new vscode.TextEdit(
174+
new vscode.Range(
175+
details.insertRange.start.line,
176+
details.insertRange.start.character,
177+
details.insertRange.end.line,
178+
details.insertRange.end.character),
179+
details.insertText)
180+
]
181+
);
182+
183+
vscode.workspace.applyEdit(edit);
184+
185+
return EditorOperationResponse.Completed;
186+
}
187+
188+
export namespace SetSelectionRequest {
189+
export const type: RequestType<SetSelectionRequestArguments, EditorOperationResponse, void> =
190+
{ get method() { return 'editor/setSelection'; } };
191+
}
192+
193+
export interface SetSelectionRequestArguments {
194+
selectionRange: Range
195+
}
196+
197+
function setSelection(details: SetSelectionRequestArguments): EditorOperationResponse {
198+
vscode.window.activeTextEditor.selections = [
199+
new vscode.Selection(
200+
asCodePosition(details.selectionRange.start),
201+
asCodePosition(details.selectionRange.end))
202+
]
203+
204+
return EditorOperationResponse.Completed;
205+
}
206+
207+
export namespace OpenFileRequest {
208+
export const type: RequestType<string, EditorOperationResponse, void> =
209+
{ get method() { return 'editor/openFile'; } };
210+
}
211+
212+
function openFile(filePath: string): Thenable<EditorOperationResponse> {
213+
214+
// Make sure the file path is absolute
215+
if (!path.win32.isAbsolute(filePath))
216+
{
217+
filePath = path.win32.resolve(
218+
vscode.workspace.rootPath,
219+
filePath);
220+
}
221+
222+
var promise =
223+
vscode.workspace.openTextDocument(filePath)
224+
.then(doc => vscode.window.showTextDocument(doc))
225+
.then(_ => EditorOperationResponse.Completed);
226+
227+
return promise;
228+
}
229+
230+
export function registerExtensionCommands(client: LanguageClient): void {
231+
232+
vscode.commands.registerCommand('PowerShell.ShowAdditionalCommands', () => {
233+
var editor = vscode.window.activeTextEditor;
234+
var start = editor.selection.start;
235+
var end = editor.selection.end;
236+
if (editor.selection.isEmpty) {
237+
start = new vscode.Position(start.line, 0)
238+
}
239+
240+
showExtensionCommands(client);
241+
});
242+
243+
client.onNotification(
244+
ExtensionCommandAddedNotification.type,
245+
command => addExtensionCommand(command));
246+
247+
client.onRequest(
248+
GetEditorContextRequest.type,
249+
details => getEditorContext());
250+
251+
client.onRequest(
252+
InsertTextRequest.type,
253+
details => insertText(details));
254+
255+
client.onRequest(
256+
SetSelectionRequest.type,
257+
details => setSelection(details));
258+
259+
client.onRequest(
260+
OpenFileRequest.type,
261+
filePath => openFile(filePath));
262+
}

src/main.ts

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { registerShowHelpCommand } from './features/ShowOnlineHelp';
1515
import { registerOpenInISECommand } from './features/OpenInISE';
1616
import { registerPowerShellFindModuleCommand } from './features/PowerShellFindModule';
1717
import { registerConsoleCommands } from './features/Console';
18+
import { registerExtensionCommands } from './features/ExtensionCommands';
1819

1920
var languageServerClient: LanguageClient = undefined;
2021

@@ -133,6 +134,7 @@ function registerFeatures() {
133134
registerConsoleCommands(languageServerClient);
134135
registerOpenInISECommand();
135136
registerPowerShellFindModuleCommand(languageServerClient);
137+
registerExtensionCommands(languageServerClient);
136138
}
137139

138140
export function deactivate(): void {

0 commit comments

Comments
 (0)