Skip to content

Commit b7cf594

Browse files
committed
Introduce new editor extensibility API
This change introduces a new PowerShell-based API that allows scripts and modules to extend the behavior of the host editor. Scripts can either automate the editor directly or register commands that can later be invoked by the user. The ExtensionService provides this behavior for any PowerShellContext by injecting a new variable '$psEditor' into the session. This version of the API is very early and will be expanded upon in future releases.
1 parent dd879af commit b7cf594

25 files changed

+2331
-23
lines changed

docs/extensions.md

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# PowerShell Editor Services Extensibility Model
2+
3+
PowerShell Editor Services exposes a common extensibility model which allows
4+
a user to write extension code in PowerShell that works across any editor that
5+
uses PowerShell Editor Services.
6+
7+
## Using Extensions
8+
9+
**TODO**
10+
11+
- Enable-EditorExtension -Name "SomeExtension.CustomAnalyzer"
12+
- Disable-EditorExtension -Name "SomeExtension.CustomAnalyzer"
13+
14+
## Writing Extensions
15+
16+
Here are some examples of writing editor extensions:
17+
18+
### Command Extensions
19+
20+
#### Executing a cmdlet or function
21+
22+
```powershell
23+
function MyExtensionFunction {
24+
Write-Output "My extension function was invoked!"
25+
}
26+
27+
Register-EditorExtension `
28+
-Command
29+
-Name "MyExt.MyExtensionFunction" `
30+
-DisplayName "My extension function" `
31+
-Function MyExtensionFunction
32+
```
33+
34+
#### Executing a script block
35+
36+
```powershell
37+
Register-EditorExtension `
38+
-Command
39+
-Name "MyExt.MyExtensionScriptBlock" `
40+
-DisplayName "My extension script block" `
41+
-ScriptBlock { Write-Output "My extension script block was invoked!" }
42+
```
43+
44+
#### Additional Parameters
45+
46+
##### ExecuteInSession [switch]
47+
48+
Causes the command to be executed in the user's current session. By default,
49+
commands are executed in a global session that isn't affected by script
50+
execution. Adding this parameter will cause the command to be executed in the
51+
context of the user's session.
52+
53+
### Analyzer Extensions
54+
55+
```powershell
56+
function Invoke-MyAnalyzer {
57+
param(
58+
$FilePath,
59+
$Ast,
60+
$StartLine,
61+
$StartColumn,
62+
$EndLine,
63+
$EndColumn
64+
)
65+
}
66+
67+
Register-EditorExtension `
68+
-Analyzer
69+
-Name "MyExt.MyAnalyzer" `
70+
-DisplayName "My analyzer extension" `
71+
-Function Invoke-MyAnalyzer
72+
```
73+
74+
#### Additional Parameters
75+
76+
##### DelayInterval [int]
77+
78+
Specifies the interval after which this analyzer will be run when the
79+
user finishes typing in the script editor.
80+
81+
### Formatter Extensions
82+
83+
```powershell
84+
function Invoke-MyFormatter {
85+
param(
86+
$FilePath,
87+
$ScriptText,
88+
$StartLine,
89+
$StartColumn,
90+
$EndLine,
91+
$EndColumn
92+
)
93+
}
94+
95+
Register-EditorExtension `
96+
-Formatter
97+
-Name "MyExt.MyFormatter" `
98+
-DisplayName "My formatter extension" `
99+
-Function Invoke-MyFormatter
100+
```
101+
102+
#### Additional Parameters
103+
104+
##### SupportsSelections [switch]
105+
106+
Indicates that this formatter extension can format selections in a larger
107+
file rather than formatting the entire file. If this parameter is not
108+
specified then the entire file will be sent to the extension for every
109+
call.
110+
111+
## Examples
112+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
7+
8+
namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer
9+
{
10+
public class ExtensionCommandAddedNotification
11+
{
12+
public static readonly
13+
EventType<ExtensionCommandAddedNotification> Type =
14+
EventType<ExtensionCommandAddedNotification>.Create("powerShell/extensionCommandAdded");
15+
16+
public string Name { get; set; }
17+
18+
public string DisplayName { get; set; }
19+
}
20+
21+
public class ExtensionCommandUpdatedNotification
22+
{
23+
public static readonly
24+
EventType<ExtensionCommandUpdatedNotification> Type =
25+
EventType<ExtensionCommandUpdatedNotification>.Create("powerShell/extensionCommandUpdated");
26+
27+
public string Name { get; set; }
28+
}
29+
30+
public class ExtensionCommandRemovedNotification
31+
{
32+
public static readonly
33+
EventType<ExtensionCommandRemovedNotification> Type =
34+
EventType<ExtensionCommandRemovedNotification>.Create("powerShell/extensionCommandRemoved");
35+
36+
public string Name { get; set; }
37+
}
38+
39+
public class ClientEditorContext
40+
{
41+
public string CurrentFilePath { get; set; }
42+
43+
public Position CursorPosition { get; set; }
44+
45+
public Range SelectionRange { get; set; }
46+
47+
}
48+
49+
public class InvokeExtensionCommandRequest
50+
{
51+
public static readonly
52+
RequestType<InvokeExtensionCommandRequest, string> Type =
53+
RequestType<InvokeExtensionCommandRequest, string>.Create("powerShell/invokeExtensionCommand");
54+
55+
public string Name { get; set; }
56+
57+
public ClientEditorContext Context { get; set; }
58+
}
59+
60+
public class GetEditorContextRequest
61+
{
62+
public static readonly
63+
RequestType<GetEditorContextRequest, ClientEditorContext> Type =
64+
RequestType<GetEditorContextRequest, ClientEditorContext>.Create("editor/getEditorContext");
65+
}
66+
67+
public enum EditorCommandResponse
68+
{
69+
Unsupported,
70+
OK
71+
}
72+
73+
public class InsertTextRequest
74+
{
75+
public static readonly
76+
RequestType<InsertTextRequest, EditorCommandResponse> Type =
77+
RequestType<InsertTextRequest, EditorCommandResponse>.Create("editor/insertText");
78+
79+
public string FilePath { get; set; }
80+
81+
public string InsertText { get; set; }
82+
83+
public Range InsertRange { get; set; }
84+
}
85+
86+
public class SetSelectionRequest
87+
{
88+
public static readonly
89+
RequestType<SetSelectionRequest, EditorCommandResponse> Type =
90+
RequestType<SetSelectionRequest, EditorCommandResponse>.Create("editor/setSelection");
91+
92+
public Range SelectionRange { get; set; }
93+
}
94+
95+
public class SetCursorPositionRequest
96+
{
97+
public static readonly
98+
RequestType<SetCursorPositionRequest, EditorCommandResponse> Type =
99+
RequestType<SetCursorPositionRequest, EditorCommandResponse>.Create("editor/setCursorPosition");
100+
101+
public Position CursorPosition { get; set; }
102+
}
103+
104+
public class OpenFileRequest
105+
{
106+
public static readonly
107+
RequestType<string, EditorCommandResponse> Type =
108+
RequestType<string, EditorCommandResponse>.Create("editor/openFile");
109+
}
110+
}
111+

