21 | {{^_disableToc}} 22 | {{>partials/toc}} 23 |
24 | {{/_disableToc}} 25 | {{#_disableToc}} 26 |
27 | {{/_disableToc}} 28 | {{#_disableAffix}} 29 |
30 | {{/_disableAffix}} 31 | {{^_disableAffix}} 32 |
33 | {{/_disableAffix}} 34 |
35 | {{^_disableContribution}} 36 | {{#docurl}} 37 | Improve this Doc 38 | {{/docurl}} 39 | {{/_disableContribution}} 40 | {{{rawTitle}}} 41 | {{{conceptual}}} 42 |
43 |
44 | {{^_disableAffix}} 45 | {{>partials/affix}} 46 | {{/_disableAffix}} 47 |
48 |
49 | {{^_disableFooter}} 50 | {{>partials/footer}} 51 | {{/_disableFooter}} 52 |
53 | {{>partials/scripts}} 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Extensions/EditorRequests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using OmniSharp.Extensions.LanguageServer.Protocol.Models; 5 | 6 | namespace Microsoft.PowerShell.EditorServices.Extensions 7 | { 8 | internal class ExtensionCommandAddedNotification 9 | { 10 | public string Name { get; set; } 11 | 12 | public string DisplayName { get; set; } 13 | } 14 | 15 | internal class ExtensionCommandUpdatedNotification 16 | { 17 | public string Name { get; set; } 18 | } 19 | 20 | internal class ExtensionCommandRemovedNotification 21 | { 22 | public string Name { get; set; } 23 | } 24 | 25 | internal class GetEditorContextRequest 26 | { } 27 | 28 | internal enum EditorOperationResponse 29 | { 30 | Completed, 31 | Failed 32 | } 33 | 34 | internal class InsertTextRequest 35 | { 36 | public string FilePath { get; set; } 37 | 38 | public string InsertText { get; set; } 39 | 40 | public Range InsertRange { get; set; } 41 | } 42 | 43 | internal class SetSelectionRequest 44 | { 45 | public Range SelectionRange { get; set; } 46 | } 47 | 48 | internal class SetCursorPositionRequest 49 | { 50 | public Position CursorPosition { get; set; } 51 | } 52 | 53 | internal class OpenFileDetails 54 | { 55 | public string FilePath { get; set; } 56 | 57 | public bool Preview { get; set; } 58 | } 59 | 60 | internal class SaveFileDetails 61 | { 62 | public string FilePath { get; set; } 63 | 64 | public string NewPath { get; set; } 65 | } 66 | 67 | internal class StatusBarMessageDetails 68 | { 69 | public string Message { get; set; } 70 | 71 | public int? Timeout { get; set; } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /docs/guide/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | > NOTE: The user guide is currently under development and may be missing 4 | > important information. If you feel that a particular area is missing or 5 | > poorly explained, please feel free to file an issue at our [GitHub site](https://github.com/PowerShell/PowerShellEditorServices/issues) 6 | 7 | PowerShell Editor Services is a tool that provides useful services to code 8 | editors that need a great PowerShell editing experience. 9 | 10 | ## The .NET API 11 | 12 | The .NET API provides the complete set of services which can be used in 13 | code editors or any other type of application. The easiest way to get 14 | started with it is to add the [Microsoft.PowerShell.EditorServices](https://www.nuget.org/packages/Microsoft.PowerShell.EditorServices/) 15 | NuGet package to your C# project. 16 | 17 | If you're a developer that would like to use PowerShell Editor Services in 18 | a .NET application, read the page titled [Using the .NET API](using_the_dotnet_api.md) 19 | to learn more. 20 | 21 | ## The Host Process 22 | 23 | The host process provides a JSON-based API wrapper around the .NET APIs so 24 | that editors written in non-.NET languages can make use of its capabilities. 25 | In the future the host process will allow the use of network-based channels 26 | to enable all of the APIs to be accessed remotely. 27 | 28 | If you're a developer that would like to integrate PowerShell Editor Services 29 | into your favorite editor, read the page titled [Using the Host Process](using_the_host_process.md) 30 | to learn more. 31 | 32 | ## Writing Extensions in PowerShell 33 | 34 | If you're using an editor that leverages PowerShell Editor Services to provide 35 | PowerShell editing capabilities, you may be able to extend its behavior using 36 | our PowerShell-based editor extension API. Read the page titled [Extending the 37 | Host Editor](extensions.md) to learn more. -------------------------------------------------------------------------------- /module/docs/Get-Token.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PowerShellEditorServices.Commands-help.xml 3 | online version: https://github.com/PowerShell/PowerShellEditorServices/tree/main/module/docs/Get-Token.md 4 | schema: 2.0.0 5 | --- 6 | 7 | # Get-Token 8 | 9 | ## SYNOPSIS 10 | 11 | Get parser tokens from a script position. 12 | 13 | ## SYNTAX 14 | 15 | ```powershell 16 | Get-Token [[-Extent] ] 17 | ``` 18 | 19 | ## DESCRIPTION 20 | 21 | The Get-Token function can retrieve tokens from the current editor context, or from a ScriptExtent object. You can then use the ScriptExtent functions to manipulate the text at it's location. 22 | 23 | ## EXAMPLES 24 | 25 | ### -------------------------- EXAMPLE 1 -------------------------- 26 | 27 | ```powershell 28 | using namespace System.Management.Automation.Language 29 | Find-Ast { $_ -is [IfStatementAst] } -First | Get-Token 30 | ``` 31 | 32 | Gets all tokens from the first IfStatementAst. 33 | 34 | ### -------------------------- EXAMPLE 2 -------------------------- 35 | 36 | ```powershell 37 | Get-Token | Where-Object { $_.Kind -eq 'Comment' } 38 | ``` 39 | 40 | Gets all comment tokens. 41 | 42 | ## PARAMETERS 43 | 44 | ### -Extent 45 | 46 | Specifies the extent that a token must be within to be returned. 47 | 48 | ```yaml 49 | Type: IScriptExtent 50 | Parameter Sets: (All) 51 | Aliases: 52 | 53 | Required: False 54 | Position: 1 55 | Default value: None 56 | Accept pipeline input: True (ByPropertyName, ByValue) 57 | Accept wildcard characters: False 58 | ``` 59 | 60 | ## INPUTS 61 | 62 | ### System.Management.Automation.Language.IScriptExtent 63 | 64 | You can pass extents to get tokens from to this function. You can also pass objects that with a property named "Extent", like Ast objects from the Find-Ast function. 65 | 66 | ## OUTPUTS 67 | 68 | ### System.Management.Automation.Language.Token 69 | 70 | ## NOTES 71 | 72 | ## RELATED LINKS 73 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Services/PowerShell/Host/NullPSHostRawUI.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Management.Automation.Host; 6 | 7 | namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host 8 | { 9 | internal class NullPSHostRawUI : PSHostRawUserInterface 10 | { 11 | private readonly BufferCell[,] _buffer; 12 | 13 | public NullPSHostRawUI() => _buffer = new BufferCell[0, 0]; 14 | 15 | public override Coordinates WindowPosition { get; set; } 16 | 17 | public override Size MaxWindowSize => new() { Width = _buffer.GetLength(0), Height = _buffer.GetLength(1) }; 18 | 19 | public override Size MaxPhysicalWindowSize => MaxWindowSize; 20 | 21 | public override bool KeyAvailable => false; 22 | 23 | public override ConsoleColor ForegroundColor { get; set; } 24 | 25 | public override int CursorSize { get; set; } 26 | 27 | public override Coordinates CursorPosition { get; set; } 28 | 29 | public override Size BufferSize { get; set; } 30 | 31 | public override ConsoleColor BackgroundColor { get; set; } 32 | 33 | public override Size WindowSize { get; set; } 34 | 35 | public override string WindowTitle { get; set; } 36 | 37 | public override void FlushInputBuffer() { } 38 | 39 | public override BufferCell[,] GetBufferContents(Rectangle rectangle) => _buffer; 40 | 41 | public override KeyInfo ReadKey(ReadKeyOptions options) => default; 42 | 43 | public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill) { } 44 | 45 | public override void SetBufferContents(Coordinates origin, BufferCell[,] contents) { } 46 | 47 | public override void SetBufferContents(Rectangle rectangle, BufferCell fill) { } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/vim-simple-test.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('pses') 2 | let s:assert = themis#helper('assert') 3 | 4 | function s:suite.before() 5 | let l:pses_path = g:repo_root . '/module' 6 | let g:LanguageClient_serverCommands = { 7 | \ 'ps1': [ 'pwsh', '-NoLogo', '-NoProfile', '-Command', 8 | \ l:pses_path . '/PowerShellEditorServices/Start-EditorServices.ps1', '-Stdio' ] 9 | \ } 10 | let g:LanguageClient_serverStderr = 'DEBUG' 11 | let g:LanguageClient_loggingFile = g:repo_root . '/LanguageClient.log' 12 | let g:LanguageClient_serverStderr = g:repo_root . '/LanguageServer.log' 13 | endfunction 14 | 15 | function s:suite.has_language_client() 16 | call s:assert.includes(&runtimepath, g:repo_root . '/LanguageClient-neovim') 17 | call s:assert.cmd_exists('LanguageClientStart') 18 | call s:assert.not_empty(g:LanguageClient_serverCommands) 19 | call s:assert.true(LanguageClient#HasCommand('ps1')) 20 | endfunction 21 | 22 | function s:suite.analyzes_powershell_file() 23 | view test/vim-test.ps1 " This must not use quotes! 24 | 25 | let l:bufnr = bufnr('vim-test.ps1$') 26 | call s:assert.not_equal(l:bufnr, -1) 27 | let l:bufinfo = getbufinfo(l:bufnr)[0] 28 | 29 | call s:assert.equal(l:bufinfo.name, g:repo_root . '/test/vim-test.ps1') 30 | call s:assert.includes(getbufline(l:bufinfo.name, 1), 'function Do-Work {}') 31 | " TODO: This shouldn't be necessary, vim-ps1 works locally but not in CI. 32 | call setbufvar(l:bufinfo.bufnr, '&filetype', 'ps1') 33 | call s:assert.equal(getbufvar(l:bufinfo.bufnr, '&filetype'), 'ps1') 34 | 35 | execute 'LanguageClientStart' 36 | execute 'sleep' 5 37 | call s:assert.equal(getbufvar(l:bufinfo.name, 'LanguageClient_isServerRunning'), 1) 38 | call s:assert.equal(getbufvar(l:bufinfo.name, 'LanguageClient_projectRoot'), g:repo_root) 39 | call s:assert.equal(getbufvar(l:bufinfo.name, 'LanguageClient_statusLineDiagnosticsCounts'), {'E': 0, 'W': 1, 'H': 0, 'I': 0}) 40 | endfunction 41 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Utility/Extensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Text; 6 | 7 | namespace Microsoft.PowerShell.EditorServices.Utility 8 | { 9 | internal static class ObjectExtensions 10 | { 11 | /// 12 | /// Extension to evaluate an object's ToString() method in an exception safe way. This will 13 | /// extension method will not throw. 14 | /// 15 | /// The object on which to call ToString() 16 | /// The ToString() return value or a suitable error message is that throws. 17 | public static string SafeToString(this object obj) 18 | { 19 | string str; 20 | 21 | try 22 | { 23 | str = obj.ToString(); 24 | } 25 | catch (Exception ex) 26 | { 27 | str = $""; 28 | } 29 | 30 | return str; 31 | } 32 | 33 | /// 34 | /// Same as but never CRLF. Use this when building 35 | /// formatting for clients that may not render CRLF correctly. 36 | /// 37 | /// 38 | public static StringBuilder AppendLineLF(this StringBuilder self) => self.Append('\n'); 39 | 40 | /// 41 | /// Same as but never CRLF. Use this when building 42 | /// formatting for clients that may not render CRLF correctly. 43 | /// 44 | /// 45 | /// 46 | public static StringBuilder AppendLineLF(this StringBuilder self, string value) 47 | => self.Append(value).Append('\n'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Services/PowerShell/Handlers/EvaluateHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Management.Automation; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.PowerShell.EditorServices.Services.PowerShell; 8 | using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; 9 | 10 | namespace Microsoft.PowerShell.EditorServices.Handlers 11 | { 12 | /// 13 | /// Handler for a custom request type for evaluating PowerShell. 14 | /// This is generally for F8 support, to allow execution of a highlighted code snippet in the console as if it were copy-pasted. 15 | /// 16 | internal class EvaluateHandler : IEvaluateHandler 17 | { 18 | private readonly IInternalPowerShellExecutionService _executionService; 19 | 20 | public EvaluateHandler(IInternalPowerShellExecutionService executionService) => _executionService = executionService; 21 | 22 | public async Task Handle(EvaluateRequestArguments request, CancellationToken cancellationToken) 23 | { 24 | // This API is mostly used for F8 execution so it requires the foreground. 25 | await _executionService.ExecutePSCommandAsync( 26 | new PSCommand().AddScript(request.Expression), 27 | CancellationToken.None, 28 | new PowerShellExecutionOptions 29 | { 30 | RequiresForeground = true, 31 | WriteInputToHost = true, 32 | WriteOutputToHost = true, 33 | AddToHistory = true, 34 | ThrowOnError = false, 35 | }).ConfigureAwait(false); 36 | 37 | // TODO: Should we return a more informative result? 38 | return new EvaluateResponseBody 39 | { 40 | Result = "", 41 | VariablesReference = 0 42 | }; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Services/CodeLens/ICodeLensProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.PowerShell.EditorServices.Services.TextDocument; 8 | using OmniSharp.Extensions.LanguageServer.Protocol.Models; 9 | 10 | namespace Microsoft.PowerShell.EditorServices.CodeLenses 11 | { 12 | /// 13 | /// Specifies the contract for a Code Lens provider. 14 | /// 15 | internal interface ICodeLensProvider 16 | { 17 | /// 18 | /// Specifies a unique identifier for the feature provider, typically a 19 | /// fully-qualified name like "Microsoft.PowerShell.EditorServices.MyProvider" 20 | /// 21 | string ProviderId { get; } 22 | 23 | /// 24 | /// Provides a collection of CodeLenses for the given 25 | /// document. 26 | /// 27 | /// 28 | /// The document for which CodeLenses should be provided. 29 | /// 30 | /// An IEnumerable of CodeLenses. 31 | IEnumerable ProvideCodeLenses(ScriptFile scriptFile); 32 | 33 | /// 34 | /// Resolves a CodeLens that was created without a Command. 35 | /// 36 | /// 37 | /// The CodeLens to resolve. 38 | /// 39 | /// 40 | /// The ScriptFile to resolve it in (sometimes unused). 41 | /// 42 | /// 43 | /// 44 | /// A Task which returns the resolved CodeLens when completed. 45 | /// 46 | Task ResolveCodeLens( 47 | CodeLens codeLens, 48 | ScriptFile scriptFile, 49 | CancellationToken cancellationToken); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | net8.0;net462 6 | Microsoft.PowerShell.EditorServices.Test 7 | x64 8 | 9 | 10 | 11 | $(DefineConstants);CoreCLR 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | PreserveNewest 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /.github/workflows/close-stale-issues.yml: -------------------------------------------------------------------------------- 1 | name: Issue Housekeeping 2 | 3 | permissions: 4 | issues: write 5 | 6 | on: 7 | schedule: 8 | - cron: "0 * * * *" 9 | 10 | jobs: 11 | stale-resolved-issues: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/stale@v9 15 | name: Label resolved issues as needing fix verification 16 | with: 17 | any-of-labels: "Resolution-Answered,Resolution-Duplicate,Resolution-External,Resolution-Fixed,Resolution-Inactive" 18 | stale-issue-label: "Needs: Fix Verification" 19 | days-before-stale: 0 20 | days-before-close: -1 21 | stale-issue-message: "This issue has been labeled as resolved, please verify the provided fix (or other reason)." 22 | labels-to-remove-when-stale: "Needs: Maintainer Attention,Needs: Triage" 23 | stale-fixed-issues: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/stale@v9 27 | name: Close issues needing fix verification after 1 week of inactivity 28 | with: 29 | stale-issue-label: "Needs: Fix Verification" 30 | days-before-stale: -1 31 | labels-to-add-when-unstale: "Needs: Maintainer Attention" 32 | close-issue-reason: completed 33 | close-issue-message: "This issue has been labeled as needing fix verification and has not had any activity a week. It has been closed for housekeeping purposes." 34 | stale-feedback-issues: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/stale@v9 38 | name: Close issues needing author feedback after 1 week of inactivity 39 | with: 40 | stale-issue-label: "Needs: Author Feedback" 41 | days-before-stale: -1 42 | labels-to-add-when-unstale: "Needs: Maintainer Attention" 43 | labels-to-remove-when-unstale: "Needs: Triage" 44 | close-issue-reason: completed 45 | close-issue-message: "This issue has been labeled as needing feedback and has not had any activity a week. It has been closed for housekeeping purposes." 46 | -------------------------------------------------------------------------------- /.github/workflows/ci-test.yml: -------------------------------------------------------------------------------- 1 | name: CI Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | ci: 11 | name: dotnet 12 | strategy: 13 | matrix: 14 | os: [ windows-latest, macos-latest, ubuntu-latest ] 15 | runs-on: ${{ matrix.os }} 16 | env: 17 | DOTNET_NOLOGO: true 18 | DOTNET_GENERATE_ASPNET_CERTIFICATE: false 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Install dotnet 24 | uses: actions/setup-dotnet@v4 25 | with: 26 | cache: true 27 | cache-dependency-path: '**/*.csproj' 28 | global-json-file: ./global.json 29 | 30 | - name: Install PSResources 31 | shell: pwsh 32 | run: ./tools/installPSResources.ps1 33 | 34 | - name: Download PowerShell install script 35 | uses: actions/checkout@v4 36 | with: 37 | repository: PowerShell/PowerShell 38 | path: pwsh 39 | sparse-checkout: tools/install-powershell.ps1 40 | sparse-checkout-cone-mode: false 41 | 42 | - name: Install preview 43 | shell: pwsh 44 | run: ./pwsh/tools/install-powershell.ps1 -Preview -Destination ./preview 45 | 46 | - name: If debugging, start upterm for interactive pipeline troubleshooting 47 | if: ${{ runner.debug == 1 }} 48 | uses: lhotari/action-upterm@v1 49 | with: 50 | wait-timeout-minutes: 1 51 | 52 | - name: Build and test 53 | shell: pwsh 54 | run: Invoke-Build -Configuration Release TestFull 55 | 56 | - name: Upload build artifacts 57 | if: always() 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: PowerShellEditorServices-module-${{ matrix.os }} 61 | path: module 62 | 63 | - name: Upload test results 64 | uses: actions/upload-artifact@v4 65 | if: always() 66 | with: 67 | name: PowerShellEditorServices-test-results-${{ matrix.os }} 68 | path: '**/*.trx' 69 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHost.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Globalization; 6 | using System.Management.Automation.Host; 7 | 8 | namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host 9 | { 10 | public class EditorServicesConsolePSHost : PSHost, IHostSupportsInteractiveSession 11 | { 12 | private readonly PsesInternalHost _internalHost; 13 | 14 | internal EditorServicesConsolePSHost( 15 | PsesInternalHost internalHost) => _internalHost = internalHost; 16 | 17 | public override CultureInfo CurrentCulture => _internalHost.CurrentCulture; 18 | 19 | public override CultureInfo CurrentUICulture => _internalHost.CurrentUICulture; 20 | 21 | public override Guid InstanceId => _internalHost.InstanceId; 22 | 23 | public override string Name => _internalHost.Name; 24 | 25 | public override System.Management.Automation.PSObject PrivateData => _internalHost.PrivateData; 26 | 27 | public override PSHostUserInterface UI => _internalHost.UI; 28 | 29 | public override Version Version => _internalHost.Version; 30 | 31 | public bool IsRunspacePushed => _internalHost.IsRunspacePushed; 32 | 33 | public System.Management.Automation.Runspaces.Runspace Runspace => _internalHost.Runspace; 34 | 35 | public override void EnterNestedPrompt() => _internalHost.EnterNestedPrompt(); 36 | 37 | public override void ExitNestedPrompt() => _internalHost.ExitNestedPrompt(); 38 | 39 | public override void NotifyBeginApplication() => _internalHost.NotifyBeginApplication(); 40 | 41 | public override void NotifyEndApplication() => _internalHost.NotifyEndApplication(); 42 | 43 | public void PopRunspace() => _internalHost.PopRunspace(); 44 | 45 | public void PushRunspace(System.Management.Automation.Runspaces.Runspace runspace) => _internalHost.PushRunspace(runspace); 46 | 47 | public override void SetShouldExit(int exitCode) => _internalHost.SetShouldExit(exitCode); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/vim-test.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('pses') 2 | let s:assert = themis#helper('assert') 3 | 4 | function s:suite.before() 5 | let l:pses_path = g:repo_root . '/module' 6 | let g:LanguageClient_serverCommands = { 7 | \ 'ps1': [ 'pwsh', '-NoLogo', '-NoProfile', '-Command', 8 | \ l:pses_path . '/PowerShellEditorServices/Start-EditorServices.ps1', 9 | \ '-HostName', 'vim', '-HostProfileId', 'vim', '-HostVersion', '1.0.0', 10 | \ '-BundledModulesPath', l:pses_path, '-Stdio', 11 | \ '-LogPath', g:repo_root . '/pses.log', '-LogLevel', 'Diagnostic', 12 | \ '-SessionDetailsPath', g:repo_root . '/pses_session.json' ] 13 | \ } 14 | let g:LanguageClient_serverStderr = 'DEBUG' 15 | let g:LanguageClient_loggingFile = g:repo_root . '/LanguageClient.log' 16 | let g:LanguageClient_serverStderr = g:repo_root . '/LanguageServer.log' 17 | endfunction 18 | 19 | function s:suite.has_language_client() 20 | call s:assert.includes(&runtimepath, g:repo_root . '/LanguageClient-neovim') 21 | call s:assert.cmd_exists('LanguageClientStart') 22 | call s:assert.not_empty(g:LanguageClient_serverCommands) 23 | call s:assert.true(LanguageClient#HasCommand('ps1')) 24 | endfunction 25 | 26 | function s:suite.analyzes_powershell_file() 27 | view test/vim-test.ps1 " This must not use quotes! 28 | 29 | let l:bufnr = bufnr('vim-test.ps1$') 30 | call s:assert.not_equal(l:bufnr, -1) 31 | let l:bufinfo = getbufinfo(l:bufnr)[0] 32 | 33 | call s:assert.equal(l:bufinfo.name, g:repo_root . '/test/vim-test.ps1') 34 | call s:assert.includes(getbufline(l:bufinfo.name, 1), 'function Do-Work {}') 35 | " TODO: This shouldn't be necessary, vim-ps1 works locally but not in CI. 36 | call setbufvar(l:bufinfo.bufnr, '&filetype', 'ps1') 37 | call s:assert.equal(getbufvar(l:bufinfo.bufnr, '&filetype'), 'ps1') 38 | 39 | execute 'LanguageClientStart' 40 | execute 'sleep' 5 41 | call s:assert.equal(getbufvar(l:bufinfo.name, 'LanguageClient_isServerRunning'), 1) 42 | call s:assert.equal(getbufvar(l:bufinfo.name, 'LanguageClient_projectRoot'), g:repo_root) 43 | call s:assert.equal(getbufvar(l:bufinfo.name, 'LanguageClient_statusLineDiagnosticsCounts'), {'E': 0, 'W': 1, 'H': 0, 'I': 0}) 44 | endfunction 45 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; 5 | using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; 6 | using System.Management.Automation; 7 | using System.Threading; 8 | 9 | namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console 10 | { 11 | using System; 12 | 13 | internal class PsrlReadLine : TerminalReadLine 14 | { 15 | private readonly PSReadLineProxy _psrlProxy; 16 | 17 | private readonly PsesInternalHost _psesHost; 18 | 19 | private readonly EngineIntrinsics _engineIntrinsics; 20 | 21 | public PsrlReadLine( 22 | PSReadLineProxy psrlProxy, 23 | PsesInternalHost psesHost, 24 | EngineIntrinsics engineIntrinsics, 25 | Func readKeyFunc, 26 | Action onIdleAction) 27 | { 28 | _psrlProxy = psrlProxy; 29 | _psesHost = psesHost; 30 | _engineIntrinsics = engineIntrinsics; 31 | _psrlProxy.OverrideReadKey(readKeyFunc); 32 | _psrlProxy.OverrideIdleHandler(onIdleAction); 33 | } 34 | 35 | public override string ReadLine(CancellationToken cancellationToken) => _psesHost.InvokeDelegate( 36 | representation: "ReadLine", 37 | new ExecutionOptions { RequiresForeground = true }, 38 | InvokePSReadLine, 39 | cancellationToken); 40 | 41 | public override void AddToHistory(string historyEntry) => _psrlProxy.AddToHistory(historyEntry); 42 | 43 | protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) => _psesHost.ReadKey(intercept: true, cancellationToken); 44 | 45 | private string InvokePSReadLine(CancellationToken cancellationToken) 46 | { 47 | EngineIntrinsics engineIntrinsics = _psesHost.IsRunspacePushed ? null : _engineIntrinsics; 48 | return _psrlProxy.ReadLine(_psesHost.Runspace, engineIntrinsics, cancellationToken, /* lastExecutionStatus */ null); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetailsBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter 7 | { 8 | /// 9 | /// Defines the common details between a variable and a variable container such as a scope 10 | /// in the current debugging session. 11 | /// 12 | internal abstract class VariableDetailsBase 13 | { 14 | /// 15 | /// Provides a constant that is used as the starting variable ID for all. 16 | /// Avoid 0 as it indicates a variable node with no children. 17 | /// variables. 18 | /// 19 | public const int FirstVariableId = 1; 20 | 21 | /// 22 | /// Gets the numeric ID of the variable which can be used to refer 23 | /// to it in future requests. 24 | /// 25 | public int Id { get; set; } 26 | 27 | /// 28 | /// Gets the variable's name. 29 | /// 30 | public string Name { get; protected set; } 31 | 32 | /// 33 | /// Gets the string representation of the variable's value. 34 | /// If the variable is an expandable object, this string 35 | /// will be empty. 36 | /// 37 | public string ValueString { get; protected set; } 38 | 39 | /// 40 | /// Gets the type of the variable's value. 41 | /// 42 | public string Type { get; protected set; } 43 | 44 | /// 45 | /// Returns true if the variable's value is expandable, meaning 46 | /// that it has child properties or its contents can be enumerated. 47 | /// 48 | public bool IsExpandable { get; protected set; } 49 | 50 | /// 51 | /// If this variable instance is expandable, this method returns the 52 | /// details of its children. Otherwise it returns an empty array. 53 | /// 54 | public abstract VariableDetailsBase[] GetChildren(ILogger logger); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/vim-test.yml: -------------------------------------------------------------------------------- 1 | name: Vim End-to-End Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ main ] 9 | merge_group: 10 | types: [ checks_requested ] 11 | 12 | jobs: 13 | vim: 14 | name: themis 15 | runs-on: ubuntu-latest 16 | env: 17 | DOTNET_NOLOGO: true 18 | DOTNET_GENERATE_ASPNET_CERTIFICATE: false 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Install dotnet 24 | uses: actions/setup-dotnet@v4 25 | with: 26 | cache: true 27 | cache-dependency-path: '**/*.csproj' 28 | 29 | - name: Install PSResources 30 | shell: pwsh 31 | run: tools/installPSResources.ps1 32 | 33 | - name: Build 34 | shell: pwsh 35 | run: Invoke-Build Build 36 | 37 | - name: Install Vim 38 | id: vim 39 | uses: rhysd/action-setup-vim@v1 40 | with: 41 | version: nightly 42 | 43 | - name: Checkout vim-ps1 44 | uses: actions/checkout@v4 45 | with: 46 | repository: PProvost/vim-ps1 47 | path: vim-ps1 48 | 49 | - name: Checkout LanguageClient-neovim 50 | uses: actions/checkout@v4 51 | with: 52 | repository: autozimu/LanguageClient-neovim 53 | path: LanguageClient-neovim 54 | 55 | - name: Install LanguageClient-neovim 56 | run: ./install.sh 57 | working-directory: LanguageClient-neovim 58 | 59 | - name: Checkout Themis 60 | uses: actions/checkout@v4 61 | with: 62 | repository: thinca/vim-themis 63 | path: vim-themis 64 | 65 | # - name: Debug if run with debugging enabled 66 | # uses: lhotari/action-upterm@v1 67 | 68 | - name: Run Themis with full CLI 69 | env: 70 | THEMIS_VIM: ${{ steps.vim.outputs.executable }} 71 | run: ./vim-themis/bin/themis ./test/vim-test.vim 72 | 73 | - name: Run Themis with simple CLI 74 | env: 75 | THEMIS_VIM: ${{ steps.vim.outputs.executable }} 76 | run: ./vim-themis/bin/themis ./test/vim-simple-test.vim 77 | -------------------------------------------------------------------------------- /test/emacs-simple-test.el: -------------------------------------------------------------------------------- 1 | ;;; emacs-simple-test.el --- Integration testing script -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (c) Microsoft Corporation. 4 | ;; Licensed under the MIT License. 5 | 6 | ;; Author: Andy Jordan 7 | ;; Keywords: PowerShell, LSP 8 | 9 | ;;; Code: 10 | 11 | ;; Avoid using old packages. 12 | (setq load-prefer-newer t) 13 | 14 | ;; Improved TLS Security. 15 | (with-eval-after-load 'gnutls 16 | (custom-set-variables 17 | '(gnutls-verify-error t) 18 | '(gnutls-min-prime-bits 3072))) 19 | 20 | ;; Package setup. 21 | (require 'package) 22 | (add-to-list 'package-archives 23 | '("melpa" . "https://melpa.org/packages/") t) 24 | (package-initialize) 25 | (package-refresh-contents) 26 | 27 | (require 'ert) 28 | 29 | (require 'flymake) 30 | 31 | (unless (package-installed-p 'powershell) 32 | (package-install 'powershell)) 33 | (require 'powershell) 34 | 35 | (unless (package-installed-p 'eglot) 36 | (package-install 'eglot)) 37 | (require 'eglot) 38 | 39 | (ert-deftest powershell-editor-services () 40 | "Eglot should connect to PowerShell Editor Services." 41 | (let* ((repo (project-root (project-current))) 42 | (start-script (expand-file-name "module/PowerShellEditorServices/Start-EditorServices.ps1" repo)) 43 | (test-script (expand-file-name "test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1" repo)) 44 | (eglot-sync-connect t)) 45 | (add-to-list 46 | 'eglot-server-programs 47 | `(powershell-mode 48 | . ("pwsh" "-NoLogo" "-NoProfile" "-Command" ,start-script "-Stdio"))) 49 | (with-current-buffer (find-file-noselect test-script) 50 | (should (eq major-mode 'powershell-mode)) 51 | (should (apply #'eglot--connect (eglot--guess-contact))) 52 | (should (eglot-current-server)) 53 | (let ((lsp (eglot-current-server))) 54 | (should (string= (eglot--project-nickname lsp) "PowerShellEditorServices")) 55 | (should (member (cons 'powershell-mode "powershell") (eglot--languages lsp)))) 56 | (sleep-for 5) ; TODO: Wait for "textDocument/publishDiagnostics" instead 57 | (flymake-start) 58 | (goto-char (point-min)) 59 | (flymake-goto-next-error) 60 | (should (eq 'flymake-warning (face-at-point)))))) 61 | 62 | (provide 'emacs-test) 63 | ;;; emacs-test.el ends here 64 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Utility/AsyncUtils.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace Microsoft.PowerShell.EditorServices.Utility 11 | { 12 | /// 13 | /// Provides utility methods for common asynchronous operations. 14 | /// 15 | internal static class AsyncUtils 16 | { 17 | /// 18 | /// Creates a with an handle initial and 19 | /// max count of one. 20 | /// 21 | /// A simple single handle . 22 | internal static SemaphoreSlim CreateSimpleLockingSemaphore() => new(initialCount: 1, maxCount: 1); 23 | 24 | internal static Task HandleErrorsAsync( 25 | this Task task, 26 | ILogger logger, 27 | [CallerMemberName] string callerName = null, 28 | [CallerFilePath] string callerSourceFile = null, 29 | [CallerLineNumber] int callerLineNumber = -1) 30 | { 31 | return task.IsCompleted && !(task.IsFaulted || task.IsCanceled) 32 | ? task 33 | : LogTaskErrors(task, logger, callerName, callerSourceFile, callerLineNumber); 34 | } 35 | 36 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD003:Avoid awaiting foreign Tasks", Justification = "It's a wrapper.")] 37 | private static async Task LogTaskErrors(Task task, ILogger logger, string callerName, string callerSourceFile, int callerLineNumber) 38 | { 39 | try 40 | { 41 | await task.ConfigureAwait(false); 42 | } 43 | catch (OperationCanceledException) 44 | { 45 | logger.LogDebug($"Task canceled in '{callerName}' in file '{callerSourceFile}' line {callerLineNumber}"); 46 | throw; 47 | } 48 | catch (Exception e) 49 | { 50 | logger.LogError(e, $"Exception thrown running task in '{callerName}' in file '{callerSourceFile}' line {callerLineNumber}"); 51 | throw; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Services/PowerShell/IPowerShellExecutionService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; 5 | using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Management.Automation; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using SMA = System.Management.Automation; 12 | 13 | namespace Microsoft.PowerShell.EditorServices.Services.PowerShell 14 | { 15 | public interface IPowerShellExecutionService 16 | { 17 | Task ExecuteDelegateAsync( 18 | string representation, 19 | ExecutionOptions executionOptions, 20 | Func func, 21 | CancellationToken cancellationToken); 22 | 23 | Task ExecuteDelegateAsync( 24 | string representation, 25 | ExecutionOptions executionOptions, 26 | Action action, 27 | CancellationToken cancellationToken); 28 | 29 | Task ExecuteDelegateAsync( 30 | string representation, 31 | ExecutionOptions executionOptions, 32 | Func func, 33 | CancellationToken cancellationToken); 34 | 35 | Task ExecuteDelegateAsync( 36 | string representation, 37 | ExecutionOptions executionOptions, 38 | Action action, 39 | CancellationToken cancellationToken); 40 | 41 | Task> ExecutePSCommandAsync( 42 | PSCommand psCommand, 43 | CancellationToken cancellationToken, 44 | PowerShellExecutionOptions executionOptions = null); 45 | 46 | Task ExecutePSCommandAsync( 47 | PSCommand psCommand, 48 | CancellationToken cancellationToken, 49 | PowerShellExecutionOptions executionOptions = null); 50 | 51 | void CancelCurrentTask(); 52 | } 53 | 54 | internal interface IInternalPowerShellExecutionService : IPowerShellExecutionService 55 | { 56 | event Action RunspaceChanged; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1: -------------------------------------------------------------------------------- 1 | class MyClass { 2 | [String] $Name; 3 | [Int32] $Number; } 4 | [bool]$scriptBool = $false 5 | $scriptInt = 42 6 | function Test-Variables { 7 | $strVar = "Hello" 8 | [string]$strVar2 = "Hello2" 9 | $arrVar = @(1, 2, $strVar, $objVar) 10 | $assocArrVar = @{ firstChild = "Child"; secondChild = 42 } 11 | $classVar = [MyClass]::new(); 12 | $classVar.Name = "Test" 13 | $classVar.Number = 42; 14 | $enumVar = $ErrorActionPreference 15 | $nullString = [NullString]::Value 16 | $psObjVar = New-Object -TypeName PSObject -Property @{Name = 'John'; Age = 75 } 17 | $psCustomObjVar = [PSCustomObject] @{Name = 'Paul'; Age = 73 } 18 | $procVar = Get-Process -PID $PID 19 | $trueVar = $true 20 | $falseVar = $false 21 | Write-Output "Done" 22 | } 23 | 24 | Test-Variables 25 | # NOTE: If a line is added to the function above, the line numbers in the 26 | # associated unit tests MUST be adjusted accordingly. 27 | 28 | $SCRIPT:simpleArray = @( 29 | 1 30 | 2 31 | 'red' 32 | 'blue' 33 | ) 34 | 35 | # This is a dummy function that the test will use to stop and evaluate the debug environment 36 | function __BreakDebuggerEnumerableShowsRawView{}; __BreakDebuggerEnumerableShowsRawView 37 | 38 | $SCRIPT:simpleDictionary = @{ 39 | item1 = 1 40 | item2 = 2 41 | item3 = 'red' 42 | item4 = 'blue' 43 | } 44 | function __BreakDebuggerDictionaryShowsRawView{}; __BreakDebuggerDictionaryShowsRawView 45 | 46 | $SCRIPT:sortedDictionary = [Collections.Generic.SortedDictionary[string, object]]::new() 47 | $sortedDictionary[1] = 1 48 | $sortedDictionary[2] = 2 49 | $sortedDictionary['red'] = 'red' 50 | $sortedDictionary['blue'] = 'red' 51 | 52 | # This is a dummy function that the test will use to stop and evaluate the debug environment 53 | function __BreakDebuggerDerivedDictionaryPropertyInRawView{}; __BreakDebuggerDerivedDictionaryPropertyInRawView 54 | 55 | class CustomToString { 56 | [String]$String = 'Hello' 57 | [String]ToString() { 58 | return $this.String.ToUpper() 59 | } 60 | } 61 | $SCRIPT:CustomToStrings = 1..1000 | ForEach-Object { 62 | [CustomToString]::new() 63 | } 64 | 65 | # This is a dummy function that the test will use to stop and evaluate the debug environment 66 | function __BreakDebuggerToStringShouldMarshallToPipeline{}; __BreakDebuggerToStringShouldMarshallToPipeline 67 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ScopesHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.PowerShell.EditorServices.Services; 8 | using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; 9 | using Microsoft.PowerShell.EditorServices.Utility; 10 | using OmniSharp.Extensions.DebugAdapter.Protocol.Models; 11 | using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; 12 | 13 | namespace Microsoft.PowerShell.EditorServices.Handlers 14 | { 15 | internal class ScopesHandler : IScopesHandler 16 | { 17 | private readonly DebugService _debugService; 18 | 19 | public ScopesHandler(DebugService debugService) => _debugService = debugService; 20 | 21 | /// 22 | /// Retrieves the variable scopes (containers) for the currently selected stack frame. Variables details are fetched via a separate request. 23 | /// 24 | public Task Handle(ScopesArguments request, CancellationToken cancellationToken) 25 | { 26 | // HACK: The StackTraceHandler injects an artificial label frame as the first frame as a performance optimization, so when scopes are requested by the client, we need to adjust the frame index accordingly to match the underlying PowerShell frame, so when the client clicks on the label (or hit the default breakpoint), they get variables populated from the top of the PowerShell stackframe. If the client dives deeper, we need to reflect that as well (though 90% of debug users don't actually investigate this) 27 | // VSCode Frame 0 (Label) -> PowerShell StackFrame 0 (for convenience) 28 | // VSCode Frame 1 (First Real PS Frame) -> Also PowerShell StackFrame 0 29 | // VSCode Frame 2 -> PowerShell StackFrame 1 30 | // VSCode Frame 3 -> PowerShell StackFrame 2 31 | // etc. 32 | int powershellFrameId = request.FrameId == 0 ? 0 : (int)request.FrameId - 1; 33 | 34 | VariableScope[] variableScopes = _debugService.GetVariableScopes(powershellFrameId); 35 | 36 | return Task.FromResult(new ScopesResponse 37 | { 38 | Scopes = new Container( 39 | variableScopes 40 | .Select(LspDebugUtils.CreateScope) 41 | ) 42 | }); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /module/PowerShellEditorServices/Commands/Public/ConvertFrom-ScriptExtent.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | function ConvertFrom-ScriptExtent { 5 | <# 6 | .EXTERNALHELP ..\PowerShellEditorServices.Commands-help.xml 7 | #> 8 | [CmdletBinding()] 9 | [OutputType([Microsoft.PowerShell.EditorServices.Extensions.IFileRange, Microsoft.PowerShell.EditorServices], ParameterSetName='BufferRange')] 10 | [OutputType([Microsoft.PowerShell.EditorServices.Extensions.IFilePosition, Microsoft.PowerShell.EditorServices], ParameterSetName='BufferPosition')] 11 | param( 12 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 13 | [ValidateNotNullOrEmpty()] 14 | [System.Management.Automation.Language.IScriptExtent[]] 15 | $Extent, 16 | 17 | [Parameter(ParameterSetName='BufferRange')] 18 | [switch] 19 | $BufferRange, 20 | 21 | [Parameter(ParameterSetName='BufferPosition')] 22 | [switch] 23 | $BufferPosition, 24 | 25 | [Parameter(ParameterSetName='BufferPosition')] 26 | [switch] 27 | $Start, 28 | 29 | [Parameter(ParameterSetName='BufferPosition')] 30 | [switch] 31 | $End 32 | ) 33 | process { 34 | foreach ($aExtent in $Extent) { 35 | switch ($PSCmdlet.ParameterSetName) { 36 | BufferRange { 37 | # yield 38 | [Microsoft.PowerShell.EditorServices.Extensions.FileRange, Microsoft.PowerShell.EditorServices]::new( 39 | $aExtent.StartLineNumber, 40 | $aExtent.StartColumnNumber, 41 | $aExtent.EndLineNumber, 42 | $aExtent.EndColumnNumber) 43 | } 44 | BufferPosition { 45 | if ($End) { 46 | $line = $aExtent.EndLineNumber 47 | $column = $aExtent.EndLineNumber 48 | } else { 49 | $line = $aExtent.StartLineNumber 50 | $column = $aExtent.StartLineNumber 51 | } 52 | # yield 53 | [Microsoft.PowerShell.EditorServices.Extensions.FileRange, Microsoft.PowerShell.EditorServices]::new( 54 | $line, 55 | $column) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Services/DebugAdapter/Handlers/SetVariableHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Management.Automation; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.PowerShell.EditorServices.Services; 10 | using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; 11 | using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; 12 | using OmniSharp.Extensions.JsonRpc; 13 | 14 | namespace Microsoft.PowerShell.EditorServices.Handlers 15 | { 16 | internal class SetVariableHandler : ISetVariableHandler 17 | { 18 | private readonly ILogger _logger; 19 | private readonly DebugService _debugService; 20 | 21 | public SetVariableHandler( 22 | ILoggerFactory loggerFactory, 23 | DebugService debugService) 24 | { 25 | _logger = loggerFactory.CreateLogger(); 26 | _debugService = debugService; 27 | } 28 | 29 | public async Task Handle(SetVariableArguments request, CancellationToken cancellationToken) 30 | { 31 | try 32 | { 33 | string updatedValue = 34 | await _debugService.SetVariableAsync( 35 | (int)request.VariablesReference, 36 | request.Name, 37 | request.Value).ConfigureAwait(false); 38 | 39 | return new SetVariableResponse { Value = updatedValue }; 40 | } 41 | catch (Exception e) when (e is ArgumentTransformationMetadataException or 42 | InvalidPowerShellExpressionException or 43 | SessionStateUnauthorizedAccessException) 44 | { 45 | // Catch common, innocuous errors caused by the user supplying a value that can't be converted or the variable is not settable. 46 | string msg = $"Failed to set variable: {e.Message}"; 47 | _logger.LogTrace(msg); 48 | throw new RpcErrorException(0, null, msg); 49 | } 50 | catch (Exception e) 51 | { 52 | string msg = $"Unexpected error setting variable: {e.Message}"; 53 | _logger.LogError(msg); 54 | throw new RpcErrorException(0, null, msg); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Services/PowerShell/Runspace/RunspaceChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.PowerShell.EditorServices.Utility; 5 | 6 | namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace 7 | { 8 | /// 9 | /// Defines the set of actions that will cause the runspace to be changed. 10 | /// 11 | internal enum RunspaceChangeAction 12 | { 13 | /// 14 | /// The runspace change was caused by entering a new session. 15 | /// 16 | Enter, 17 | 18 | /// 19 | /// The runspace change was caused by exiting the current session. 20 | /// 21 | Exit, 22 | 23 | /// 24 | /// The runspace change was caused by shutting down the service. 25 | /// 26 | Shutdown 27 | } 28 | 29 | /// 30 | /// Provides arguments for the PowerShellContext.RunspaceChanged event. 31 | /// 32 | internal class RunspaceChangedEventArgs 33 | { 34 | /// 35 | /// Creates a new instance of the RunspaceChangedEventArgs class. 36 | /// 37 | /// The action which caused the runspace to change. 38 | /// The previously active runspace. 39 | /// The newly active runspace. 40 | public RunspaceChangedEventArgs( 41 | RunspaceChangeAction changeAction, 42 | IRunspaceInfo previousRunspace, 43 | IRunspaceInfo newRunspace) 44 | { 45 | Validate.IsNotNull(nameof(previousRunspace), previousRunspace); 46 | 47 | ChangeAction = changeAction; 48 | PreviousRunspace = previousRunspace; 49 | NewRunspace = newRunspace; 50 | } 51 | 52 | /// 53 | /// Gets the RunspaceChangeAction which caused this event. 54 | /// 55 | public RunspaceChangeAction ChangeAction { get; } 56 | 57 | /// 58 | /// Gets a RunspaceDetails object describing the previous runspace. 59 | /// 60 | public IRunspaceInfo PreviousRunspace { get; } 61 | 62 | /// 63 | /// Gets a RunspaceDetails object describing the new runspace. 64 | /// 65 | public IRunspaceInfo NewRunspace { get; } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | all 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Services/PowerShell/Host/NullPSHostUI.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.ObjectModel; 7 | using System.Management.Automation; 8 | using System.Management.Automation.Host; 9 | using System.Security; 10 | 11 | namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host 12 | { 13 | internal class NullPSHostUI : PSHostUserInterface 14 | { 15 | public NullPSHostUI() => RawUI = new NullPSHostRawUI(); 16 | 17 | public override bool SupportsVirtualTerminal => false; 18 | 19 | public override PSHostRawUserInterface RawUI { get; } 20 | 21 | public override Dictionary Prompt(string caption, string message, Collection descriptions) => new(); 22 | 23 | public override int PromptForChoice(string caption, string message, Collection choices, int defaultChoice) => 0; 24 | 25 | public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) => new(userName: string.Empty, password: new SecureString()); 26 | 27 | public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) 28 | => PromptForCredential(caption, message, userName, targetName, PSCredentialTypes.Default, PSCredentialUIOptions.Default); 29 | 30 | public override string ReadLine() => string.Empty; 31 | 32 | public override SecureString ReadLineAsSecureString() => new(); 33 | 34 | public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) { } 35 | 36 | public override void Write(string value) { } 37 | 38 | public override void WriteDebugLine(string message) { } 39 | 40 | public override void WriteErrorLine(string value) { } 41 | 42 | public override void WriteInformation(InformationRecord record) { } 43 | 44 | public override void WriteLine() { } 45 | 46 | public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) { } 47 | 48 | public override void WriteLine(string value) { } 49 | 50 | public override void WriteProgress(long sourceId, ProgressRecord record) { } 51 | 52 | public override void WriteVerboseLine(string message) { } 53 | 54 | public override void WriteWarningLine(string message) { } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /module/docs/Join-ScriptExtent.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PowerShellEditorServices.Commands-help.xml 3 | online version: https://github.com/PowerShell/PowerShellEditorServices/tree/main/module/docs/Join-ScriptExtent.md 4 | schema: 2.0.0 5 | --- 6 | 7 | # Join-ScriptExtent 8 | 9 | ## SYNOPSIS 10 | 11 | Combine multiple ScriptExtent objects into a single ScriptExtent. 12 | 13 | ## SYNTAX 14 | 15 | ```powershell 16 | Join-ScriptExtent [[-Extent] ] [] 17 | ``` 18 | 19 | ## DESCRIPTION 20 | 21 | The Join-ScriptExtent function will combine all ScriptExtent objects piped to it into a single extent. This can be used combine multiple ASTs, tokens, or other script elements into a single object that can then be manipulated or used for more targeted searches. 22 | 23 | ## EXAMPLES 24 | 25 | ### -------------------------- EXAMPLE 1 -------------------------- 26 | 27 | ```powershell 28 | $ast = Find-Ast { $_.Arguments.Count -gt 4 } -First 29 | $ast.Arguments[0..1] | Join-ScriptExtent | Set-ScriptExtent -Text '' 30 | ``` 31 | 32 | Finds the first InvokeMemberExpression ast that has over 4 arguments and removes the first two. 33 | 34 | ## PARAMETERS 35 | 36 | ### -Extent 37 | 38 | Specifies the extents to combine. If a single extent is passed, it will be returned as is. If no extents are passed nothing will be returned. Extents passed from the pipeline are processed after pipeline input completes. 39 | 40 | ```yaml 41 | Type: IScriptExtent[] 42 | Parameter Sets: (All) 43 | Aliases: 44 | 45 | Required: False 46 | Position: 1 47 | Default value: None 48 | Accept pipeline input: True (ByPropertyName, ByValue) 49 | Accept wildcard characters: False 50 | ``` 51 | 52 | ### CommonParameters 53 | 54 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216). 55 | 56 | ## INPUTS 57 | 58 | ### System.Management.Automation.Language.IScriptExtent 59 | 60 | You can pass ScriptExtent objects to this function. You can also pass objects with a property named "Extent" such as ASTs from Find-Ast or tokens from Get-Token. 61 | 62 | ## OUTPUTS 63 | 64 | ### System.Management.Automation.Language.IScriptExtent 65 | 66 | The combined ScriptExtent object is returned. 67 | 68 | ## NOTES 69 | 70 | ## RELATED LINKS 71 | 72 | [ConvertTo-ScriptExtent](ConvertTo-ScriptExtent.md) 73 | [ConvertFrom-ScriptExtent](ConvertFrom-ScriptExtent.md) 74 | [Test-ScriptExtent](Test-ScriptExtent.md) 75 | [Set-ScriptExtent](Set-ScriptExtent.md) 76 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Management.Automation; 6 | using Microsoft.PowerShell.EditorServices.Utility; 7 | 8 | namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter 9 | { 10 | /// 11 | /// Provides details about a command breakpoint that is set in the PowerShell debugger. 12 | /// 13 | internal sealed class CommandBreakpointDetails : BreakpointDetailsBase 14 | { 15 | /// 16 | /// Gets the name of the command on which the command breakpoint has been set. 17 | /// 18 | public string Name { get; private set; } 19 | 20 | private CommandBreakpointDetails() 21 | { 22 | } 23 | 24 | /// 25 | /// Creates an instance of the class from the individual 26 | /// pieces of breakpoint information provided by the client. 27 | /// 28 | /// The name of the command to break on. 29 | /// Condition string that would be applied to the breakpoint Action parameter. 30 | /// 31 | internal static CommandBreakpointDetails Create(string name, string condition = null) 32 | { 33 | Validate.IsNotNull(nameof(name), name); 34 | 35 | return new CommandBreakpointDetails 36 | { 37 | Name = name, 38 | Condition = condition 39 | }; 40 | } 41 | 42 | /// 43 | /// Creates an instance of the class from a 44 | /// PowerShell CommandBreakpoint object. 45 | /// 46 | /// The Breakpoint instance from which details will be taken. 47 | /// A new instance of the BreakpointDetails class. 48 | internal static CommandBreakpointDetails Create(Breakpoint breakpoint) 49 | { 50 | Validate.IsNotNull(nameof(breakpoint), breakpoint); 51 | 52 | if (breakpoint is not CommandBreakpoint commandBreakpoint) 53 | { 54 | throw new ArgumentException( 55 | "Unexpected breakpoint type: " + breakpoint.GetType().Name); 56 | } 57 | 58 | return new() 59 | { 60 | Verified = true, 61 | Name = commandBreakpoint.Command, 62 | Condition = commandBreakpoint.Action?.ToString() 63 | }; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Services/PowerShell/Handlers/ExpandAliasHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Management.Automation; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using MediatR; 8 | using OmniSharp.Extensions.JsonRpc; 9 | using Microsoft.PowerShell.EditorServices.Services.PowerShell; 10 | 11 | namespace Microsoft.PowerShell.EditorServices.Handlers 12 | { 13 | [Serial, Method("powerShell/expandAlias")] 14 | internal interface IExpandAliasHandler : IJsonRpcRequestHandler { } 15 | 16 | internal class ExpandAliasParams : IRequest 17 | { 18 | public string Text { get; set; } 19 | } 20 | 21 | internal class ExpandAliasResult 22 | { 23 | public string Text { get; set; } 24 | } 25 | 26 | internal class ExpandAliasHandler : IExpandAliasHandler 27 | { 28 | private readonly IInternalPowerShellExecutionService _executionService; 29 | 30 | public ExpandAliasHandler(IInternalPowerShellExecutionService executionService) => _executionService = executionService; 31 | 32 | public async Task Handle(ExpandAliasParams request, CancellationToken cancellationToken) 33 | { 34 | const string script = @" 35 | function __Expand-Alias { 36 | [System.Diagnostics.DebuggerHidden()] 37 | param($targetScript) 38 | 39 | [ref]$errors=$null 40 | 41 | $tokens = [System.Management.Automation.PsParser]::Tokenize($targetScript, $errors).Where({$_.type -eq 'command'}) | 42 | Sort-Object Start -Descending 43 | 44 | foreach ($token in $tokens) { 45 | $definition=(Get-Command ('`'+$token.Content) -CommandType Alias -ErrorAction SilentlyContinue).Definition 46 | 47 | if($definition) { 48 | $lhs=$targetScript.Substring(0, $token.Start) 49 | $rhs=$targetScript.Substring($token.Start + $token.Length) 50 | 51 | $targetScript=$lhs + $definition + $rhs 52 | } 53 | } 54 | 55 | $targetScript 56 | }"; 57 | 58 | // TODO: Refactor to not rerun the function definition every time. 59 | PSCommand psCommand = new(); 60 | psCommand 61 | .AddScript(script) 62 | .AddStatement() 63 | .AddCommand("__Expand-Alias") 64 | .AddArgument(request.Text); 65 | System.Collections.Generic.IReadOnlyList result = await _executionService.ExecutePSCommandAsync(psCommand, cancellationToken).ConfigureAwait(false); 66 | 67 | return new ExpandAliasResult 68 | { 69 | Text = result[0] 70 | }; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/emacs-test.el: -------------------------------------------------------------------------------- 1 | ;;; emacs-test.el --- Integration testing script -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (c) Microsoft Corporation. 4 | ;; Licensed under the MIT License. 5 | 6 | ;; Author: Andy Jordan 7 | ;; Keywords: PowerShell, LSP 8 | 9 | ;;; Code: 10 | 11 | ;; Avoid using old packages. 12 | (setq load-prefer-newer t) 13 | 14 | ;; Improved TLS Security. 15 | (with-eval-after-load 'gnutls 16 | (custom-set-variables 17 | '(gnutls-verify-error t) 18 | '(gnutls-min-prime-bits 3072))) 19 | 20 | ;; Package setup. 21 | (require 'package) 22 | (add-to-list 'package-archives 23 | '("melpa" . "https://melpa.org/packages/") t) 24 | (package-initialize) 25 | (package-refresh-contents) 26 | 27 | (require 'ert) 28 | 29 | (require 'flymake) 30 | 31 | (unless (package-installed-p 'powershell) 32 | (package-install 'powershell)) 33 | (require 'powershell) 34 | 35 | (unless (package-installed-p 'eglot) 36 | (package-install 'eglot)) 37 | (require 'eglot) 38 | 39 | (ert-deftest powershell-editor-services () 40 | "Eglot should connect to PowerShell Editor Services." 41 | (let* ((repo (project-root (project-current))) 42 | (start-script (expand-file-name "module/PowerShellEditorServices/Start-EditorServices.ps1" repo)) 43 | (module-path (expand-file-name "module" repo)) 44 | (log-path (expand-file-name "test/emacs-test.log" repo)) 45 | (session-path (expand-file-name "test/emacs-session.json" repo)) 46 | (test-script (expand-file-name "test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1" repo)) 47 | (eglot-sync-connect t)) 48 | (add-to-list 49 | 'eglot-server-programs 50 | `(powershell-mode 51 | . ("pwsh" "-NoLogo" "-NoProfile" "-Command" ,start-script 52 | "-HostName" "Emacs" "-HostProfileId" "Emacs" "-HostVersion" "1.0.0" 53 | "-BundledModulesPath" ,module-path 54 | "-LogPath" ,log-path "-LogLevel" "Diagnostic" 55 | "-SessionDetailsPath" ,session-path 56 | "-Stdio"))) 57 | (with-current-buffer (find-file-noselect test-script) 58 | (should (eq major-mode 'powershell-mode)) 59 | (should (apply #'eglot--connect (eglot--guess-contact))) 60 | (should (eglot-current-server)) 61 | (let ((lsp (eglot-current-server))) 62 | (should (string= (eglot--project-nickname lsp) "PowerShellEditorServices")) 63 | (should (member (cons 'powershell-mode "powershell") (eglot--languages lsp)))) 64 | (sleep-for 5) ; TODO: Wait for "textDocument/publishDiagnostics" instead 65 | (flymake-start) 66 | (goto-char (point-min)) 67 | (flymake-goto-next-error) 68 | (should (eq 'flymake-warning (face-at-point)))))) 69 | 70 | (provide 'emacs-test) 71 | ;;; emacs-test.el ends here 72 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin) and [PowerShell](https://github.com/PowerShell). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/PowerShellEditorServices/Services/PowerShell/Runspace/SessionDetails.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections; 6 | using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; 7 | using System.Linq; 8 | 9 | namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace 10 | { 11 | using System.Management.Automation; 12 | 13 | /// 14 | /// Provides details about the current PowerShell session. 15 | /// 16 | internal class SessionDetails 17 | { 18 | private const string Property_ComputerName = "computerName"; 19 | private const string Property_ProcessId = "processId"; 20 | private const string Property_InstanceId = "instanceId"; 21 | 22 | /// 23 | /// Runs a PowerShell command to gather details about the current session. 24 | /// 25 | /// A data object containing details about the PowerShell session. 26 | public static SessionDetails GetFromPowerShell(PowerShell pwsh) 27 | { 28 | Hashtable detailsObject = pwsh 29 | .AddScript( 30 | $"[System.Diagnostics.DebuggerHidden()]param() @{{ '{Property_ComputerName}' = if ([Environment]::MachineName) {{[Environment]::MachineName}} else {{'localhost'}}; '{Property_ProcessId}' = $PID; '{Property_InstanceId}' = $host.InstanceId }}", 31 | useLocalScope: true) 32 | .InvokeAndClear() 33 | .FirstOrDefault(); 34 | 35 | return new SessionDetails( 36 | (int)detailsObject[Property_ProcessId], 37 | (string)detailsObject[Property_ComputerName], 38 | (Guid?)detailsObject[Property_InstanceId]); 39 | } 40 | 41 | /// 42 | /// Creates an instance of SessionDetails using the information 43 | /// contained in the PSObject which was obtained using the 44 | /// PSCommand returned by GetDetailsCommand. 45 | /// 46 | public SessionDetails( 47 | int processId, 48 | string computerName, 49 | Guid? instanceId) 50 | { 51 | ProcessId = processId; 52 | ComputerName = computerName; 53 | InstanceId = instanceId; 54 | } 55 | 56 | /// 57 | /// Gets the process ID of the current process. 58 | /// 59 | public int? ProcessId { get; } 60 | 61 | /// 62 | /// Gets the name of the current computer. 63 | /// 64 | public string ComputerName { get; } 65 | 66 | /// 67 | /// Gets the current PSHost instance ID. 68 | /// 69 | public Guid? InstanceId { get; } 70 | } 71 | } 72 | --------------------------------------------------------------------------------