├── .gitignore ├── .github ├── dependabot.yml └── ISSUE_TEMPLATE │ ├── Feature_Request.md │ └── Bug_Report.md ├── tsconfig.json ├── src └── client │ ├── messages.ts │ ├── utils.ts │ ├── process.ts │ ├── settings.ts │ ├── extension.ts │ └── platform.ts ├── .vscode └── launch.json ├── downloadSnippets.ps1 ├── azure-pipelines.yml ├── LICENSE ├── downloadPSES.ps1 ├── README.md ├── tools └── Dockerfile ├── CONTRIBUTING.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | PowerShellEditorServices/ 3 | Snippets/ 4 | .pses 5 | node_modules 6 | out 7 | yarn.lock 8 | coc-powershell-*.tgz 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: "@types/node" 10 | versions: 11 | - "> 10.17.19" 12 | - dependency-name: typescript 13 | versions: 14 | - 4.1.5 15 | - 4.2.3 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noUnusedLocals": false, 4 | "noUnusedParameters": false, 5 | "noImplicitAny": true, 6 | "noImplicitReturns": true, 7 | "target": "es6", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "rootDir": "src", 11 | "outDir": "out", 12 | "lib": [ "es2016" ], 13 | "sourceMap": true, 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | "server", 20 | "Scratch.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_Request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request/idea 🚀 3 | about: Suggest a new feature or improvement (this does not mean you have to implement it) 4 | 5 | --- 6 | 7 | **Summary of the new feature** 8 | 9 | A clear and concise description of what the problem is that the new feature would solve. 10 | Try formulating it in user story style (if applicable): 11 | 'As a user I want X so that Y.' with X being the being the action and Y being the value of the action. 12 | 13 | **Proposed technical implementation details (optional)** 14 | 15 | A clear and concise description of what you want to happen. 16 | -------------------------------------------------------------------------------- /src/client/messages.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | // The collection of custom messages that coc-powershell will send to/receive from PowerShell Editor Services 7 | 8 | export const EvaluateRequestMessage = "evaluate"; 9 | export interface IEvaluateRequestArguments { 10 | expression: string; 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_Report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 🐛 3 | about: Report errors or unexpected behavior 🤔 4 | 5 | --- 6 | 7 | ### System Details 8 | 9 | * _Vim or NeoVim?_: 10 | * _Version of Vim (run `vim --version`) or NeoVim_ (run `nvim --version`): 11 | * _Version of `coc-powershell` (in Vim or NeoVim: `:CocList extensions coc-powershell`)_: 12 | * _Operating System_: 13 | * _PowerShell version (in PowerShell: `$PSVersionTable`)_: 14 | 15 | ### Issue Description 16 | 17 | I am experiencing a problem with... 18 | 19 | #### Expected Behaviour 20 | 21 | -- Description of what *should* be happening -- 22 | 23 | #### Actual Behaviour 24 | 25 | -- Description of what actually happens -- 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "Attach by Process ID", 11 | "processId": "${command:PickProcess}" 12 | }, 13 | { 14 | "name": "PowerShell: Launch Current File", 15 | "type": "PowerShell", 16 | "request": "launch", 17 | "script": "${file}", 18 | "cwd": "${file}" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /downloadSnippets.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | Write-Host Starting download of Snippets from vscode-powershell 4 | if (!$IsCoreCLR) { 5 | # We only need to do this in Windows PowerShell. 6 | [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 7 | } 8 | 9 | # Fail on anything 10 | $ErrorActionPreference = "Stop" 11 | # Progress doesn't display properly in vim 12 | $ProgressPreference = "SilentlyContinue" 13 | 14 | Push-Location $PSScriptRoot 15 | 16 | $download = "https://raw.githubusercontent.com/PowerShell/vscode-powershell/master/snippets/PowerShell.json" 17 | 18 | $dir = "$PSScriptRoot/Snippets/" 19 | 20 | $null = New-Item -Path $dir -ItemType Directory -Force 21 | Invoke-WebRequest $download -OutFile "$dir\PowerShell.json" 22 | 23 | Pop-Location 24 | 25 | Write-Host Completed downloading Snippets. 26 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'ubuntu-latest' 11 | 12 | steps: 13 | - task: NodeTool@0 14 | inputs: 15 | versionSpec: '10.x' 16 | displayName: 'Install Node.js' 17 | 18 | - pwsh: ./build.ps1 -Pack -Test 19 | displayName: 'Build script' 20 | 21 | - task: CopyFiles@2 22 | inputs: 23 | sourceFolder: '$(Build.SourcesDirectory)' 24 | contents: '*.tgz' 25 | targetFolder: $(Build.ArtifactStagingDirectory)/npm 26 | displayName: 'Copy npm package' 27 | 28 | - task: CopyFiles@2 29 | inputs: 30 | sourceFolder: '$(Build.SourcesDirectory)' 31 | contents: 'package.json' 32 | targetFolder: $(Build.ArtifactStagingDirectory)/npm 33 | displayName: 'Copy package.json' 34 | 35 | - task: PublishBuildArtifacts@1 36 | inputs: 37 | pathtoPublish: '$(Build.ArtifactStagingDirectory)/npm' 38 | artifactName: npm 39 | displayName: 'Publish npm artifact' 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yatao Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /downloadPSES.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | param( 3 | [switch]$AllowPreRelease 4 | ) 5 | if (!$IsCoreCLR) { 6 | # We only need to do this in Windows PowerShell. 7 | [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 8 | } 9 | 10 | # Fail on anything 11 | $ErrorActionPreference = "Stop" 12 | # Progress doesn't display properly in vim 13 | $ProgressPreference = "SilentlyContinue" 14 | 15 | Push-Location $PSScriptRoot 16 | 17 | $repo = "PowerShell/PowerShellEditorServices" 18 | $file = "PowerShellEditorServices.zip" 19 | 20 | $releases = "https://api.github.com/repos/$repo/releases" 21 | 22 | Write-Host Determining latest PowerShell Editor Services release... 23 | $tag = ((Invoke-RestMethod $releases)| Where-Object { $_.prerelease -eq $AllowPreRelease })[0].tag_name 24 | Write-Host Latest Release: $tag 25 | 26 | $download = "https://github.com/$repo/releases/download/$tag/$file" 27 | $zip = "pses.zip" 28 | Write-Host Downloading PowerShell Editor Services: $tag 29 | Invoke-WebRequest $download -OutFile $zip 30 | 31 | Write-Host Extracting release files... 32 | Microsoft.PowerShell.Archive\Expand-Archive $zip $pwd -Force 33 | 34 | Remove-Item $zip -Force 35 | 36 | Write-Host PowerShell Editor Services install completed. 37 | 38 | Pop-Location 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coc-powershell 2 | 3 | [![Build Status](https://v-yadli.visualstudio.com/coc-powershell/_apis/build/status/coc-extensions.coc-powershell?branchName=master)](https://v-yadli.visualstudio.com/coc-powershell/_build/latest?definitionId=4&branchName=master) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/821f104c56834a73acede8d387fc4c2b)](https://www.codacy.com/manual/coc-extensions/coc-powershell?utm_source=github.com&utm_medium=referral&utm_content=coc-extensions/coc-powershell&utm_campaign=Badge_Grade) 5 | ![npm](https://img.shields.io/npm/v/coc-powershell.svg) 6 | 7 | A vim plugin powered by 8 | [PowerShellEditorServices](https://github.com/PowerShell/PowerShellEditorServices) and 9 | [coc.nvim](https://github.com/neoclide/coc.nvim) 10 | to provide a rich PowerShell editing experience. 11 | 12 | Features include: 13 | * Intellisense/Completions 14 | * Go to definition 15 | * [PSScriptAnalyzer](https://github.com/PowerShell/PSScriptAnalyzer) integration 16 | * Integrated REPL environment that shares the context with the language services 17 | * and much more! 18 | 19 | ## Prerequisites 20 | 21 | 1. Vim 8.0+ or NeoVim 22 | 2. [PowerShell Core](https://github.com/powershell/powershell) or Windows PowerShell 23 | 3. [coc.nvim](https://github.com/neoclide/coc.nvim) 24 | 25 | ## Installation 26 | 27 | `coc-powershell` is an extension for `coc.nvim`. 28 | You can install `coc.nvim` with a plugin manager like [vim-plug](https://github.com/junegunn/vim-plug): 29 | ```vimL 30 | Plug 'neoclide/coc.nvim', {'branch': 'release'} 31 | ``` 32 | 33 | Then, use `:CocInstall coc-powershell` to install. 34 | 35 | Alternatively, you can have `coc.nvim` automatically install the extension if it's missing: 36 | ```vimL 37 | let g:coc_global_extensions=[ 'coc-powershell', ... ] 38 | ``` 39 | 40 | ## Configuration 41 | 42 | ### Disable the integrated console 43 | 44 | You can disable the integrated console when a PowerShell file is opened by editing your `coc-settings.json` file and setting `powershell.integratedConsole.showOnStartup` to `false`. 45 | 46 | ```json 47 | { 48 | "powershell.integratedConsole.showOnStartup": false 49 | } 50 | ``` 51 | 52 | ## Recommended plugins 53 | 54 | * [coc-snippets](https://github.com/neoclide/coc-snippets) Used to allow snippets [(requires neovim 0.4 or latest vim8)](https://github.com/neoclide/coc.nvim/wiki/F.A.Q#how-to-make-preview-window-shown-aside-with-pum). 55 | * Can be installed with `:CocInstall coc-snippets` 56 | * [vim-polyglot](https://github.com/sheerun/vim-polyglot) for syntax highlighting 🎨 57 | -------------------------------------------------------------------------------- /tools/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile creates two system users (vim, nvim) 2 | # 3 | # vim user: 4 | # * configured for vim with /home/vim/.vimrc 5 | # * vim-plug installed into /home/.vim/ 6 | # 7 | # nvim user: 8 | # * configured for neovim with /home/nvim/.config/nvim/init.vim 9 | # * vim-plug installed into /home/nvim/.local/share/nvim/ 10 | # 11 | # Plugins enabled by default: 12 | # * neoclide/coc.nvim 13 | # * bling/vim-airline 14 | # * sheerun/vim-polyglot 15 | # * coc-powershell (coc.nvim plugin) 16 | # 17 | # Both vim and neovim configurations are set with pwsh as default shell. 18 | # 19 | # Default container user is root with /tmp as WORKDIR. 20 | # 21 | # To test with neovim run: 22 | # runuser -l nvim -c 'nvim test.ps1' 23 | # 24 | # To test with vim run: 25 | # runuser -l vim -c 'vim test.ps1' 26 | # 27 | # You can also switch into each user by running: 28 | # 29 | # su - nvim 30 | # su - vim 31 | 32 | FROM mcr.microsoft.com/powershell:latest 33 | ARG DEBIAN_FRONTEND=noninteractive 34 | 35 | WORKDIR /tmp 36 | 37 | # Install base packages 38 | RUN apt-get update -qq 39 | RUN apt-get install wget vim git curl software-properties-common -y 40 | 41 | # Install neovim 42 | RUN add-apt-repository ppa:neovim-ppa/stable 43 | RUN apt-get update -qq 44 | RUN apt-get install neovim -y 45 | 46 | # Install nodejs 47 | RUN bash -c 'bash <(curl -sL install-node.now.sh/lts) --yes' 48 | 49 | # Create paswordless vim and nvim users 50 | RUN adduser --disabled-password --gecos "" vim 51 | RUN adduser --disabled-password --gecos "" nvim 52 | 53 | # Configure vim as vim user. 54 | USER vim 55 | WORKDIR /home/vim 56 | 57 | RUN curl -sfLo ~/.vim/autoload/plug.vim --create-dirs \ 58 | https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim 59 | 60 | RUN echo "call plug#begin('~/.vim/plugged')\n\ 61 | Plug 'neoclide/coc.nvim', {'branch': 'release'}\n\ 62 | Plug 'bling/vim-airline'\n\ 63 | Plug 'sheerun/vim-polyglot'\n\ 64 | call plug#end()\n\ 65 | autocmd FileType ps1 setlocal shell=pwsh" >> ~/.vimrc 66 | 67 | RUN vim +PlugInstall +qall 68 | RUN vim -c 'CocInstall -sync coc-powershell coc-snippets coc-json|q' 69 | 70 | RUN rm -rf /tmp/coc-nvim* 71 | 72 | 73 | # Configure nvim as nvim user. 74 | USER nvim 75 | WORKDIR /home/nvim 76 | 77 | RUN curl -fLo ~/.local/share/nvim/site/autoload/plug.vim --create-dirs \ 78 | https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim 79 | 80 | RUN mkdir -p ~/.config/nvim 81 | 82 | RUN echo "call plug#begin('~/.local/share/nvim/plugged')\n\ 83 | Plug 'neoclide/coc.nvim', {'branch': 'release'}\n\ 84 | Plug 'bling/vim-airline'\n\ 85 | Plug 'sheerun/vim-polyglot'\n\ 86 | call plug#end()\n\ 87 | autocmd FileType ps1 setlocal shell=pwsh" >> ~/.config/nvim/init.vim 88 | 89 | RUN nvim +PlugInstall +qall 90 | RUN nvim -c 'CocInstall -sync coc-powershell coc-snippets coc-json|q' 91 | 92 | USER root 93 | RUN usermod -s /usr/bin/pwsh vim 94 | RUN usermod -s /usr/bin/pwsh nvim 95 | 96 | RUN rm -rf /tmp/* 97 | WORKDIR /tmp 98 | 99 | CMD ["/usr/bin/pwsh"] 100 | -------------------------------------------------------------------------------- /src/client/utils.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | "use strict"; 6 | 7 | import fs = require("fs"); 8 | import path = require("path"); 9 | import { ensurePathExists, checkIfFileExists } from 'coc-utils' 10 | 11 | export let PowerShellLanguageId = "powershell"; 12 | 13 | export interface IEditorServicesSessionDetails { 14 | status: string; 15 | reason: string; 16 | detail: string; 17 | powerShellVersion: string; 18 | channel: string; 19 | languageServicePort: number; 20 | debugServicePort: number; 21 | languageServicePipeName: string; 22 | debugServicePipeName: string; 23 | } 24 | 25 | export type IReadSessionFileCallback = (details: IEditorServicesSessionDetails) => void; 26 | export type IWaitForSessionFileCallback = (details: IEditorServicesSessionDetails, error: string) => void; 27 | 28 | const sessionsFolder = path.resolve(__dirname, "..", "..", "sessions/"); 29 | const sessionFilePathPrefix = path.resolve(sessionsFolder, "PSES-VSCode-" + process.env.VSCODE_PID); 30 | 31 | // Create the sessions path if it doesn't exist already 32 | ensurePathExists(sessionsFolder); 33 | 34 | export function getSessionFilePath(uniqueId: number) { 35 | return `${sessionFilePathPrefix}-${uniqueId}`; 36 | } 37 | 38 | export function getDebugSessionFilePath() { 39 | return `${sessionFilePathPrefix}-Debug`; 40 | } 41 | 42 | export function writeSessionFile(sessionFilePath: string, sessionDetails: IEditorServicesSessionDetails) { 43 | ensurePathExists(sessionsFolder); 44 | 45 | const writeStream = fs.createWriteStream(sessionFilePath); 46 | writeStream.write(JSON.stringify(sessionDetails)); 47 | writeStream.close(); 48 | } 49 | 50 | export function waitForSessionFile(sessionFilePath: string, callback: IWaitForSessionFileCallback) { 51 | 52 | function innerTryFunc(remainingTries: number, delayMilliseconds: number) { 53 | if (remainingTries === 0) { 54 | callback(undefined, "Timed out waiting for session file to appear."); 55 | } else if (!checkIfFileExists(sessionFilePath)) { 56 | // Wait a bit and try again 57 | setTimeout( 58 | () => { innerTryFunc(remainingTries - 1, delayMilliseconds); }, 59 | delayMilliseconds); 60 | } else { 61 | // Session file was found, load and return it 62 | callback(readSessionFile(sessionFilePath), undefined); 63 | } 64 | } 65 | 66 | // Try once every 2 seconds, 60 times - making two full minutes 67 | innerTryFunc(60, 2000); 68 | } 69 | 70 | export function readSessionFile(sessionFilePath: string): IEditorServicesSessionDetails { 71 | const fileContents = fs.readFileSync(sessionFilePath, "utf-8"); 72 | return JSON.parse(fileContents); 73 | } 74 | 75 | export function deleteSessionFile(sessionFilePath: string) { 76 | try { 77 | fs.unlinkSync(sessionFilePath); 78 | } catch (e) { 79 | // TODO: Be more specific about what we're catching 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /src/client/process.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import fs = require("fs"); 7 | import path = require("path"); 8 | import vscode = require("coc.nvim"); 9 | import Settings = require("./settings"); 10 | import utils = require("./utils"); 11 | import crypto = require("crypto"); 12 | import { isWindowsOS } from 'coc-utils' 13 | 14 | export class PowerShellProcess { 15 | public static escapeSingleQuotes(pspath: string): string { 16 | return pspath.replace(new RegExp("'", "g"), "''"); 17 | } 18 | 19 | public onExited: vscode.Event; 20 | private onExitedEmitter = new vscode.Emitter(); 21 | 22 | private consoleTerminal: vscode.Terminal = undefined; 23 | private consoleCloseSubscription: vscode.Disposable; 24 | private sessionFilePath: string 25 | private sessionDetails: utils.IEditorServicesSessionDetails; 26 | 27 | public log = vscode.workspace.createOutputChannel('powershell') 28 | private cocPowerShellRoot = path.join(__dirname, "..", ".."); 29 | private bundledModulesPath = path.join(this.cocPowerShellRoot, "PowerShellEditorServices"); 30 | 31 | constructor( 32 | private config: Settings.ISettings, 33 | private pwshPath: string, 34 | private title: string) { 35 | 36 | this.onExited = this.onExitedEmitter.event; 37 | } 38 | 39 | public async start(): Promise { 40 | 41 | // If PowerShellEditorServices is not downloaded yet, run the install script to do so. 42 | if (!fs.existsSync(this.bundledModulesPath)) { 43 | const errMessage = "[Error] PowerShell Editor Services not found. Package is not in the correct format." 44 | this.log.appendLine(errMessage); 45 | throw errMessage; 46 | } 47 | 48 | this.log.appendLine("starting.") 49 | this.log.appendLine(`pwshPath = ${this.pwshPath}`) 50 | this.log.appendLine(`bundledModulesPath = ${this.bundledModulesPath}`) 51 | 52 | let logDir = path.join(this.cocPowerShellRoot, `/.pses/logs/${crypto.randomBytes(16).toString("hex")}-${process.pid}`) 53 | this.sessionFilePath = path.join(logDir, "session") 54 | 55 | // Make sure no old session file exists 56 | utils.deleteSessionFile(this.sessionFilePath); 57 | 58 | let powerShellArgs: string[] = [] 59 | 60 | // Only add ExecutionPolicy param on Windows 61 | if (isWindowsOS()) { 62 | powerShellArgs.push("-ExecutionPolicy", "Bypass"); 63 | } 64 | 65 | // Make sure the log directory exists. PowerShell Editor Services needs this at the moment. 66 | if (!fs.existsSync(logDir)) { 67 | fs.mkdirSync(logDir, { 68 | recursive: true 69 | }); 70 | } 71 | 72 | powerShellArgs.push( 73 | "-NoProfile", 74 | "-NonInteractive", 75 | path.join(this.bundledModulesPath, "/PowerShellEditorServices/Start-EditorServices.ps1"), 76 | "-HostName", "coc.vim", 77 | "-HostProfileId", "coc.vim", 78 | "-HostVersion", "2.0.0", 79 | "-LogPath", path.join(logDir, "log.txt"), 80 | "-LogLevel", this.config.developer.editorServicesLogLevel || "Normal", 81 | "-BundledModulesPath", this.bundledModulesPath, 82 | "-EnableConsoleRepl", 83 | "-SessionDetailsPath", this.sessionFilePath) 84 | 85 | this.consoleTerminal = await vscode.workspace.createTerminal({ 86 | name: this.title, 87 | shellPath: this.pwshPath, 88 | shellArgs: powerShellArgs 89 | }) 90 | 91 | if (!this.config.integratedConsole.showOnStartup) { 92 | this.consoleTerminal.hide(); 93 | } 94 | 95 | await new Promise((resolve, reject) => { 96 | // Start the language client 97 | utils.waitForSessionFile( 98 | this.sessionFilePath, 99 | (sessionDetails, error) => { 100 | // Clean up the session file 101 | utils.deleteSessionFile(this.sessionFilePath); 102 | 103 | if (error) { 104 | reject(error); 105 | } else { 106 | this.sessionDetails = sessionDetails; 107 | resolve(this.sessionDetails); 108 | } 109 | }); 110 | }) 111 | 112 | this.consoleCloseSubscription = 113 | vscode.workspace.onDidCloseTerminal( 114 | (terminal) => { 115 | if (terminal === this.consoleTerminal) { 116 | this.log.appendLine("powershell.exe terminated or terminal UI was closed"); 117 | this.onExitedEmitter.fire(); 118 | } 119 | }, this); 120 | 121 | this.consoleTerminal.processId.then( 122 | (pid) => { this.log.appendLine(`powershell.exe started, pid: ${pid}`); }); 123 | 124 | return this.sessionDetails 125 | } 126 | 127 | public async showTerminalIfNotVisible() { 128 | if (this.consoleTerminal) { 129 | const winid: number = await vscode.workspace.nvim.eval(`bufwinid(${this.consoleTerminal.bufnr})`) as number; 130 | 131 | // Show terminal if it's hidden when running "execute" commands or if focusConsoleOnExecute, 132 | // this will cause the cursor to jump down into the terminal. 133 | if (this.config.integratedConsole.focusConsoleOnExecute || winid == -1) { 134 | this.consoleTerminal.show(); 135 | } 136 | } 137 | } 138 | 139 | public showTerminal() { 140 | this.consoleTerminal.show(); 141 | } 142 | 143 | public hideTerminal() { 144 | this.consoleTerminal.hide(); 145 | } 146 | 147 | public async toggleTerminal() { 148 | const winid: number = await vscode.workspace.nvim.eval(`bufwinid(${this.consoleTerminal.bufnr})`) as number; 149 | if (winid == -1) { 150 | this.consoleTerminal.show(); 151 | } else { 152 | this.consoleTerminal.hide(); 153 | } 154 | } 155 | 156 | public dispose() { 157 | 158 | // Clean up the session file 159 | utils.deleteSessionFile(this.sessionFilePath); 160 | 161 | if (this.consoleCloseSubscription) { 162 | this.consoleCloseSubscription.dispose(); 163 | this.consoleCloseSubscription = undefined; 164 | } 165 | 166 | if (this.consoleTerminal) { 167 | this.log.appendLine("Terminating PowerShell process..."); 168 | this.consoleTerminal.dispose(); 169 | this.consoleTerminal = undefined; 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/client/settings.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | "use strict"; 6 | 7 | import vscode = require("coc.nvim"); 8 | import utils = require("./utils"); 9 | import { isWindowsOS } from 'coc-utils' 10 | 11 | enum CodeFormattingPreset { 12 | Custom, 13 | Allman, 14 | OTBS, 15 | Stroustrup, 16 | } 17 | 18 | enum PipelineIndentationStyle { 19 | IncreaseIndentationForFirstPipeline, 20 | IncreaseIndentationAfterEveryPipeline, 21 | NoIndentation, 22 | } 23 | 24 | export enum HelpCompletion { 25 | Disabled = "Disabled", 26 | BlockComment = "BlockComment", 27 | LineComment = "LineComment", 28 | } 29 | 30 | export interface IPowerShellAdditionalExePathSettings { 31 | versionName: string; 32 | exePath: string; 33 | } 34 | 35 | export interface IBugReportingSettings { 36 | project: string; 37 | } 38 | 39 | export interface ICodeFoldingSettings { 40 | enable?: boolean; 41 | showLastLine?: boolean; 42 | } 43 | 44 | export interface ICodeFormattingSettings { 45 | autoCorrectAliases: boolean; 46 | preset: CodeFormattingPreset; 47 | openBraceOnSameLine: boolean; 48 | newLineAfterOpenBrace: boolean; 49 | newLineAfterCloseBrace: boolean; 50 | pipelineIndentationStyle: PipelineIndentationStyle; 51 | whitespaceBeforeOpenBrace: boolean; 52 | whitespaceBeforeOpenParen: boolean; 53 | whitespaceAroundOperator: boolean; 54 | whitespaceAfterSeparator: boolean; 55 | whitespaceBetweenParameters: boolean; 56 | whitespaceInsideBrace: boolean; 57 | addWhitespaceAroundPipe: boolean; 58 | trimWhitespaceAroundPipe: boolean; 59 | ignoreOneLineBlock: boolean; 60 | alignPropertyValuePairs: boolean; 61 | useConstantStrings: boolean; 62 | useCorrectCasing: boolean; 63 | } 64 | 65 | export interface IScriptAnalysisSettings { 66 | enable?: boolean; 67 | settingsPath: string; 68 | } 69 | 70 | export interface IDebuggingSettings { 71 | createTemporaryIntegratedConsole?: boolean; 72 | } 73 | 74 | export interface IDeveloperSettings { 75 | featureFlags?: string[]; 76 | powerShellExePath?: string; 77 | bundledModulesPath?: string; 78 | editorServicesLogLevel?: string; 79 | editorServicesWaitForDebugger?: boolean; 80 | powerShellExeIsWindowsDevBuild?: boolean; 81 | } 82 | 83 | export interface ISettings { 84 | powerShellAdditionalExePaths?: IPowerShellAdditionalExePathSettings[]; 85 | powerShellDefaultVersion?: string; 86 | powerShellExePath?: string; 87 | bundledModulesPath?: string; 88 | startAutomatically?: boolean; 89 | useX86Host?: boolean; 90 | enableProfileLoading?: boolean; 91 | helpCompletion: string; 92 | scriptAnalysis?: IScriptAnalysisSettings; 93 | debugging?: IDebuggingSettings; 94 | developer?: IDeveloperSettings; 95 | codeFolding?: ICodeFoldingSettings; 96 | codeFormatting?: ICodeFormattingSettings; 97 | integratedConsole?: IIntegratedConsoleSettings; 98 | bugReporting?: IBugReportingSettings; 99 | } 100 | 101 | export interface IIntegratedConsoleSettings { 102 | showOnStartup?: boolean; 103 | focusConsoleOnExecute?: boolean; 104 | executeInCurrentScope?: boolean; 105 | } 106 | 107 | export function load(): ISettings { 108 | const configuration: vscode.WorkspaceConfiguration = 109 | vscode.workspace.getConfiguration( 110 | utils.PowerShellLanguageId); 111 | 112 | const defaultBugReportingSettings: IBugReportingSettings = { 113 | project: "https://github.com/yatli/coc-powershell", 114 | }; 115 | 116 | const defaultScriptAnalysisSettings: IScriptAnalysisSettings = { 117 | enable: true, 118 | settingsPath: "", 119 | }; 120 | 121 | const defaultDebuggingSettings: IDebuggingSettings = { 122 | createTemporaryIntegratedConsole: false, 123 | }; 124 | 125 | // TODO: Remove when PSReadLine is out of preview 126 | const featureFlags = []; 127 | if (isWindowsOS()) { 128 | featureFlags.push("PSReadLine"); 129 | } 130 | 131 | const defaultDeveloperSettings: IDeveloperSettings = { 132 | featureFlags, 133 | powerShellExePath: undefined, 134 | bundledModulesPath: "../../../PowerShellEditorServices/module", 135 | editorServicesLogLevel: "Normal", 136 | editorServicesWaitForDebugger: false, 137 | powerShellExeIsWindowsDevBuild: false, 138 | }; 139 | 140 | const defaultCodeFoldingSettings: ICodeFoldingSettings = { 141 | enable: true, 142 | showLastLine: false, 143 | }; 144 | 145 | const defaultCodeFormattingSettings: ICodeFormattingSettings = { 146 | autoCorrectAliases: false, 147 | preset: CodeFormattingPreset.Custom, 148 | openBraceOnSameLine: true, 149 | newLineAfterOpenBrace: true, 150 | newLineAfterCloseBrace: true, 151 | pipelineIndentationStyle: PipelineIndentationStyle.NoIndentation, 152 | whitespaceBeforeOpenBrace: true, 153 | whitespaceBeforeOpenParen: true, 154 | whitespaceAroundOperator: true, 155 | whitespaceAfterSeparator: true, 156 | whitespaceBetweenParameters: false, 157 | whitespaceInsideBrace: true, 158 | addWhitespaceAroundPipe: true, 159 | trimWhitespaceAroundPipe: false, 160 | ignoreOneLineBlock: true, 161 | alignPropertyValuePairs: true, 162 | useConstantStrings: false, 163 | useCorrectCasing: false, 164 | }; 165 | 166 | const defaultIntegratedConsoleSettings: IIntegratedConsoleSettings = { 167 | showOnStartup: true, 168 | focusConsoleOnExecute: true, 169 | executeInCurrentScope: false, 170 | }; 171 | 172 | return { 173 | startAutomatically: 174 | configuration.get("startAutomatically", true), 175 | powerShellAdditionalExePaths: 176 | configuration.get("powerShellAdditionalExePaths", undefined), 177 | powerShellDefaultVersion: 178 | configuration.get("powerShellDefaultVersion", undefined), 179 | powerShellExePath: 180 | configuration.get("powerShellExePath", undefined), 181 | bundledModulesPath: 182 | "../../modules", 183 | useX86Host: 184 | configuration.get("useX86Host", false), 185 | enableProfileLoading: 186 | configuration.get("enableProfileLoading", false), 187 | helpCompletion: 188 | configuration.get("helpCompletion", HelpCompletion.BlockComment), 189 | scriptAnalysis: 190 | configuration.get("scriptAnalysis", defaultScriptAnalysisSettings), 191 | debugging: 192 | configuration.get("debugging", defaultDebuggingSettings), 193 | developer: 194 | getWorkspaceSettingsWithDefaults(configuration, "developer", defaultDeveloperSettings), 195 | codeFolding: 196 | configuration.get("codeFolding", defaultCodeFoldingSettings), 197 | codeFormatting: 198 | configuration.get("codeFormatting", defaultCodeFormattingSettings), 199 | integratedConsole: 200 | configuration.get("integratedConsole", defaultIntegratedConsoleSettings), 201 | bugReporting: 202 | configuration.get("bugReporting", defaultBugReportingSettings), 203 | }; 204 | } 205 | 206 | export async function change(settingName: string, newValue: any, global: boolean = false): Promise { 207 | const configuration: vscode.WorkspaceConfiguration = 208 | vscode.workspace.getConfiguration( 209 | utils.PowerShellLanguageId); 210 | 211 | await configuration.update(settingName, newValue, global); 212 | } 213 | 214 | function getWorkspaceSettingsWithDefaults( 215 | workspaceConfiguration: vscode.WorkspaceConfiguration, 216 | settingName: string, 217 | defaultSettings: TSettings): TSettings { 218 | 219 | const importedSettings: TSettings = workspaceConfiguration.get(settingName, defaultSettings); 220 | 221 | for (const setting in importedSettings) { 222 | if (importedSettings[setting]) { 223 | defaultSettings[setting] = importedSettings[setting]; 224 | } 225 | } 226 | return defaultSettings; 227 | } 228 | -------------------------------------------------------------------------------- /src/client/extension.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import * as net from 'net'; 8 | import { commands, workspace, ExtensionContext, events } from 'coc.nvim'; 9 | import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, StreamInfo } from 'coc.nvim'; 10 | import { fileURLToPath, getPlatformDetails } from 'coc-utils' 11 | import settings = require("./settings"); 12 | import * as process from './process'; 13 | import { EvaluateRequestMessage, IEvaluateRequestArguments } from "./messages"; 14 | import { PowerShellExeFinder, IPowerShellExeDetails } from './platform'; 15 | 16 | async function getSelectedTextToExecute(mode: string): Promise { 17 | let doc = workspace.getDocument(workspace.bufnr); 18 | if (!doc) return ""; 19 | 20 | if (mode === 'n') { 21 | // get whole line. 22 | let range = await workspace.getCursorPosition(); 23 | if (range) return doc.getline(range.line); 24 | } else { 25 | let range = await workspace.getSelectedRange(mode, doc); 26 | if (range) return doc.textDocument.getText(range); 27 | } 28 | 29 | return ""; 30 | } 31 | 32 | function startREPLProc(context: ExtensionContext, config: settings.ISettings, pwshPath: string, title: string) { 33 | return async () => { 34 | let proc = new process.PowerShellProcess(config, pwshPath, title) 35 | let sessionDetails = await proc.start() 36 | let socket = net.connect(sessionDetails.languageServicePipeName); 37 | let streamInfo = () => new Promise((resolve, __) => { 38 | socket.on( 39 | "connect", 40 | () => { 41 | proc.log.appendLine("Language service connected."); 42 | resolve({writer: socket, reader: socket}); 43 | }); 44 | }); 45 | 46 | 47 | // Options to control the language client 48 | let clientOptions: LanguageClientOptions = { 49 | // Register the server for powershell documents 50 | documentSelector: [{ scheme: 'file', language: 'ps1' }], 51 | synchronize: { 52 | // Synchronize the setting section 'powershell' to the server 53 | configurationSection: 'powershell', 54 | // Notify the server about file changes to PowerShell files contain in the workspace 55 | fileEvents: [ 56 | workspace.createFileSystemWatcher('**/*.ps1'), 57 | workspace.createFileSystemWatcher('**/*.psd1'), 58 | workspace.createFileSystemWatcher('**/*.psm1') 59 | ] 60 | } 61 | } 62 | 63 | // Create the language client and start the client. 64 | let client = new LanguageClient('ps1', 'PowerShell Language Server', streamInfo, clientOptions); 65 | let disposable = client.start(); 66 | 67 | let doEval = async function(mode: string) { 68 | let document = await workspace.document 69 | if (!document || document.filetype !== 'ps1') { 70 | return 71 | } 72 | 73 | const content = await getSelectedTextToExecute(mode); 74 | 75 | const evaluateArgs: IEvaluateRequestArguments = { 76 | expression: content, 77 | } 78 | client.sendRequest(EvaluateRequestMessage, evaluateArgs) 79 | 80 | await proc.showTerminalIfNotVisible(); 81 | } 82 | 83 | 84 | let cmdShowTerminal = commands.registerCommand("powershell.showTerminal", () => proc.showTerminal()); 85 | let cmdHideTerminal = commands.registerCommand("powershell.hideTerminal", () => proc.hideTerminal()); 86 | let cmdToggleTerminal = commands.registerCommand("powershell.toggleTerminal", () => proc.toggleTerminal()); 87 | 88 | let cmdEvalLine = commands.registerCommand("powershell.evaluateLine", async () => doEval('n')); 89 | let cmdEvalSelection = commands.registerCommand("powershell.evaluateSelection", async () => doEval('v')); 90 | let cmdExecFile = commands.registerCommand("powershell.execute", async (...args: any[]) => { 91 | let document = await workspace.document 92 | if (!document || document.filetype !== 'ps1') { 93 | return; 94 | } 95 | 96 | if(document.schema === "untitled") { 97 | workspace.showMessage( 98 | "Can't run file because it's an in-memory file. Save the contents to a file and try again.", 99 | 'error'); 100 | return; 101 | } 102 | 103 | let argStrs = args 104 | ? args.map(x => `${x}`) 105 | : [] 106 | 107 | let filePath = fileURLToPath(document.uri) 108 | proc.log.appendLine(`executing: ${filePath}`) 109 | 110 | // Escape single quotes by adding a second single quote. 111 | if(filePath.indexOf('\'') !== -1) { 112 | filePath = filePath.replace(/'/, '\'\'') 113 | } 114 | 115 | // workaround until document.dirty works 116 | if (Number.parseInt(await workspace.nvim.commandOutput("echo &modified"))) { 117 | if(! await workspace.showPrompt("Your file will be saved first before it runs. Is that ok?")) { 118 | return; 119 | } 120 | // workaround until document.textDocument.save() is supported. 121 | await workspace.nvim.command('w'); 122 | } 123 | 124 | const config = settings.load(); 125 | const exeChar = config.integratedConsole.executeInCurrentScope ? "." : "&"; 126 | const evaluateArgs: IEvaluateRequestArguments = { 127 | expression: `${exeChar} '${filePath}'`, 128 | }; 129 | 130 | await client.sendRequest(EvaluateRequestMessage, evaluateArgs); 131 | await proc.showTerminalIfNotVisible(); 132 | }) 133 | 134 | // Push the disposable to the context's subscriptions so that the 135 | // client can be deactivated on extension deactivation 136 | context.subscriptions.push(disposable, cmdExecFile, cmdEvalLine, cmdEvalSelection, cmdShowTerminal, cmdHideTerminal, cmdToggleTerminal ); 137 | 138 | return proc.onExited 139 | } 140 | } 141 | 142 | export async function activate(context: ExtensionContext) { 143 | 144 | let config = settings.load() 145 | 146 | const powershellExeFinder = new PowerShellExeFinder( 147 | getPlatformDetails(), 148 | config.powerShellAdditionalExePaths); 149 | 150 | let pwshPath = config.powerShellExePath; 151 | 152 | try { 153 | if (config.powerShellDefaultVersion) { 154 | for (const details of powershellExeFinder.enumeratePowerShellInstallations()) { 155 | // Need to compare names case-insensitively, from https://stackoverflow.com/a/2140723 156 | const wantedName = config.powerShellDefaultVersion; 157 | if (wantedName.localeCompare(details.displayName, undefined, { sensitivity: "accent" }) === 0) { 158 | pwshPath = details.exePath; 159 | break; 160 | } 161 | } 162 | } 163 | 164 | pwshPath = pwshPath || 165 | powershellExeFinder.getFirstAvailablePowerShellInstallation().exePath; 166 | 167 | } catch (e) { 168 | this.log.writeError(`Error occurred while searching for a PowerShell executable:\n${e}`); 169 | return; 170 | } 171 | 172 | // Status bar entry showing PS version 173 | let versionBarItem = workspace.createStatusBarItem(0, {progress: false}) 174 | versionBarItem.text = pwshPath.indexOf("powershell.exe") >= 0 175 | ? "PS-Desktop" 176 | : "PS-Core" 177 | versionBarItem.show() 178 | 179 | events.on('BufEnter', async () => { 180 | let document = await workspace.document 181 | if (!document) { 182 | versionBarItem.hide() 183 | return 184 | } 185 | 186 | if (document.filetype === 'ps1') { 187 | versionBarItem.show() 188 | } else { 189 | versionBarItem.hide() 190 | } 191 | }) 192 | 193 | let fnproc = startREPLProc(context, config, pwshPath, "PowerShell REPL") 194 | 195 | let daemon = async function() { 196 | let onExit = await fnproc() 197 | onExit(async () => { await daemon() }) 198 | } 199 | 200 | await daemon() 201 | } 202 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Shamelessly taken from the [coc.nvim](https://github.com/neoclide/coc.nvim) project with sincere thanks. 4 | For the purposes of this document, we will use `vim` to refer to either [vim](https://www.vim.org) or [neovim](https://neovim.io). `vimrc` will refer to the vimrc or init.vim wherever it may reside. 5 | 6 | ## How do I... 7 | 8 | - [Use This Guide](#introduction)? 9 | - Make Something? 🤓👩🏽‍💻📜🍳 10 | - [Project Setup](#project-setup) 11 | - [Contribute Documentation](#contribute-documentation) 12 | - [Contribute Code](#contribute-code) 13 | - Manage Something ✅🙆🏼💃👔 14 | - [Provide Support on Issues](#provide-support-on-issues) 15 | - [Review Pull Requests](#review-pull-requests) 16 | - [Join the Project Team](#join-the-project-team) 17 | 18 | ## Introduction 19 | 20 | Thank you so much for your interest in contributing!. All types of contributions are encouraged and valued. See the [table of contents](#toc) for different ways to help and details about how this project handles them!📝 21 | 22 | The [Project Team](#join-the-project-team) looks forward to your contributions. 🙌🏾✨ 23 | 24 | ## Project Setup 25 | 26 | So you wanna contribute some code! That's great! This project uses GitHub Pull Requests to manage contributions, so [read up on how to fork a GitHub project and file a PR](https://guides.github.com/activities/forking) if you've never done it before. 27 | 28 | If this seems like a lot or you aren't able to do all this setup, you might also be able to [edit the files directly](https://help.github.com/articles/editing-files-in-another-user-s-repository/) without having to do any of this setup. Yes, [even code](#contribute-code). 29 | 30 | ### Run Project Locally 31 | 32 | If you want to go the usual route and run the project locally, though: 33 | 34 | - [Install PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell) 35 | - [Install Node.js](https://nodejs.org/en/download/) 36 | - [Install npm](https://npmjs.com) 37 | - [Fork the project](https://guides.github.com/activities/forking/#fork) 38 | - In your PowerShell terminal 39 | - `cd path/to/your/coc-powershell` 40 | - `./build.ps1` 41 | - Edit your vimrc 42 | - Add `set runtimepath^=/path/to/coc-powershell` 43 | - Launch vim 44 | - Verify `coc-powershell` has been installed with `:CocList extension` 45 | - `coc-powershell` should show with `[RTP]` and `/path/to/coc-powershell` 46 | - If you've installed coc-powershell with `:CocInstall` you may need to `:CocUninstall coc-powershell` then reload vim 47 | 48 | And you should be ready to go! 49 | 50 | ## Contribute Documentation 51 | 52 | Documentation is a super important, critical part of this project. Docs are how we keep track of what we're doing, how, and why. It's how we stay on the same page about our policies. And it's how we tell others everything they need in order to be able to use this project -- or contribute to it. So thank you in advance. 53 | 54 | Documentation contributions of any size are welcome! Feel free to file a PR even if you're just rewording a sentence to be more clear, or fixing a spelling mistake! 55 | 56 | To contribute documentation: 57 | 58 | - [Set up the project](#project-setup). 59 | - Edit or add any relevant documentation. 60 | - Make sure your changes are formatted correctly and consistently with the rest of the documentation. 61 | - Re-read what you wrote, and run a spellchecker on it to make sure you didn't miss anything. 62 | - In your commit message(s), begin the first line with `docs:`. For example: `docs: Adding a doc contrib section to CONTRIBUTING.md`. 63 | - Write clear, concise commit message(s) using [conventional-changelog format](https://github.com/conventional-changelog/conventional-changelog-angular/blob/master/convention.md). Documentation commits should use `docs(): `. 64 | - Go to https://github.com/yatli/coc-powershell/pulls and open a new pull request with your changes. 65 | - If your PR is connected to an open issue, add a line in your PR's description that says `Fixes: #123`, where `#123` is the number of the issue you're fixing. 66 | 67 | ## Contribute Code 68 | 69 | We like code commits a lot! They're super handy, and they keep the project going and doing the work it needs to do to be useful to others. 70 | 71 | Code contributions of just about any size are acceptable! 72 | 73 | The main difference between code contributions and documentation contributions is that contributing code encourages the inclusion of relevant tests for the code being added or changed. 74 | 75 | To contribute code: 76 | 77 | - [Set up the project](#project-setup). 78 | - Make any necessary changes to the source code. 79 | - Include any [additional documentation](#contribute-documentation) the changes might need. 80 | - Write tests that verify that your contribution works as expected when necessary. 81 | - Dependency updates, additions, or removals should be in individual commits. 82 | - Go to https://github.com/yatli/coc-powershell/pulls and open a new pull request with your changes. 83 | - If your PR is connected to an open issue, add a line in your PR's description that says `Fixes: #123`, where `#123` is the number of the issue you're fixing. 84 | 85 | Once you've filed the PR: 86 | 87 | - Barring special circumstances, maintainers will not review PRs until all checks pass (Travis, AppVeyor, etc). 88 | - One or more maintainers will use GitHub's review feature to review your PR. 89 | - If the maintainer asks for any changes, edit your changes, push, and ask for another review. Additional tags (such as `needs-tests`) will be added depending on the review. 90 | - If the maintainer decides to pass on your PR, they will thank you for the contribution and explain why they won't be accepting the changes. That's ok! We still really appreciate you taking the time to do it, and we don't take that lightly. 💚 91 | - If your PR gets accepted, it will be marked as such, and merged into the `latest` branch soon after. Your contribution will be distributed to the masses next time the maintainers [tag a release](#tag-a-release) 92 | 93 | ## Provide Support on Issues 94 | 95 | [Needs Collaborator](#join-the-project-team): none 96 | 97 | Helping out other users with their questions is a really awesome way of contributing to any community. It's not uncommon for most of the issues on an open source projects being support-related questions by users trying to understand something they ran into, or find their way around a known bug. 98 | 99 | Sometimes, the `support` label will be added to things that turn out to actually be other things, like bugs or feature requests. In that case, suss out the details with the person who filed the original issue, add a comment explaining what the bug is, and change the label from `support` to `bug` or `feature`. If you can't do this yourself, @mention a maintainer so they can do it. 100 | 101 | In order to help other folks out with their questions: 102 | 103 | - Read through the list until you find something that you're familiar enough with to give an answer to. 104 | - Respond to the issue with whatever details are needed to clarify the question, or get more details about what's going on. 105 | - Once the discussion wraps up and things are clarified, either close the issue, or ask the original issue filer (or a maintainer) to close it for you. 106 | 107 | Some notes on picking up support issues: 108 | 109 | - Avoid responding to issues you don't know you can answer accurately. 110 | - As much as possible, try to refer to past issues with accepted answers. Link to them from your replies with the `#123` format. 111 | - Be kind and patient with users -- often, folks who have run into confusing things might be upset or impatient. This is ok. Try to understand where they're coming from, and if you're too uncomfortable with the tone, feel free to stay away or withdraw from the issue. (note: if the user is outright hostile or is violating the CoC, [refer to the Code of Conduct](CODE_OF_CONDUCT.md) to resolve the conflict). 112 | 113 | ## Review Pull Requests 114 | 115 | [Needs Collaborator](#join-the-project-team): Issue Tracker 116 | 117 | While anyone can comment on a PR, add feedback, etc, PRs are only _approved_ by team members with Issue Tracker or higher permissions. 118 | 119 | PR reviews use [GitHub's own review feature](https://help.github.com/articles/about-pull-request-reviews/), which manages comments, approval, and review iteration. 120 | 121 | Some notes: 122 | 123 | - You may ask for minor changes ("nitpicks"), but consider whether they are really blockers to merging: try to err on the side of "approve, with comments". 124 | - _ALL PULL REQUESTS_ should be covered by a test: either by a previously-failing test, an existing test that covers the entire functionality of the submitted code, or new tests to verify any new/changed behavior. All tests must also pass and follow established conventions. Test coverage should not drop, unless the specific case is considered reasonable by maintainers. 125 | - Please make sure you're familiar with the code or documentation being updated, unless it's a minor change (spellchecking, minor formatting, etc). You may @mention another project member who you think is better suited for the review, but still provide a non-approving review of your own. 126 | - Be extra kind: people who submit code/doc contributions are putting themselves in a pretty vulnerable position, and have put time and care into what they've done (even if that's not obvious to you!) -- always respond with respect, be understanding, but don't feel like you need to sacrifice your standards for their sake, either. Just don't be a jerk about it? 127 | 128 | ## Join the Project Team 129 | 130 | ### Ways to Join 131 | 132 | There are many ways to contribute! Most of them don't require any official status unless otherwise noted. That said, there's a couple of positions that grant special repository abilities, and this section describes how they're granted and what they do. 133 | 134 | All of the below positions are granted based on the project team's needs, as well as their consensus opinion about whether they would like to work with the person and think that they would fit well into that position. The process is relatively informal, and it's likely that people who express interest in participating can just be granted the permissions they'd like. 135 | 136 | You can spot a collaborator on the repo by looking for the `[Collaborator]` or `[Owner]` tags next to their names. 137 | 138 | | Permission | Description | 139 | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 140 | | Issue Tracker | Granted to contributors who express a strong interest in spending time on the project's issue tracker. These tasks are mainly [labeling issues](#label-issues), [cleaning up old ones](#clean-up-issues-and-prs), and [reviewing pull requests](#review-pull-requests), as well as all the usual things non-team-member contributors can do. Issue handlers should not merge pull requests, tag releases, or directly commit code themselves: that should still be done through the usual pull request process. Becoming an Issue Handler means the project team trusts you to understand enough of the team's process and context to implement it on the issue tracker. | 141 | | Committer | Granted to contributors who want to handle the actual pull request merges, tagging new versions, etc. Committers should have a good level of familiarity with the codebase, and enough context to understand the implications of various changes, as well as a good sense of the will and expectations of the project team. | 142 | | Admin/Owner | Granted to people ultimately responsible for the project, its community, etc. | 143 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coc-powershell", 3 | "displayName": "PowerShell Language Server for coc.nvim", 4 | "description": "PowerShell Language Support using PowerShell Editor Services", 5 | "author": "Yatao Li, Tyler Leonhardt, Cory Knox", 6 | "license": "MIT", 7 | "readme": "README.md", 8 | "version": "0.1.4", 9 | "publisher": "yatli, tylerleonhardt, corbob", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/coc-extensions/coc-powershell" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/coc-extensions/coc-powershell/issues" 16 | }, 17 | "homepage": "https://github.com/coc-extensions/coc-powershell#readme", 18 | "engines": { 19 | "coc": ">=0.0.77" 20 | }, 21 | "keywords": [ 22 | "pwsh", 23 | "powershell", 24 | "coc.nvim", 25 | "dotnet", 26 | ".NET Core", 27 | ".NET" 28 | ], 29 | "categories": [ 30 | "Programming Languages", 31 | "Linters", 32 | "coc.nvim" 33 | ], 34 | "activationEvents": [ 35 | "onLanguage:ps1", 36 | "onLanguage:powershell", 37 | "onLanguage:PowerShell", 38 | "onLanguage:Powershell", 39 | "onLanguage:pwsh" 40 | ], 41 | "main": "out/client/extension.js", 42 | "files": [ 43 | "out", 44 | "syntaxes", 45 | "src/downloadPSES.ps1", 46 | "README.md", 47 | "LICENSE", 48 | "Snippets", 49 | "PowerShellEditorServices" 50 | ], 51 | "contributes": { 52 | "snippets": [ 53 | { 54 | "language": "ps1", 55 | "path": "Snippets/PowerShell.json" 56 | } 57 | ], 58 | "languages": [ 59 | { 60 | "id": "ps1", 61 | "aliases": [ 62 | "PowerShell", 63 | "pwsh" 64 | ], 65 | "extensions": [ 66 | ".ps1", 67 | ".psm1", 68 | ".psd1" 69 | ] 70 | } 71 | ], 72 | "grammars": [], 73 | "configuration": { 74 | "type": "object", 75 | "title": "PowerShell Configuration", 76 | "properties": { 77 | "powershell.sideBar.CommandExplorerVisibility": { 78 | "type": "boolean", 79 | "default": true, 80 | "description": "Specifies the visibility of the Command Explorer in the PowerShell Side Bar." 81 | }, 82 | "powershell.sideBar.CommandExplorerExcludeFilter": { 83 | "type": "array", 84 | "default": [], 85 | "description": "Specify array of Modules to exclude from Command Explorer listing." 86 | }, 87 | "powershell.powerShellExePath": { 88 | "type": "string", 89 | "default": "", 90 | "isExecutable": true, 91 | "description": "Specifies the full path to a PowerShell executable. Changes the installation of PowerShell used for language and debugging services." 92 | }, 93 | "powershell.powerShellAdditionalExePaths": { 94 | "type": "array", 95 | "description": "Specifies an array of versionName / exePath pairs where exePath points to a non-standard install location for PowerShell and versionName can be used to reference this path with the powershell.powerShellDefaultVersion setting.", 96 | "isExecutable": true, 97 | "uniqueItems": true, 98 | "items": { 99 | "type": "object", 100 | "required": [ 101 | "versionName", 102 | "exePath" 103 | ], 104 | "properties": { 105 | "versionName": { 106 | "type": "string", 107 | "description": "Specifies the version name of this PowerShell executable. The version name can be referenced via the powershell.powerShellDefaultVersion setting." 108 | }, 109 | "exePath": { 110 | "type": "string", 111 | "description": "Specifies the path to the PowerShell executable. Typically this is a path to a non-standard install location." 112 | } 113 | } 114 | } 115 | }, 116 | "powershell.powerShellDefaultVersion": { 117 | "type": "string", 118 | "description": "Specifies the PowerShell version name, as displayed by the 'PowerShell: Show Session Menu' command, used when the extension loads e.g \"Windows PowerShell (x86)\" or \"PowerShell Core 6 (x64)\"." 119 | }, 120 | "powershell.startAutomatically": { 121 | "type": "boolean", 122 | "default": true, 123 | "description": "Starts PowerShell extension features automatically when a PowerShell file opens. If false, to start the extension, use the 'PowerShell: Restart Current Session' command. IntelliSense, code navigation, integrated console, code formatting, and other features are not enabled until the extension starts." 124 | }, 125 | "powershell.useX86Host": { 126 | "type": "boolean", 127 | "default": false, 128 | "description": "Uses the 32-bit language service on 64-bit Windows. This setting has no effect on 32-bit Windows or on the PowerShell extension debugger, which has its own architecture configuration." 129 | }, 130 | "powershell.enableProfileLoading": { 131 | "type": "boolean", 132 | "default": true, 133 | "description": "Loads user and system-wide PowerShell profiles (profile.ps1 and Microsoft.VSCode_profile.ps1) into the PowerShell session. This affects IntelliSense and interactive script execution, but it does not affect the debugger." 134 | }, 135 | "powershell.bugReporting.project": { 136 | "type": "string", 137 | "default": "https://github.com/PowerShell/vscode-powershell", 138 | "description": "Specifies the url of the GitHub project in which to generate bug reports." 139 | }, 140 | "powershell.helpCompletion": { 141 | "type": "string", 142 | "enum": [ 143 | "Disabled", 144 | "BlockComment", 145 | "LineComment" 146 | ], 147 | "default": "BlockComment", 148 | "description": "Controls the comment-based help completion behavior triggered by typing '##'. Set the generated help style with 'BlockComment' or 'LineComment'. Disable the feature with 'Disabled'." 149 | }, 150 | "powershell.scriptAnalysis.enable": { 151 | "type": "boolean", 152 | "default": true, 153 | "description": "Enables real-time script analysis from PowerShell Script Analyzer. Uses the newest installed version of the PSScriptAnalyzer module or the version bundled with this extension, if it is newer." 154 | }, 155 | "powershell.scriptAnalysis.settingsPath": { 156 | "type": "string", 157 | "default": "", 158 | "description": "Specifies the path to a PowerShell Script Analyzer settings file. To override the default settings for all projects, enter an absolute path, or enter a path relative to your workspace." 159 | }, 160 | "powershell.codeFolding.enable": { 161 | "type": "boolean", 162 | "default": true, 163 | "description": "Enables syntax based code folding. When disabled, the default indentation based code folding is used." 164 | }, 165 | "powershell.codeFolding.showLastLine": { 166 | "type": "boolean", 167 | "default": true, 168 | "description": "Shows the last line of a folded section similar to the default VSCode folding style. When disabled, the entire folded region is hidden." 169 | }, 170 | "powershell.codeFormatting.autoCorrectAliases": { 171 | "type": "boolean", 172 | "default": false, 173 | "description": "Replaces aliases with their aliased name." 174 | }, 175 | "powershell.codeFormatting.preset": { 176 | "type": "string", 177 | "enum": [ 178 | "Custom", 179 | "Allman", 180 | "OTBS", 181 | "Stroustrup" 182 | ], 183 | "default": "Custom", 184 | "description": "Sets the codeformatting options to follow the given indent style in a way that is compatible with PowerShell syntax. For more information about the brace styles please refer to https://github.com/PoshCode/PowerShellPracticeAndStyle/issues/81." 185 | }, 186 | "powershell.codeFormatting.openBraceOnSameLine": { 187 | "type": "boolean", 188 | "default": true, 189 | "description": "Places open brace on the same line as its associated statement." 190 | }, 191 | "powershell.codeFormatting.newLineAfterOpenBrace": { 192 | "type": "boolean", 193 | "default": true, 194 | "description": "Adds a newline (line break) after an open brace." 195 | }, 196 | "powershell.codeFormatting.newLineAfterCloseBrace": { 197 | "type": "boolean", 198 | "default": true, 199 | "description": "Adds a newline (line break) after a closing brace." 200 | }, 201 | "powershell.codeFormatting.pipelineIndentationStyle": { 202 | "type": "string", 203 | "enum": [ 204 | "IncreaseIndentationForFirstPipeline", 205 | "IncreaseIndentationAfterEveryPipeline", 206 | "NoIndentation", 207 | "None" 208 | ], 209 | "default": "NoIndentation", 210 | "description": "Multi-line pipeline style settings (default: NoIndentation)." 211 | }, 212 | "powershell.codeFormatting.whitespaceBeforeOpenBrace": { 213 | "type": "boolean", 214 | "default": true, 215 | "description": "Adds a space between a keyword and its associated scriptblock expression." 216 | }, 217 | "powershell.codeFormatting.whitespaceBeforeOpenParen": { 218 | "type": "boolean", 219 | "default": true, 220 | "description": "Adds a space between a keyword (if, elseif, while, switch, etc) and its associated conditional expression." 221 | }, 222 | "powershell.codeFormatting.whitespaceAroundOperator": { 223 | "type": "boolean", 224 | "default": true, 225 | "description": "Adds spaces before and after an operator ('=', '+', '-', etc.)." 226 | }, 227 | "powershell.codeFormatting.whitespaceAfterSeparator": { 228 | "type": "boolean", 229 | "default": true, 230 | "description": "Adds a space after a separator (',' and ';')." 231 | }, 232 | "powershell.codeFormatting.whitespaceInsideBrace": { 233 | "type": "boolean", 234 | "default": true, 235 | "description": "Adds a space after an opening brace ('{') and before a closing brace ('}')." 236 | }, 237 | "powershell.codeFormatting.whitespaceBetweenParameters": { 238 | "type": "boolean", 239 | "default": false, 240 | "description": "Removes redundant whitespace between parameters." 241 | }, 242 | "powershell.codeFormatting.addWhitespaceAroundPipe": { 243 | "type": "boolean", 244 | "default": true, 245 | "description": "Adds a space before and after the pipeline operator ('|') if it is missing." 246 | }, 247 | "powershell.codeFormatting.trimWhitespaceAroundPipe": { 248 | "type": "boolean", 249 | "default": false, 250 | "description": "Trims extraneous whitespace (more than 1 character) before and after the pipeline operator ('|')." 251 | }, 252 | "powershell.codeFormatting.ignoreOneLineBlock": { 253 | "type": "boolean", 254 | "default": true, 255 | "description": "Does not reformat one-line code blocks, such as \"if (...) {...} else {...}\"." 256 | }, 257 | "powershell.codeFormatting.alignPropertyValuePairs": { 258 | "type": "boolean", 259 | "default": true, 260 | "description": "Align assignment statements in a hashtable or a DSC Configuration." 261 | }, 262 | "powershell.codeFormatting.useConstantStrings": { 263 | "type": "boolean", 264 | "default": false, 265 | "description": "Use single quotes if a string is not interpolated and its value does not contain a single quote." 266 | }, 267 | "powershell.codeFormatting.useCorrectCasing": { 268 | "type": "boolean", 269 | "default": false, 270 | "description": "Use correct casing for cmdlets." 271 | }, 272 | "powershell.integratedConsole.showOnStartup": { 273 | "type": "boolean", 274 | "default": true, 275 | "description": "Shows the integrated console when the PowerShell extension is initialized." 276 | }, 277 | "powershell.integratedConsole.focusConsoleOnExecute": { 278 | "type": "boolean", 279 | "default": true, 280 | "description": "Switches focus to the console when a script selection is run or a script file is debugged. This is an accessibility feature. To disable it, set to false." 281 | }, 282 | "powershell.integratedConsole.executeInCurrentScope": { 283 | "type": "boolean", 284 | "default": false, 285 | "description": "Decides whether or not to use the call operator `& script.ps1` (default) or the dot source operator `. script.ps1` to run the script using the `powershell.execute` command." 286 | }, 287 | "powershell.debugging.createTemporaryIntegratedConsole": { 288 | "type": "boolean", 289 | "default": false, 290 | "description": "Determines whether a temporary PowerShell Integrated Console is created for each debugging session, useful for debugging PowerShell classes and binary modules." 291 | }, 292 | "powershell.developer.bundledModulesPath": { 293 | "type": "string", 294 | "description": "Specifies an alternate path to the folder containing modules that are bundled with the PowerShell extension (i.e. PowerShell Editor Services, PSScriptAnalyzer, Plaster)" 295 | }, 296 | "powershell.developer.editorServicesLogLevel": { 297 | "type": "string", 298 | "enum": [ 299 | "Diagnostic", 300 | "Verbose", 301 | "Normal", 302 | "Warning", 303 | "Error" 304 | ], 305 | "default": "Normal", 306 | "description": "Sets the logging verbosity level for the PowerShell Editor Services host executable. Valid values are 'Diagnostic', 'Verbose', 'Normal', 'Warning', and 'Error'" 307 | }, 308 | "powershell.developer.editorServicesWaitForDebugger": { 309 | "type": "boolean", 310 | "default": false, 311 | "description": "Launches the language service with the /waitForDebugger flag to force it to wait for a .NET debugger to attach before proceeding." 312 | }, 313 | "powershell.developer.featureFlags": { 314 | "type": "array", 315 | "default": null, 316 | "description": "An array of strings that enable experimental features in the PowerShell extension." 317 | }, 318 | "powershell.developer.powerShellExeIsWindowsDevBuild": { 319 | "type": "boolean", 320 | "default": false, 321 | "description": "Indicates that the powerShellExePath points to a developer build of Windows PowerShell and configures it for development." 322 | }, 323 | "powershell.developer.powerShellExePath": { 324 | "type": "string", 325 | "default": "", 326 | "isExecutable": true, 327 | "description": "Deprecated. Please use the 'powershell.powerShellExePath' setting instead" 328 | } 329 | } 330 | }, 331 | "rootPatterns": [ 332 | { 333 | "filetype": "ps1", 334 | "patterns": [ 335 | "*.ps1", 336 | "*.psd1", 337 | "*.psm1", 338 | ".vim", 339 | ".git", 340 | ".hg" 341 | ] 342 | } 343 | ] 344 | }, 345 | "scripts": { 346 | "compile": "tsc -p ./", 347 | "watch": "tsc -watch -p ./" 348 | }, 349 | "extensionDependencies": [], 350 | "dependencies": { 351 | "coc-utils": "0.0.16" 352 | }, 353 | "devDependencies": { 354 | "@types/follow-redirects": "^1.8.0", 355 | "@types/node": "~10.17.19", 356 | "typescript": "~4.2.2", 357 | "coc.nvim": "~0.0.77" 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/client/platform.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | import * as fs from "fs"; 6 | import * as os from "os"; 7 | import * as path from "path"; 8 | import * as process from "process"; 9 | import { IPowerShellAdditionalExePathSettings } from "./settings"; 10 | 11 | const WindowsPowerShell64BitLabel = "Windows PowerShell (x64)"; 12 | const WindowsPowerShell32BitLabel = "Windows PowerShell (x86)"; 13 | 14 | const LinuxExePath = "/usr/bin/pwsh"; 15 | const LinuxPreviewExePath = "/usr/bin/pwsh-preview"; 16 | 17 | const SnapExePath = "/snap/bin/pwsh"; 18 | const SnapPreviewExePath = "/snap/bin/pwsh-preview"; 19 | 20 | const MacOSExePath = "/usr/local/bin/pwsh"; 21 | const MacOSPreviewExePath = "/usr/local/bin/pwsh-preview"; 22 | 23 | export enum OperatingSystem { 24 | Unknown, 25 | Windows, 26 | MacOS, 27 | Linux, 28 | } 29 | 30 | export interface IPlatformDetails { 31 | operatingSystem: OperatingSystem; 32 | isOS64Bit: boolean; 33 | isProcess64Bit: boolean; 34 | } 35 | 36 | export interface IPowerShellExeDetails { 37 | readonly displayName: string; 38 | readonly exePath: string; 39 | } 40 | 41 | export function getPlatformDetails(): IPlatformDetails { 42 | let operatingSystem = OperatingSystem.Unknown; 43 | 44 | if (process.platform === "win32") { 45 | operatingSystem = OperatingSystem.Windows; 46 | } else if (process.platform === "darwin") { 47 | operatingSystem = OperatingSystem.MacOS; 48 | } else if (process.platform === "linux") { 49 | operatingSystem = OperatingSystem.Linux; 50 | } 51 | 52 | const isProcess64Bit = process.arch === "x64"; 53 | 54 | return { 55 | operatingSystem, 56 | isOS64Bit: isProcess64Bit || process.env.hasOwnProperty("PROCESSOR_ARCHITEW6432"), 57 | isProcess64Bit, 58 | }; 59 | } 60 | 61 | /** 62 | * Class to lazily find installed PowerShell executables on a machine. 63 | * When given a list of additional PowerShell executables, 64 | * this will also surface those at the end of the list. 65 | */ 66 | export class PowerShellExeFinder { 67 | // This is required, since parseInt("7-preview") will return 7. 68 | private static IntRegex: RegExp = /^\d+$/; 69 | 70 | private static PwshMsixRegex: RegExp = /^Microsoft.PowerShell_.*/; 71 | 72 | private static PwshPreviewMsixRegex: RegExp = /^Microsoft.PowerShellPreview_.*/; 73 | 74 | // The platform details descriptor for the platform we're on 75 | private readonly platformDetails: IPlatformDetails; 76 | 77 | // Additional configured PowerShells 78 | private readonly additionalPSExeSettings: Iterable; 79 | 80 | private winPS: IPossiblePowerShellExe; 81 | 82 | private alternateBitnessWinPS: IPossiblePowerShellExe; 83 | 84 | /** 85 | * Create a new PowerShellFinder object to discover PowerShell installations. 86 | * @param platformDetails Information about the machine we are running on. 87 | * @param additionalPowerShellExes Additional PowerShell installations as configured in the settings. 88 | */ 89 | constructor( 90 | platformDetails?: IPlatformDetails, 91 | additionalPowerShellExes?: Iterable) { 92 | 93 | this.platformDetails = platformDetails || getPlatformDetails(); 94 | this.additionalPSExeSettings = additionalPowerShellExes || []; 95 | } 96 | 97 | /** 98 | * Returns the first available PowerShell executable found in the search order. 99 | */ 100 | public getFirstAvailablePowerShellInstallation(): IPowerShellExeDetails { 101 | for (const pwsh of this.enumeratePowerShellInstallations()) { 102 | return pwsh; 103 | } 104 | 105 | throw new Error("No available PowerShell executables on this machine."); 106 | } 107 | 108 | /** 109 | * Get an array of all PowerShell executables found when searching for PowerShell installations. 110 | */ 111 | public getAllAvailablePowerShellInstallations(): IPowerShellExeDetails[] { 112 | return Array.from(this.enumeratePowerShellInstallations()); 113 | } 114 | 115 | /** 116 | * Fixes PowerShell paths when Windows PowerShell is set to the non-native bitness. 117 | * @param configuredPowerShellPath the PowerShell path configured by the user. 118 | */ 119 | public fixWindowsPowerShellPath(configuredPowerShellPath: string): string { 120 | const altWinPS = this.findWinPS({ useAlternateBitness: true }); 121 | 122 | if (!altWinPS) { 123 | return configuredPowerShellPath; 124 | } 125 | 126 | const lowerAltWinPSPath = altWinPS.exePath.toLocaleLowerCase(); 127 | const lowerConfiguredPath = configuredPowerShellPath.toLocaleLowerCase(); 128 | 129 | if (lowerConfiguredPath === lowerAltWinPSPath) { 130 | return this.findWinPS().exePath; 131 | } 132 | 133 | return configuredPowerShellPath; 134 | } 135 | 136 | /** 137 | * Iterates through PowerShell installations on the machine according 138 | * to configuration passed in through the constructor. 139 | * PowerShell items returned by this object are verified 140 | * to exist on the filesystem. 141 | */ 142 | public *enumeratePowerShellInstallations(): Iterable { 143 | // Get the default PowerShell installations first 144 | for (const defaultPwsh of this.enumerateDefaultPowerShellInstallations()) { 145 | if (defaultPwsh && defaultPwsh.exists()) { 146 | yield defaultPwsh; 147 | } 148 | } 149 | 150 | // Also show any additionally configured PowerShells 151 | // These may be duplicates of the default installations, but given a different name. 152 | for (const additionalPwsh of this.enumerateAdditionalPowerShellInstallations()) { 153 | if (additionalPwsh && additionalPwsh.exists()) { 154 | yield additionalPwsh; 155 | } 156 | } 157 | } 158 | 159 | /** 160 | * Iterates through all the possible well-known PowerShell installations on a machine. 161 | * Returned values may not exist, but come with an .exists property 162 | * which will check whether the executable exists. 163 | */ 164 | private *enumerateDefaultPowerShellInstallations(): Iterable { 165 | // Find PSCore stable first 166 | yield this.findPSCoreStable(); 167 | 168 | switch (this.platformDetails.operatingSystem) { 169 | case OperatingSystem.Linux: 170 | // On Linux, find the snap 171 | yield this.findPSCoreStableSnap(); 172 | break; 173 | 174 | case OperatingSystem.Windows: 175 | // Windows may have a 32-bit pwsh.exe 176 | yield this.findPSCoreWindowsInstallation({ useAlternateBitness: true }); 177 | 178 | // Also look for the MSIX/UWP installation 179 | yield this.findPSCoreMsix(); 180 | 181 | break; 182 | } 183 | 184 | // Look for the .NET global tool 185 | // Some older versions of PowerShell have a bug in this where startup will fail, 186 | // but this is fixed in newer versions 187 | yield this.findPSCoreDotnetGlobalTool(); 188 | 189 | // Look for PSCore preview 190 | yield this.findPSCorePreview(); 191 | 192 | switch (this.platformDetails.operatingSystem) { 193 | // On Linux, there might be a preview snap 194 | case OperatingSystem.Linux: 195 | yield this.findPSCorePreviewSnap(); 196 | break; 197 | 198 | case OperatingSystem.Windows: 199 | // Find a preview MSIX 200 | yield this.findPSCoreMsix({ findPreview: true }); 201 | 202 | // Look for pwsh-preview with the opposite bitness 203 | yield this.findPSCoreWindowsInstallation({ useAlternateBitness: true, findPreview: true }); 204 | 205 | // Finally, get Windows PowerShell 206 | 207 | // Get the natural Windows PowerShell for the process bitness 208 | yield this.findWinPS(); 209 | 210 | // Get the alternate bitness Windows PowerShell 211 | yield this.findWinPS({ useAlternateBitness: true }); 212 | 213 | break; 214 | } 215 | } 216 | 217 | /** 218 | * Iterates through the configured additonal PowerShell executable locations, 219 | * without checking for their existence. 220 | */ 221 | private *enumerateAdditionalPowerShellInstallations(): Iterable { 222 | for (const additionalPwshSetting of this.additionalPSExeSettings) { 223 | yield new PossiblePowerShellExe(additionalPwshSetting.exePath, additionalPwshSetting.versionName); 224 | } 225 | } 226 | 227 | private findPSCoreStable(): IPossiblePowerShellExe { 228 | switch (this.platformDetails.operatingSystem) { 229 | case OperatingSystem.Linux: 230 | return new PossiblePowerShellExe(LinuxExePath, "PowerShell"); 231 | 232 | case OperatingSystem.MacOS: 233 | return new PossiblePowerShellExe(MacOSExePath, "PowerShell"); 234 | 235 | case OperatingSystem.Windows: 236 | return this.findPSCoreWindowsInstallation(); 237 | 238 | case OperatingSystem.Unknown: 239 | throw new Error("Unable to detect operating system"); 240 | } 241 | } 242 | 243 | private findPSCorePreview(): IPossiblePowerShellExe { 244 | switch (this.platformDetails.operatingSystem) { 245 | case OperatingSystem.Linux: 246 | return new PossiblePowerShellExe(LinuxPreviewExePath, "PowerShell Preview"); 247 | 248 | case OperatingSystem.MacOS: 249 | return new PossiblePowerShellExe(MacOSPreviewExePath, "PowerShell Preview"); 250 | 251 | case OperatingSystem.Windows: 252 | return this.findPSCoreWindowsInstallation({ findPreview: true }); 253 | 254 | case OperatingSystem.Unknown: 255 | throw new Error("Unable to detect operating system"); 256 | } 257 | } 258 | 259 | private findPSCoreDotnetGlobalTool(): IPossiblePowerShellExe { 260 | const exeName: string = this.platformDetails.operatingSystem === OperatingSystem.Windows 261 | ? "pwsh.exe" 262 | : "pwsh"; 263 | 264 | const dotnetGlobalToolExePath: string = path.join(os.homedir(), ".dotnet", "tools", exeName); 265 | 266 | return new PossiblePowerShellExe(dotnetGlobalToolExePath, ".NET Core PowerShell Global Tool"); 267 | } 268 | 269 | private findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}): IPossiblePowerShellExe { 270 | // We can't proceed if there's no LOCALAPPDATA path 271 | if (!process.env.LOCALAPPDATA) { 272 | return null; 273 | } 274 | 275 | // Find the base directory for MSIX application exe shortcuts 276 | const msixAppDir = path.join(process.env.LOCALAPPDATA, "Microsoft", "WindowsApps"); 277 | 278 | if (!fs.existsSync(msixAppDir)) { 279 | return null; 280 | } 281 | 282 | // Define whether we're looking for the preview or the stable 283 | const { pwshMsixDirRegex, pwshMsixName } = findPreview 284 | ? { pwshMsixDirRegex: PowerShellExeFinder.PwshPreviewMsixRegex, pwshMsixName: "PowerShell Preview (Store)" } 285 | : { pwshMsixDirRegex: PowerShellExeFinder.PwshMsixRegex, pwshMsixName: "PowerShell (Store)" }; 286 | 287 | // We should find only one such application, so return on the first one 288 | for (const subdir of fs.readdirSync(msixAppDir)) { 289 | if (pwshMsixDirRegex.test(subdir)) { 290 | const pwshMsixPath = path.join(msixAppDir, subdir, "pwsh.exe"); 291 | return new PossiblePowerShellExe(pwshMsixPath, pwshMsixName); 292 | } 293 | } 294 | 295 | // If we find nothing, return null 296 | return null; 297 | } 298 | 299 | private findPSCoreStableSnap(): IPossiblePowerShellExe { 300 | return new PossiblePowerShellExe(SnapExePath, "PowerShell Snap"); 301 | } 302 | 303 | private findPSCorePreviewSnap(): IPossiblePowerShellExe { 304 | return new PossiblePowerShellExe(SnapPreviewExePath, "PowerShell Preview Snap"); 305 | } 306 | 307 | private findPSCoreWindowsInstallation( 308 | { useAlternateBitness = false, findPreview = false }: 309 | { useAlternateBitness?: boolean; findPreview?: boolean } = {}): IPossiblePowerShellExe { 310 | 311 | const programFilesPath: string = this.getProgramFilesPath({ useAlternateBitness }); 312 | 313 | if (!programFilesPath) { 314 | return null; 315 | } 316 | 317 | const powerShellInstallBaseDir = path.join(programFilesPath, "PowerShell"); 318 | 319 | // Ensure the base directory exists 320 | if (!(fs.existsSync(powerShellInstallBaseDir) && fs.lstatSync(powerShellInstallBaseDir).isDirectory())) { 321 | return null; 322 | } 323 | 324 | let highestSeenVersion: number = -1; 325 | let pwshExePath: string = null; 326 | for (const item of fs.readdirSync(powerShellInstallBaseDir)) { 327 | 328 | let currentVersion: number = -1; 329 | if (findPreview) { 330 | // We are looking for something like "7-preview" 331 | 332 | // Preview dirs all have dashes in them 333 | const dashIndex = item.indexOf("-"); 334 | if (dashIndex < 0) { 335 | continue; 336 | } 337 | 338 | // Verify that the part before the dash is an integer 339 | const intPart: string = item.substring(0, dashIndex); 340 | if (!PowerShellExeFinder.IntRegex.test(intPart)) { 341 | continue; 342 | } 343 | 344 | // Verify that the part after the dash is "preview" 345 | if (item.substring(dashIndex + 1) !== "preview") { 346 | continue; 347 | } 348 | 349 | currentVersion = parseInt(intPart, 10); 350 | } else { 351 | // Search for a directory like "6" or "7" 352 | if (!PowerShellExeFinder.IntRegex.test(item)) { 353 | continue; 354 | } 355 | 356 | currentVersion = parseInt(item, 10); 357 | } 358 | 359 | // Ensure we haven't already seen a higher version 360 | if (currentVersion <= highestSeenVersion) { 361 | continue; 362 | } 363 | 364 | // Now look for the file 365 | const exePath = path.join(powerShellInstallBaseDir, item, "pwsh.exe"); 366 | if (!fs.existsSync(exePath)) { 367 | continue; 368 | } 369 | 370 | pwshExePath = exePath; 371 | highestSeenVersion = currentVersion; 372 | } 373 | 374 | if (!pwshExePath) { 375 | return null; 376 | } 377 | 378 | const bitness: string = programFilesPath.includes("x86") 379 | ? "(x86)" 380 | : "(x64)"; 381 | 382 | const preview: string = findPreview ? " Preview" : ""; 383 | 384 | return new PossiblePowerShellExe(pwshExePath, `PowerShell${preview} ${bitness}`); 385 | } 386 | 387 | private findWinPS({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): IPossiblePowerShellExe { 388 | 389 | // 32-bit OSes only have one WinPS on them 390 | if (!this.platformDetails.isOS64Bit && useAlternateBitness) { 391 | return null; 392 | } 393 | 394 | let winPS = useAlternateBitness ? this.alternateBitnessWinPS : this.winPS; 395 | if (winPS === undefined) { 396 | const systemFolderPath: string = this.getSystem32Path({ useAlternateBitness }); 397 | 398 | const winPSPath = path.join(systemFolderPath, "WindowsPowerShell", "v1.0", "powershell.exe"); 399 | 400 | let displayName: string; 401 | if (this.platformDetails.isProcess64Bit) { 402 | displayName = useAlternateBitness 403 | ? WindowsPowerShell32BitLabel 404 | : WindowsPowerShell64BitLabel; 405 | } else if (this.platformDetails.isOS64Bit) { 406 | displayName = useAlternateBitness 407 | ? WindowsPowerShell64BitLabel 408 | : WindowsPowerShell32BitLabel; 409 | } else { 410 | displayName = WindowsPowerShell32BitLabel; 411 | } 412 | 413 | winPS = new PossiblePowerShellExe(winPSPath, displayName, { knownToExist: true }); 414 | 415 | if (useAlternateBitness) { 416 | this.alternateBitnessWinPS = winPS; 417 | } else { 418 | this.winPS = winPS; 419 | } 420 | } 421 | 422 | return winPS; 423 | } 424 | 425 | private getProgramFilesPath( 426 | { useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string | null { 427 | 428 | if (!useAlternateBitness) { 429 | // Just use the native system bitness 430 | return process.env.ProgramFiles; 431 | } 432 | 433 | // We might be a 64-bit process looking for 32-bit program files 434 | if (this.platformDetails.isProcess64Bit) { 435 | return process.env["ProgramFiles(x86)"]; 436 | } 437 | 438 | // We might be a 32-bit process looking for 64-bit program files 439 | if (this.platformDetails.isOS64Bit) { 440 | return process.env.ProgramW6432; 441 | } 442 | 443 | // We're a 32-bit process on 32-bit Windows, there is no other Program Files dir 444 | return null; 445 | } 446 | 447 | private getSystem32Path({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string | null { 448 | const windir: string = process.env.windir; 449 | 450 | if (!useAlternateBitness) { 451 | // Just use the native system bitness 452 | return path.join(windir, "System32"); 453 | } 454 | 455 | // We might be a 64-bit process looking for 32-bit system32 456 | if (this.platformDetails.isProcess64Bit) { 457 | return path.join(windir, "SysWOW64"); 458 | } 459 | 460 | // We might be a 32-bit process looking for 64-bit system32 461 | if (this.platformDetails.isOS64Bit) { 462 | return path.join(windir, "Sysnative"); 463 | } 464 | 465 | // We're on a 32-bit Windows, so no alternate bitness 466 | return null; 467 | } 468 | } 469 | 470 | export function getWindowsSystemPowerShellPath(systemFolderName: string) { 471 | return path.join( 472 | process.env.windir, 473 | systemFolderName, 474 | "WindowsPowerShell", 475 | "v1.0", 476 | "powershell.exe"); 477 | } 478 | 479 | interface IPossiblePowerShellExe extends IPowerShellExeDetails { 480 | exists(): boolean; 481 | } 482 | 483 | class PossiblePowerShellExe implements IPossiblePowerShellExe { 484 | public readonly exePath: string; 485 | public readonly displayName: string; 486 | 487 | private knownToExist: boolean; 488 | 489 | constructor( 490 | pathToExe: string, 491 | installationName: string, 492 | { knownToExist = false }: { knownToExist?: boolean } = {}) { 493 | 494 | this.exePath = pathToExe; 495 | this.displayName = installationName; 496 | this.knownToExist = knownToExist || undefined; 497 | } 498 | 499 | public exists(): boolean { 500 | if (this.knownToExist === undefined) { 501 | this.knownToExist = fs.existsSync(this.exePath); 502 | } 503 | return this.knownToExist; 504 | } 505 | } 506 | --------------------------------------------------------------------------------