src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<Compile Include="DebugAdapter\ConfigurationDoneRequest.cs" />
5454
<Compile Include="DebugAdapter\ContinueRequest.cs" />
5555
<Compile Include="DebugAdapter\SetFunctionBreakpointsRequest.cs" />
56+
<Compile Include="LanguageServer\EditorCommands.cs" />
5657
<Compile Include="LanguageServer\FindModuleRequest.cs" />
5758
<Compile Include="LanguageServer\InstallModuleRequest.cs" />
5859
<Compile Include="MessageProtocol\IMessageSender.cs" />
@@ -125,6 +126,7 @@
125126
<Compile Include="MessageProtocol\Channel\StdioServerChannel.cs" />
126127
<Compile Include="Properties\AssemblyInfo.cs" />
127128
<Compile Include="LanguageServer\References.cs" />
129+
<Compile Include="Server\LanguageServerEditorOperations.cs" />
128130
<Compile Include="Server\LanguageServerSettings.cs" />
129131
<Compile Include="Server\OutputDebouncer.cs" />
130132
<Compile Include="Server\PromptHandlers.cs" />

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

+73-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
//
55

6+
using Microsoft.PowerShell.EditorServices.Extensions;
67
using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer;
78
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
89
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel;
@@ -27,6 +28,7 @@ public class LanguageServer : LanguageServerBase
2728
private bool profilesLoaded;
2829
private EditorSession editorSession;
2930
private OutputDebouncer outputDebouncer;
31+
private LanguageServerEditorOperations editorOperations;
3032
private LanguageServerSettings currentSettings = new LanguageServerSettings();
3133

