Skip to content

Introduce new editor extensibility API #214

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 3 commits into from
May 10, 2016
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
1 change: 1 addition & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ install:
- git submodule -q update --init

before_build:
- 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")
- nuget restore

build:
Expand Down
112 changes: 112 additions & 0 deletions docs/extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# PowerShell Editor Services Extensibility Model

PowerShell Editor Services exposes a common extensibility model which allows
a user to write extension code in PowerShell that works across any editor that
uses PowerShell Editor Services.

## Using Extensions

**TODO**

- Enable-EditorExtension -Name "SomeExtension.CustomAnalyzer"
- Disable-EditorExtension -Name "SomeExtension.CustomAnalyzer"

## Writing Extensions

Here are some examples of writing editor extensions:

### Command Extensions

#### Executing a cmdlet or function

```powershell
function MyExtensionFunction {
Write-Output "My extension function was invoked!"
}

Register-EditorExtension `
-Command
-Name "MyExt.MyExtensionFunction" `
-DisplayName "My extension function" `
-Function MyExtensionFunction
```

#### Executing a script block

```powershell
Register-EditorExtension `
-Command
-Name "MyExt.MyExtensionScriptBlock" `
-DisplayName "My extension script block" `
-ScriptBlock { Write-Output "My extension script block was invoked!" }
```

#### Additional Parameters

##### ExecuteInSession [switch]

Causes the command to be executed in the user's current session. By default,
commands are executed in a global session that isn't affected by script
execution. Adding this parameter will cause the command to be executed in the
context of the user's session.

### Analyzer Extensions

```powershell
function Invoke-MyAnalyzer {
param(
$FilePath,
$Ast,
$StartLine,
$StartColumn,
$EndLine,
$EndColumn
)
}

Register-EditorExtension `
-Analyzer
-Name "MyExt.MyAnalyzer" `
-DisplayName "My analyzer extension" `
-Function Invoke-MyAnalyzer
```

#### Additional Parameters

##### DelayInterval [int]

Specifies the interval after which this analyzer will be run when the
user finishes typing in the script editor.

### Formatter Extensions

```powershell
function Invoke-MyFormatter {
param(
$FilePath,
$ScriptText,
$StartLine,
$StartColumn,
$EndLine,
$EndColumn
)
}

Register-EditorExtension `
-Formatter
-Name "MyExt.MyFormatter" `
-DisplayName "My formatter extension" `
-Function Invoke-MyFormatter
```

#### Additional Parameters

##### SupportsSelections [switch]

Indicates that this formatter extension can format selections in a larger
file rather than formatting the entire file. If this parameter is not
specified then the entire file will be sent to the extension for every
call.

## Examples

111 changes: 111 additions & 0 deletions src/PowerShellEditorServices.Protocol/LanguageServer/EditorCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;

namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer
{
public class ExtensionCommandAddedNotification
{
public static readonly
EventType<ExtensionCommandAddedNotification> Type =
EventType<ExtensionCommandAddedNotification>.Create("powerShell/extensionCommandAdded");

public string Name { get; set; }

public string DisplayName { get; set; }
}

public class ExtensionCommandUpdatedNotification
{
public static readonly
EventType<ExtensionCommandUpdatedNotification> Type =
EventType<ExtensionCommandUpdatedNotification>.Create("powerShell/extensionCommandUpdated");

public string Name { get; set; }
}

public class ExtensionCommandRemovedNotification
{
public static readonly
EventType<ExtensionCommandRemovedNotification> Type =
EventType<ExtensionCommandRemovedNotification>.Create("powerShell/extensionCommandRemoved");

public string Name { get; set; }
}

public class ClientEditorContext
{
public string CurrentFilePath { get; set; }

public Position CursorPosition { get; set; }

public Range SelectionRange { get; set; }

}

public class InvokeExtensionCommandRequest
{
public static readonly
RequestType<InvokeExtensionCommandRequest, string> Type =
RequestType<InvokeExtensionCommandRequest, string>.Create("powerShell/invokeExtensionCommand");

public string Name { get; set; }

public ClientEditorContext Context { get; set; }
}

public class GetEditorContextRequest
{
public static readonly
RequestType<GetEditorContextRequest, ClientEditorContext> Type =
RequestType<GetEditorContextRequest, ClientEditorContext>.Create("editor/getEditorContext");
}

public enum EditorCommandResponse
{
Unsupported,
OK
}

public class InsertTextRequest
{
public static readonly
RequestType<InsertTextRequest, EditorCommandResponse> Type =
RequestType<InsertTextRequest, EditorCommandResponse>.Create("editor/insertText");

public string FilePath { get; set; }

public string InsertText { get; set; }

public Range InsertRange { get; set; }
}

public class SetSelectionRequest
{
public static readonly
RequestType<SetSelectionRequest, EditorCommandResponse> Type =
RequestType<SetSelectionRequest, EditorCommandResponse>.Create("editor/setSelection");

public Range SelectionRange { get; set; }
}

public class SetCursorPositionRequest
{
public static readonly
RequestType<SetCursorPositionRequest, EditorCommandResponse> Type =
RequestType<SetCursorPositionRequest, EditorCommandResponse>.Create("editor/setCursorPosition");

public Position CursorPosition { get; set; }
}

public class OpenFileRequest
{
public static readonly
RequestType<string, EditorCommandResponse> Type =
RequestType<string, EditorCommandResponse>.Create("editor/openFile");
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<Compile Include="DebugAdapter\ConfigurationDoneRequest.cs" />
<Compile Include="DebugAdapter\ContinueRequest.cs" />
<Compile Include="DebugAdapter\SetFunctionBreakpointsRequest.cs" />
<Compile Include="LanguageServer\EditorCommands.cs" />
<Compile Include="LanguageServer\FindModuleRequest.cs" />
<Compile Include="LanguageServer\InstallModuleRequest.cs" />
<Compile Include="MessageProtocol\IMessageSender.cs" />
Expand Down Expand Up @@ -125,6 +126,7 @@
<Compile Include="MessageProtocol\Channel\StdioServerChannel.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="LanguageServer\References.cs" />
<Compile Include="Server\LanguageServerEditorOperations.cs" />
<Compile Include="Server\LanguageServerSettings.cs" />
<Compile Include="Server\OutputDebouncer.cs" />
<Compile Include="Server\PromptHandlers.cs" />
Expand Down
76 changes: 74 additions & 2 deletions src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using Microsoft.PowerShell.EditorServices.Extensions;
using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer;
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel;
Expand All @@ -27,6 +28,7 @@ public class LanguageServer : LanguageServerBase
private bool profilesLoaded;
private EditorSession editorSession;
private OutputDebouncer outputDebouncer;
private LanguageServerEditorOperations editorOperations;
private LanguageServerSettings currentSettings = new LanguageServerSettings();

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

// Attach to ExtensionService events
this.editorSession.ExtensionService.CommandAdded += ExtensionService_ExtensionAdded;
this.editorSession.ExtensionService.CommandUpdated += ExtensionService_ExtensionUpdated;
this.editorSession.ExtensionService.CommandRemoved += ExtensionService_ExtensionRemoved;

// Create the IEditorOperations implementation
this.editorOperations =
new LanguageServerEditorOperations(
this.editorSession,
this);

// Always send console prompts through the UI in the language service
// TODO: This will change later once we have a general REPL available
// in VS Code.
Expand All @@ -61,6 +74,11 @@ public LanguageServer(HostDetails hostDetails, ChannelBase serverChannel)

protected override void Initialize()
{
// Initialize the extension service
// TODO: This should be made awaited once Initialize is async!
this.editorSession.ExtensionService.Initialize(
this.editorOperations).Wait();

// Register all supported message types

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

this.SetRequestHandler(InvokeExtensionCommandRequest.Type, this.HandleInvokeExtensionCommandRequest);

this.SetRequestHandler(DebugAdapterMessages.EvaluateRequest.Type, this.HandleEvaluateRequest);
}

Expand Down Expand Up @@ -169,6 +189,26 @@ RequestContext<object> requestContext
await requestContext.SendResult(null);
}

private Task HandleInvokeExtensionCommandRequest(
InvokeExtensionCommandRequest commandDetails,
RequestContext<string> requestContext)
{
EditorContext editorContext =
this.editorOperations.ConvertClientEditorContext(
commandDetails.Context);

Task commandTask =
this.editorSession.ExtensionService.InvokeCommand(
commandDetails.Name,
editorContext);

commandTask.ContinueWith(t =>
{
return requestContext.SendResult(null);
});

return commandTask;
}

private async Task HandleExpandAliasRequest(
string content,
Expand Down Expand Up @@ -320,7 +360,7 @@ protected async Task HandleDidChangeConfigurationNotification(
// If there is a new settings file path, restart the analyzer with the new settigs.
bool settingsPathChanged = false;
string newSettingsPath = this.currentSettings.ScriptAnalysis.SettingsPath;
if (!(oldScriptAnalysisSettingsPath?.Equals(newSettingsPath, StringComparison.OrdinalIgnoreCase) ?? false))
if (!string.Equals(oldScriptAnalysisSettingsPath, newSettingsPath, StringComparison.OrdinalIgnoreCase))
{
this.editorSession.RestartAnalysisService(newSettingsPath);
settingsPathChanged = true;
Expand Down Expand Up @@ -802,12 +842,44 @@ protected Task HandleEvaluateRequest(

#region Event Handlers

async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e)
private async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e)
{
// Queue the output for writing
await this.outputDebouncer.Invoke(e);
}

private async void ExtensionService_ExtensionAdded(object sender, EditorCommand e)
{
await this.SendEvent(
ExtensionCommandAddedNotification.Type,
new ExtensionCommandAddedNotification
{
Name = e.Name,
DisplayName = e.DisplayName
});
}

private async void ExtensionService_ExtensionUpdated(object sender, EditorCommand e)
{
await this.SendEvent(
ExtensionCommandUpdatedNotification.Type,
new ExtensionCommandUpdatedNotification
{
Name = e.Name,
});
}

private async void ExtensionService_ExtensionRemoved(object sender, EditorCommand e)
{
await this.SendEvent(
ExtensionCommandRemovedNotification.Type,
new ExtensionCommandRemovedNotification
{
Name = e.Name,
});
}


#endregion

#region Helper Methods
Expand Down
Loading