Skip to content

Commit 3313968

Browse files
committed
Merge pull request #214 from PowerShell/daviwil/extensions
Introduce new editor extensibility API
2 parents dd879af + 4816885 commit 3313968

28 files changed

+2372
-43
lines changed

Diff for: appveyor.yml

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ install:
3030
- git submodule -q update --init
3131

3232
before_build:
33+
- ps: if (Test-Path 'C:\Tools\NuGet3') { $nugetDir = 'C:\Tools\NuGet3' } else { $nugetDir = 'C:\Tools\NuGet' }; (New-Object Net.WebClient).DownloadFile('https://dist.nuget.org/win-x86-commandline/v3.3.0/nuget.exe', "$nugetDir\NuGet.exe")
3334
- nuget restore
3435

3536
build:

Diff for: 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+

Diff for: 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" />

Diff for: src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

+74-2
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,
@@ -320,7 +360,7 @@ protected async Task HandleDidChangeConfigurationNotification(
320360
// If there is a new settings file path, restart the analyzer with the new settigs.
321361
bool settingsPathChanged = false;
322362
string newSettingsPath = this.currentSettings.ScriptAnalysis.SettingsPath;
323-
if (!(oldScriptAnalysisSettingsPath?.Equals(newSettingsPath, StringComparison.OrdinalIgnoreCase) ?? false))
363+
if (!string.Equals(oldScriptAnalysisSettingsPath, newSettingsPath, StringComparison.OrdinalIgnoreCase))
324364
{
325365
this.editorSession.RestartAnalysisService(newSettingsPath);
326366
settingsPathChanged = true;
@@ -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)