3234
/// <param name="hostDetails">
@@ -47,6 +49,17 @@ public LanguageServer(HostDetails hostDetails, ChannelBase serverChannel)
4749
this.editorSession.StartSession(hostDetails);
4850
this.editorSession.ConsoleService.OutputWritten += this.powerShellContext_OutputWritten;
4951

52+
// Attach to ExtensionService events
53+
this.editorSession.ExtensionService.CommandAdded += ExtensionService_ExtensionAdded;
54+
this.editorSession.ExtensionService.CommandUpdated += ExtensionService_ExtensionUpdated;
55+
this.editorSession.ExtensionService.CommandRemoved += ExtensionService_ExtensionRemoved;
56+
57+
// Create the IEditorOperations implementation
58+
this.editorOperations =
59+
new LanguageServerEditorOperations(
60+
this.editorSession,
61+
this);
62+
5063
// Always send console prompts through the UI in the language service
5164
// TODO: This will change later once we have a general REPL available
5265
// in VS Code.
@@ -61,6 +74,11 @@ public LanguageServer(HostDetails hostDetails, ChannelBase serverChannel)
6174

6275
protected override void Initialize()
6376
{
77+
// Initialize the extension service
78+
// TODO: This should be made awaited once Initialize is async!
79+
this.editorSession.ExtensionService.Initialize(
80+
this.editorOperations).Wait();
81+
6482
// Register all supported message types
6583

6684
this.SetRequestHandler(InitializeRequest.Type, this.HandleInitializeRequest);
@@ -86,6 +104,8 @@ protected override void Initialize()
86104
this.SetRequestHandler(FindModuleRequest.Type, this.HandleFindModuleRequest);
87105
this.SetRequestHandler(InstallModuleRequest.Type, this.HandleInstallModuleRequest);
88106

107+
this.SetRequestHandler(InvokeExtensionCommandRequest.Type, this.HandleInvokeExtensionCommandRequest);
108+
89109
this.SetRequestHandler(DebugAdapterMessages.EvaluateRequest.Type, this.HandleEvaluateRequest);
90110
}
91111

@@ -169,6 +189,26 @@ RequestContext<object> requestContext
169189
await requestContext.SendResult(null);
170190
}
171191

192+
private Task HandleInvokeExtensionCommandRequest(
193+
InvokeExtensionCommandRequest commandDetails,
194+
RequestContext<string> requestContext)
195+
{
196+
EditorContext editorContext =
197+
this.editorOperations.ConvertClientEditorContext(
198+
commandDetails.Context);
199+
200+
Task commandTask =
201+
this.editorSession.ExtensionService.InvokeCommand(
202+
commandDetails.Name,
203+
editorContext);
204+
205+
commandTask.ContinueWith(t =>
206+
{
207+
return requestContext.SendResult(null);
208+
});
209+
210+
return commandTask;
211+
}
172212

173213
private async Task HandleExpandAliasRequest(
174214
string content,
@@ -802,12 +842,44 @@ protected Task HandleEvaluateRequest(
802842

803843
#region Event Handlers
804844

805-
async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e)
845+
private async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e)
806846
{
807847
// Queue the output for writing
808848
await this.outputDebouncer.Invoke(e);
809849
}
810850

851+
private async void ExtensionService_ExtensionAdded(object sender, EditorCommand e)
852+
{
853+
await this.SendEvent(
854+
ExtensionCommandAddedNotification.Type,
855+
new ExtensionCommandAddedNotification
856+
{
857+
Name = e.Name,
858+
DisplayName = e.DisplayName
859+
});
860+
}
861+
862+
private async void ExtensionService_ExtensionUpdated(object sender, EditorCommand e)
863+
{
864+
await this.SendEvent(
865+
ExtensionCommandUpdatedNotification.Type,
866+
new ExtensionCommandUpdatedNotification
867+
{
868+
Name = e.Name,
869+
});
870+
}
871+
872+
private async void ExtensionService_ExtensionRemoved(object sender, EditorCommand e)
873+
{
874+
await this.SendEvent(
875+
ExtensionCommandRemovedNotification.Type,
876+
new ExtensionCommandRemovedNotification
877+
{
878+
Name = e.Name,
879+
});
880+
}
881+
882+
811883
#endregion
812884

813885
#region Helper Methods

0 commit comments

Comments
 (0)